Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel
4f2cae5880 Map PostgreSQL port 2024-10-25 12:54:38 +02:00
Raphael Michel
c9913a0153 GH Actions: Fix PostgreSQL issues 2024-10-25 12:54:38 +02:00
275 changed files with 69232 additions and 73899 deletions

View File

@@ -38,7 +38,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install system dependencies - name: Install system dependencies
run: sudo apt update && sudo apt install -y gettext unzip run: sudo apt update && sudo apt install gettext unzip
- name: Install Python dependencies - name: Install Python dependencies
run: pip3 install -U setuptools build pip check-manifest run: pip3 install -U setuptools build pip check-manifest
- name: Run check-manifest - name: Run check-manifest

View File

@@ -37,7 +37,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install system packages - name: Install system packages
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
- name: Install Dependencies - name: Install Dependencies
run: pip3 install -Ur requirements.txt run: pip3 install -Ur requirements.txt
working-directory: ./doc working-directory: ./doc

View File

@@ -35,9 +35,9 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install system packages - name: Install system packages
run: sudo apt update && sudo apt -y install gettext run: sudo apt update && sudo apt install gettext
- name: Install Dependencies - name: Install Dependencies
run: pip3 install uv && uv pip install --system -e ".[dev]" run: pip3 install -e ".[dev]"
- name: Compile messages - name: Compile messages
run: python manage.py compilemessages run: python manage.py compilemessages
working-directory: ./src working-directory: ./src
@@ -62,7 +62,7 @@ jobs:
- name: Install system packages - name: Install system packages
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies - name: Install Dependencies
run: pip3 install uv && uv pip install --system -e ".[dev]" run: pip3 install -e ".[dev]"
- name: Spellcheck translations - name: Spellcheck translations
run: potypo run: potypo
working-directory: ./src working-directory: ./src

View File

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

View File

@@ -37,7 +37,7 @@ jobs:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: pretix POSTGRES_DB: pretix
options: >- options: >-
--health-cmd "pg_isready -U postgres -d pretix" --health-cmd pg_isready
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
@@ -56,9 +56,9 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Install system dependencies - name: Install system dependencies
run: sudo apt update && sudo apt install -y gettext run: sudo apt update && sudo apt install gettext
- name: Install Python dependencies - name: Install Python dependencies
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
- name: Run checks - name: Run checks
run: python manage.py check run: python manage.py check
working-directory: ./src working-directory: ./src
@@ -70,15 +70,15 @@ jobs:
run: make all compress run: make all compress
- name: Run tests - name: Run tests
working-directory: ./src working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100 run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
- name: Run concurrency tests - name: Run concurrency tests
working-directory: ./src working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
if: matrix.database == 'postgres' if: matrix.database == 'postgres'
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v1
with: with:
file: src/coverage.xml file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false fail_ci_if_error: true
if: matrix.database == 'postgres' && matrix.python-version == '3.11' if: matrix.database == 'postgres' && matrix.python-version == '3.11'

View File

@@ -10,7 +10,7 @@ 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 --maxfail=100 - PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
except: except:
- pypi - pypi
pypi: pypi:

View File

@@ -288,7 +288,6 @@ Example::
[django] [django]
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x- secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
debug=off debug=off
passwords_argon2=on
``secret`` ``secret``
The secret to be used by Django for signing and verification purposes. If this The secret to be used by Django for signing and verification purposes. If this
@@ -304,10 +303,6 @@ Example::
.. WARNING:: Never set this to ``True`` in production! .. WARNING:: Never set this to ``True`` in production!
``passwords_argon``
Use the ``argon2`` algorithm for password hashing. Disable on systems with a small number of CPU cores (currently
less than 8).
``profile`` ``profile``
Enable code profiling for a random subset of requests. Disabled by default, see Enable code profiling for a random subset of requests. Disabled by default, see
:ref:`perf-monitoring` for details. :ref:`perf-monitoring` for details.

View File

@@ -231,10 +231,11 @@ The following snippet is an example on how to configure a nginx proxy for pretix
} }
} }
server { server {
listen 443 ssl default_server; listen 443 default_server;
listen [::]:443 ipv6only=on ssl default_server; listen [::]:443 ipv6only=on default_server;
server_name pretix.mydomain.com; server_name pretix.mydomain.com;
ssl on;
ssl_certificate /path/to/cert.chain.pem; ssl_certificate /path/to/cert.chain.pem;
ssl_certificate_key /path/to/key.pem; ssl_certificate_key /path/to/key.pem;

View File

@@ -216,10 +216,11 @@ The following snippet is an example on how to configure a nginx proxy for pretix
} }
} }
server { server {
listen 443 ssl default_server; listen 443 default_server;
listen [::]:443 ipv6only=on ssl default_server; listen [::]:443 ipv6only=on default_server;
server_name pretix.mydomain.com; server_name pretix.mydomain.com;
ssl on;
ssl_certificate /path/to/cert.chain.pem; ssl_certificate /path/to/cert.chain.pem;
ssl_certificate_key /path/to/key.pem; ssl_certificate_key /path/to/key.pem;

View File

@@ -71,7 +71,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -113,7 +113,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -146,7 +146,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -167,7 +167,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -216,7 +216,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,

View File

@@ -31,6 +31,8 @@ subevent integer ID of the date
position_count integer Number of tickets that match this list (read-only). position_count integer Number of tickets that match this list (read-only).
checkin_count integer Number of check-ins performed on this list (read-only). checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state. include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in. allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan. allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged. rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
@@ -89,7 +91,10 @@ Endpoints
"allow_entry_after_exit": true, "allow_entry_after_exit": true,
"exit_all_at": null, "exit_all_at": null,
"rules": {}, "rules": {},
"addon_match": false "addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
} }
] ]
} }
@@ -141,7 +146,10 @@ Endpoints
"allow_entry_after_exit": true, "allow_entry_after_exit": true,
"exit_all_at": null, "exit_all_at": null,
"rules": {}, "rules": {},
"addon_match": false "addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
} }
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
@@ -238,7 +246,10 @@ Endpoints
"subevent": null, "subevent": null,
"allow_multiple_entries": false, "allow_multiple_entries": false,
"allow_entry_after_exit": true, "allow_entry_after_exit": true,
"addon_match": false "addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
} }
**Example response**: **Example response**:
@@ -260,7 +271,10 @@ Endpoints
"subevent": null, "subevent": null,
"allow_multiple_entries": false, "allow_multiple_entries": false,
"allow_entry_after_exit": true, "allow_entry_after_exit": true,
"addon_match": false "addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
} }
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for :param organizer: The ``slug`` field of the organizer of the event/item to create a list for
@@ -312,7 +326,10 @@ Endpoints
"subevent": null, "subevent": null,
"allow_multiple_entries": false, "allow_multiple_entries": false,
"allow_entry_after_exit": true, "allow_entry_after_exit": true,
"addon_match": false "addon_match": false,
"auto_checkin_sales_channels": [
"pretixpos"
]
} }
:param organizer: The ``slug`` field of the organizer to modify :param organizer: The ``slug`` field of the organizer to modify
@@ -325,7 +342,7 @@ Endpoints
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/ .. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
Delete a check-in list. **Note that this also deletes the information on all check-ins performed via this list.** Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
**Example request**: **Example request**:

View File

@@ -352,12 +352,12 @@ Fetching individual invoices
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch :param event: The ``slug`` field of the event to fetch
:param number: The ``number`` field of the invoice to fetch :param invoice_no: The ``invoice_no`` field of the invoice to fetch
:statuscode 200: no error :statuscode 200: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/download/ .. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/download/
Download an invoice in PDF format. Download an invoice in PDF format.
@@ -384,7 +384,7 @@ Fetching individual invoices
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch :param event: The ``slug`` field of the event to fetch
:param number: The ``number`` field of the invoice to fetch :param invoice_no: The ``invoice_no`` field of the invoice to fetch
:statuscode 200: no error :statuscode 200: no error
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
@@ -397,7 +397,7 @@ Modifying invoices
Invoices cannot be edited directly, but the following actions can be triggered: Invoices cannot be edited directly, but the following actions can be triggered:
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/reissue/ .. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
Cancels the invoice and creates a new one. Cancels the invoice and creates a new one.
@@ -419,13 +419,13 @@ Invoices cannot be edited directly, but the following actions can be triggered:
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch :param event: The ``slug`` field of the event to fetch
:param number: The ``number`` field of the invoice to reissue :param invoice_no: The ``invoice_no`` field of the invoice to reissue
:statuscode 200: no error :statuscode 200: no error
:statuscode 400: The invoice has already been canceled :statuscode 400: The invoice has already been canceled
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/regenerate/ .. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/regenerate/
Re-generates the invoice from order data. Re-generates the invoice from order data.
@@ -447,7 +447,7 @@ Invoices cannot be edited directly, but the following actions can be triggered:
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch :param event: The ``slug`` field of the event to fetch
:param number: The ``number`` field of the invoice to regenerate :param invoice_no: The ``invoice_no`` field of the invoice to regenerate
:statuscode 200: no error :statuscode 200: no error
:statuscode 400: The invoice has already been canceled :statuscode 400: The invoice has already been canceled
:statuscode 401: Authentication failure :statuscode 401: Authentication failure

View File

@@ -104,10 +104,6 @@ url string The full URL to
payments list of objects List of payment processes (see below) payments list of objects List of payment processes (see below)
refunds list of objects List of refund processes (see below) refunds list of objects List of refund processes (see below)
last_modified datetime Last modification of this object last_modified datetime Last modification of this object
cancellation_date datetime Time of order cancellation (or ``null``). **Note**:
Will not be set for partial cancellations and is not
reliable for orders that have been cancelled,
reactivated and cancelled again.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
@@ -155,9 +151,6 @@ cancellation_date datetime Time of order c
The ``expires`` attribute can now be passed during order creation. The ``expires`` attribute can now be passed during order creation.
.. versionchanged:: 2024.11
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
.. _order-position-resource: .. _order-position-resource:
@@ -213,17 +206,6 @@ checkins list of objects List of **succe
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful. ├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``. ├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system └ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
print_logs list of objects List of print jobs recorded e.g. by the pretix apps
├ id integer Internal ID of the print job
├ successful boolean Whether the print job successfully resulted in a print.
This is not expected to be 100 % reliable information (since
printer feedback is never perfect) and there is no guarantee
that unsuccessful jobs will be logged.
├ device_id integer Attribute ``device_id`` of the device that recorded the print. Can be ``null``.
├ datetime datetime Time of printing
├ source string Source of print job, e.g. name of the app used.
├ type string Type of print (currently ``badge``, ``ticket``, ``certificate``, or ``other``)
└ info object Additional data with client-dependent structure.
downloads list of objects List of ticket download options downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``) ├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL └ url string Download URL
@@ -251,10 +233,6 @@ pdf_data object Data object req
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added. The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
.. versionchanged:: 2024.9
The attribute ``print_logs`` has been added.
.. _order-payment-resource: .. _order-payment-resource:
Order payment resource Order payment resource
@@ -421,21 +399,10 @@ List of all orders
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -471,15 +438,14 @@ List of all orders
"provider": "banktransfer" "provider": "banktransfer"
} }
], ],
"refunds": [], "refunds": []
"cancellation_date": null
} }
] ]
} }
:query integer page: The page number in case of a multi-page result set, default is 1 :query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``, :query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
``last_modified``, ``status`` and ``cancellation_date``. Default: ``datetime`` ``last_modified``, and ``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code :query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above) :query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names) :query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
@@ -660,22 +626,10 @@ Fetching individual orders
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -711,8 +665,7 @@ Fetching individual orders
"provider": "banktransfer" "provider": "banktransfer"
} }
], ],
"refunds": [], "refunds": []
"cancellation_date": null
} }
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
@@ -1024,8 +977,8 @@ Creating orders
* ``internal_reference`` * ``internal_reference``
* ``vat_id`` * ``vat_id``
* ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check * ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well! trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``positions`` * ``positions``
@@ -1628,22 +1581,10 @@ List of all order positions
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -1754,22 +1695,10 @@ Fetching individual positions
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -1866,10 +1795,6 @@ Manipulating individual positions
The endpoints to manage blocks have been added. The endpoints to manage blocks have been added.
.. versionchanged:: 2024.9
The API now supports logging ticket and badge prints.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/ .. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Updates specific fields on an order position. Currently, only the following fields are supported: Updates specific fields on an order position. Currently, only the following fields are supported:
@@ -2129,59 +2054,6 @@ Manipulating individual positions
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/printlog/
Creates a print log, stating that this ticket has been printed.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/printlog/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"datetime": "2024-09-19T13:37:00+02:00",
"source": "pretixPOS",
"type": "badge",
"info": {
"cashier": 1234
}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/pdf
{
"id": 1234,
"device_id": null,
"datetime": "2024-09-19T13:37:00+02:00",
"source": "pretixPOS",
"type": "badge",
"info": {
"cashier": 1234
}
}
:param organizer: The ``slug`` field of the organizer to create a log for
:param event: The ``slug`` field of the event to create a log for
:param id: The ``id`` field of the order position to create a log for
:statuscode 201: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
**or** downloads are not available for this order position at this time. The response content will
contain more details.
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Changing order contents Changing order contents
----------------------- -----------------------

View File

@@ -313,7 +313,7 @@ Endpoints for event exports
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
Endpoints for organizer exports Endpoints for organizer exports
------------------------------- ---------------------------
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/ .. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
@@ -553,4 +553,4 @@ Endpoints for organizer exports
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource. :statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3 .. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3

View File

@@ -1,4 +1,4 @@
.. _`rest-seats`: .. _`rest-reusablemedia`:
Seats Seats
===== =====

View File

@@ -17,7 +17,6 @@ First, you need to declare that you are using non-essential cookies by respondin
signal: signal:
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: register_cookie_providers :members: register_cookie_providers
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class: You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:

View File

@@ -22,14 +22,12 @@ Order events
There are multiple signals that will be sent out in the ordering cycle: There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text :members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins Check-ins
""""""""" """""""""
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: checkin_created :members: checkin_created
@@ -41,21 +39,18 @@ Frontend
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request :members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
Request flow Request flow
"""""""""""" """"""""""""
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: process_request, process_response :members: process_request, process_response
Vouchers Vouchers
"""""""" """"""""
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: voucher_redeem_info :members: voucher_redeem_info
Backend Backend
@@ -67,28 +62,24 @@ Backend
item_formsets, order_search_filter_q, order_search_forms item_formsets, order_search_filter_q, order_search_forms
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in :members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
Vouchers Vouchers
"""""""" """"""""
.. automodule:: pretix.control.signals .. automodule:: pretix.control.signals
:no-index:
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation :members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
Dashboards Dashboards
"""""""""" """"""""""
.. automodule:: pretix.control.signals .. automodule:: pretix.control.signals
:no-index:
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top :members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
Ticket designs Ticket designs
"""""""""""""" """"""""""""""
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: layout_text_variables, layout_image_variables :members: layout_text_variables, layout_image_variables
.. automodule:: pretix.plugins.ticketoutputpdf.signals .. automodule:: pretix.plugins.ticketoutputpdf.signals
@@ -98,9 +89,4 @@ API
--- ---
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: validate_event_settings, api_event_settings_fields :members: validate_event_settings, api_event_settings_fields
.. automodule:: pretix.api.signals
:no-index:
:members: register_device_security_profile

View File

@@ -60,7 +60,6 @@ that we'll provide in this plugin:
Similar signals exist for other objects: Similar signals exist for other objects:
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: voucher_import_columns :members: voucher_import_columns

View File

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

View File

@@ -86,10 +86,7 @@ Signals
------- -------
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: register_text_placeholders :members: register_text_placeholders
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: register_mail_placeholders :members: register_mail_placeholders

View File

@@ -1,5 +1,5 @@
KulturPass KulturPass
========== =========
.. note:: .. note::

View File

@@ -158,7 +158,7 @@ expects and - more importantly - supports.
for a sample configuration in an academic context. for a sample configuration in an academic context.
Note, that you can have multiple attributes with the same ``friendlyName`` Note, that you can have multiple attributes with the same ``friendlyName``
but different ``name`` value. This is often used in systems, where the same but different ``name``s. This is often used in systems, where the same
information (for example a persons name) is saved in different fields - information (for example a persons name) is saved in different fields -
for example because one institution is returning SAML 1.0 and other for example because one institution is returning SAML 1.0 and other
institutions are returning SAML 2.0 style attributes. Typically, this only institutions are returning SAML 2.0 style attributes. Typically, this only

View File

@@ -29,7 +29,7 @@ dependencies = [
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab "arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel", "babel",
"BeautifulSoup4==4.12.*", "BeautifulSoup4==4.12.*",
"bleach==6.2.*", "bleach==5.0.*",
"celery==5.4.*", "celery==5.4.*",
"chardet==5.2.*", "chardet==5.2.*",
"cryptography>=3.4.2", "cryptography>=3.4.2",
@@ -43,7 +43,7 @@ dependencies = [
"django-formset-js-improved==0.5.0.3", "django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1", "django-formtools==2.5.1",
"django-hierarkey==1.2.*", "django-hierarkey==1.2.*",
"django-hijack==3.7.*", "django-hijack==3.6.*",
"django-i18nfield==1.9.*,>=1.9.4", "django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9", "django-libsass==0.9",
"django-localflavor==4.0", "django-localflavor==4.0",
@@ -74,25 +74,26 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*", "paypal-checkout-serversdk==1.0.*",
"PyJWT==2.9.*", "PyJWT==2.9.*",
"phonenumberslite==8.13.*", "phonenumberslite==8.13.*",
"Pillow==11.0.*", "Pillow==10.4.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==5.28.*", "protobuf==5.28.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
"pycparser==2.22", "pycparser==2.22",
"pycryptodome==3.21.*", "pycryptodome==3.21.*",
"pypdf==5.1.*", "pypdf==5.0.*",
"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",
"pytz-deprecation-shim==0.1.*", "pytz-deprecation-shim==0.1.*",
"pyuca", "pyuca",
"qrcode==8.0", "qrcode==8.0",
"redis==5.2.*", "redis==5.1.*",
"reportlab==4.2.*", "reportlab==4.2.*",
"requests==2.31.*", "requests==2.31.*",
"sentry-sdk==2.18.*", "sentry-sdk==2.17.*",
"sepaxml==2.6.*", "sepaxml==2.6.*",
"slimit",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
"tlds>=2020041600", "tlds>=2020041600",
@@ -101,13 +102,13 @@ dependencies = [
"vat_moss_forked==2020.3.20.0.11.0", "vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*", "vobject==0.9.*",
"webauthn==2.2.*", "webauthn==2.2.*",
"zeep==4.3.*" "zeep==4.2.*"
] ]
[project.optional-dependencies] [project.optional-dependencies]
memcached = ["pylibmc"] memcached = ["pylibmc"]
dev = [ dev = [
"aiohttp==3.11.*", "aiohttp==3.10.*",
"coverage", "coverage",
"coveralls", "coveralls",
"fakeredis==2.26.*", "fakeredis==2.26.*",
@@ -116,11 +117,12 @@ dev = [
"isort==5.13.*", "isort==5.13.*",
"pep8-naming==0.14.*", "pep8-naming==0.14.*",
"potypo", "potypo",
"pytest-asyncio>=0.24", "pytest-asyncio",
"pytest-cache", "pytest-cache",
"pytest-cov", "pytest-cov",
"pytest-django==4.*", "pytest-django==4.*",
"pytest-mock==3.14.*", "pytest-mock==3.14.*",
"pytest-rerunfailures==14.*",
"pytest-sugar", "pytest-sugar",
"pytest-xdist==3.6.*", "pytest-xdist==3.6.*",
"pytest==8.3.*", "pytest==8.3.*",

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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__ = "2024.11.0" __version__ = "2024.10.0.dev0"

View File

@@ -27,7 +27,7 @@ from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from pretix.api.auth.devicesecurity import ( from pretix.api.auth.devicesecurity import (
FullAccessSecurityProfile, get_all_security_profiles, DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
) )
from pretix.base.models import Device from pretix.base.models import Device
@@ -58,8 +58,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate(self, request): def authenticate(self, request):
r = super().authenticate(request) r = super().authenticate(request)
if r and isinstance(r[1], Device): if r and isinstance(r[1], Device):
profiles = get_all_security_profiles() profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
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

View File

@@ -20,40 +20,13 @@
# <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.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.api.signals import register_device_security_profile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ALL_PROFILES = None
class BaseSecurityProfile: class FullAccessSecurityProfile:
@property
def identifier(self) -> str:
"""
Unique identifier for this profile.
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
Human-readable name (can be a ``gettext_lazy`` object).
"""
raise NotImplementedError()
def is_allowed(self, request) -> bool:
"""
Return whether a given request should be allowed.
"""
raise NotImplementedError()
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)')
@@ -61,7 +34,7 @@ class FullAccessSecurityProfile(BaseSecurityProfile):
return True return True
class AllowListSecurityProfile(BaseSecurityProfile): class AllowListSecurityProfile:
allowlist = () allowlist = ()
def is_allowed(self, request): def is_allowed(self, request):
@@ -104,7 +77,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:order-list'), ('GET', 'api-v1:order-list'),
('GET', 'api-v1:orderposition-pdf_image'), ('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('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'),
@@ -140,7 +112,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:revokedsecrets-list'), ('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'), ('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('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'),
@@ -176,7 +147,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:revokedsecrets-list'), ('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'), ('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('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'),
@@ -184,28 +154,87 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
) )
def get_all_security_profiles(): class PretixPosSecurityProfile(AllowListSecurityProfile):
global _ALL_PROFILES identifier = 'pretixpos'
verbose_name = _('pretixPOS')
if _ALL_PROFILES: allowlist = (
return _ALL_PROFILES ('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
types = OrderedDict() ('GET', 'api-v1:idempotency.query'),
for recv, ret in register_device_security_profile.send(None): ('GET', 'api-v1:device.info'),
if isinstance(ret, (list, tuple)): ('POST', 'api-v1:device.update'),
for r in ret: ('POST', 'api-v1:device.revoke'),
types[r.identifier] = r ('POST', 'api-v1:device.roll'),
else: ('GET', 'api-v1:event-list'),
types[ret.identifier] = ret ('GET', 'api-v1:event-detail'),
_ALL_PROFILES = types ('GET', 'api-v1:subevent-list'),
return types ('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles") ('GET', 'api-v1:question-list'),
def register_default_webhook_events(sender, **kwargs): ('GET', 'api-v1:quota-list'),
return ( ('GET', 'api-v1:taxrule-list'),
FullAccessSecurityProfile(), ('GET', 'api-v1:ticketlayout-list'),
PretixScanSecurityProfile(), ('GET', 'api-v1:ticketlayoutitem-list'),
PretixScanNoSyncSecurityProfile(), ('GET', 'api-v1:badgelayout-list'),
PretixScanNoSyncNoSearchSecurityProfile(), ('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:voucher-list'),
('GET', 'api-v1:voucher-detail'),
('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('PATCH', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:order-mark-canceled'),
('POST', 'api-v1:orderpayment-list'),
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('POST', 'api-v1:cartposition-bulk-create'),
('GET', 'api-v1:checkinlist-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
('POST', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
('GET', 'plugins:pretix_posbackend:poscashier-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
('PUT', 'plugins:pretix_posbackend:file.upload'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:event.settings'),
('GET', 'plugins:pretix_seating:event.event'),
('GET', 'plugins:pretix_seating:event.event.subevent'),
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-list'),
) )
DEVICE_SECURITY_PROFILES = {
k.identifier: k() for k in (
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixScanNoSyncNoSearchSecurityProfile,
PretixPosSecurityProfile,
)
}

View File

@@ -235,7 +235,7 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
return cid return cid
def create(self, validated_data): def create(self, validated_data):
validated_data.pop('sales_channel', None) validated_data.pop('sales_channel')
addons_data = validated_data.pop('addons', None) addons_data = validated_data.pop('addons', None)
bundled_data = validated_data.pop('bundled', None) bundled_data = validated_data.pop('bundled', None)

View File

@@ -26,22 +26,31 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.event import SubEventSerializer from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.media import MEDIA_TYPES from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, CheckinList from pretix.base.models import Checkin, CheckinList, SalesChannel
class CheckinListSerializer(I18nAwareModelSerializer): class CheckinListSerializer(I18nAwareModelSerializer):
checkin_count = serializers.IntegerField(read_only=True) checkin_count = serializers.IntegerField(read_only=True)
position_count = serializers.IntegerField(read_only=True) position_count = serializers.IntegerField(read_only=True)
auto_checkin_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta: class Meta:
model = CheckinList model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count', fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending', 'allow_multiple_entries', 'allow_entry_after_exit', 'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used') 'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
if 'subevent' in self.context['request'].query_params.getlist('expand'): if 'subevent' in self.context['request'].query_params.getlist('expand'):
self.fields['subevent'] = SubEventSerializer(read_only=True) self.fields['subevent'] = SubEventSerializer(read_only=True)

View File

@@ -55,7 +55,7 @@ from pretix.base.models import (
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund, BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
PrintLog, RevokedTicketSecret, RevokedTicketSecret,
) )
from pretix.base.pdf import get_images, get_variables from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages from pretix.base.services.cart import error_messages
@@ -284,26 +284,6 @@ class CheckinSerializer(I18nAwareModelSerializer):
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type') fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
class PrintLogSerializer(serializers.ModelSerializer):
device_id = serializers.SlugRelatedField(
source='device',
slug_field='device_id',
read_only=True,
)
class Meta:
model = PrintLog
fields = (
"id",
"successful",
"datetime",
"source",
"type",
"device_id",
"info",
)
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)
@@ -496,7 +476,6 @@ class OrderPositionListSerializer(serializers.ListSerializer):
class OrderPositionSerializer(I18nAwareModelSerializer): class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True) checkins = CheckinSerializer(many=True, read_only=True)
print_logs = PrintLogSerializer(many=True, read_only=True)
answers = AnswerSerializer(many=True) answers = AnswerSerializer(many=True)
downloads = PositionDownloadsField(source='*', read_only=True) downloads = PositionDownloadsField(source='*', read_only=True)
order = serializers.SlugRelatedField(slug_field='code', read_only=True) order = serializers.SlugRelatedField(slug_field='code', read_only=True)
@@ -511,7 +490,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount', 'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use') 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
read_only_fields = ( read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret', 'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
@@ -598,9 +577,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval', 'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
'valid_from', 'valid_until', 'blocked') 'blocked')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -753,12 +732,12 @@ class OrderSerializer(I18nAwareModelSerializer):
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date', 'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads', 'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date' 'url', 'customer', 'valid_if_pending', 'api_meta'
) )
read_only_fields = ( read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date', 'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer', 'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date' 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -1515,7 +1494,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.answers = answers pos.answers = answers
pos.pseudonymization_id = "PREVIEW" pos.pseudonymization_id = "PREVIEW"
pos.checkins = [] pos.checkins = []
pos.print_logs = []
pos_map[pos.positionid] = pos pos_map[pos.positionid] = pos
else: else:
if pos.voucher: if pos.voucher:

View File

@@ -29,7 +29,6 @@ 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
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.serializers import AsymmetricField from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField from pretix.api.serializers.order import CompatibleJSONField
@@ -298,7 +297,6 @@ class DeviceSerializer(serializers.ModelSerializer):
revoked = serializers.BooleanField(read_only=True) revoked = serializers.BooleanField(read_only=True)
initialized = serializers.DateTimeField(read_only=True) initialized = serializers.DateTimeField(read_only=True)
initialization_token = serializers.DateTimeField(read_only=True) initialization_token = serializers.DateTimeField(read_only=True)
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
class Meta: class Meta:
model = Device model = Device
@@ -308,10 +306,6 @@ class DeviceSerializer(serializers.ModelSerializer):
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile' 'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
class TeamInviteSerializer(serializers.ModelSerializer): class TeamInviteSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@@ -32,17 +32,10 @@ from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal() register_webhook_events = Signal()
""" """
This signal is sent out to get all known webhook events. Receivers should return an This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
instances. instances.
""" """
register_device_security_profile = Signal()
"""
This signal is sent out to get all known device security_profiles. Receivers should
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
or a list of such instances.
"""
@receiver(periodic_task) @receiver(periodic_task)
@scopes_disabled() @scopes_disabled()

View File

@@ -62,7 +62,6 @@ from pretix.base.models import (
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition, CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken, Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
) )
from pretix.base.models.orders import PrintLog
from pretix.base.services.checkin import ( from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin, CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
) )
@@ -116,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
if 'subevent' in self.request.query_params.getlist('expand'): if 'subevent' in self.request.query_params.getlist('expand'):
qs = qs.prefetch_related( qs = qs.prefetch_related(
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set', 'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
'subevent__seat_category_mappings', 'subevent__meta_values', 'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
) )
return qs return qs
@@ -143,9 +142,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
data=self.request.data data=self.request.data
) )
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
instance.checkins.all().delete()
instance.log_action( instance.log_action(
'pretix.event.checkinlist.deleted', 'pretix.event.checkinlist.deleted',
user=self.request.user, user=self.request.user,
@@ -368,9 +365,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch( Prefetch(
lookup='checkins', lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device') queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
), ),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')), Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related( Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
@@ -382,7 +378,6 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
'positions', 'positions',
OrderPosition.objects.prefetch_related( OrderPosition.objects.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'item', 'variation', 'answers', 'answers__options', 'answers__question', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
) )
) )
@@ -394,9 +389,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch( Prefetch(
lookup='checkins', lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device') queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
), ),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')) Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat') ).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')

View File

@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import base64 import base64
import copy
import logging import logging
from cryptography.hazmat.backends.openssl.backend import Backend from cryptography.hazmat.backends.openssl.backend import Backend
@@ -147,8 +146,6 @@ class InitializeView(APIView):
permission_classes = () permission_classes = ()
def post(self, request, format=None): def post(self, request, format=None):
from pretix.base.signals import device_info_updated
serializer = InitializationRequestSerializer(data=request.data) serializer = InitializationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@@ -163,8 +160,6 @@ class InitializeView(APIView):
if device.revoked: if device.revoked:
raise ValidationError({'token': ['This initialization token has been revoked.']}) raise ValidationError({'token': ['This initialization token has been revoked.']})
old_instance = copy.copy(device)
device.initialized = now() device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand') device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model') device.hardware_model = serializer.validated_data.get('hardware_model')
@@ -179,10 +174,6 @@ class InitializeView(APIView):
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device) device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
device_info_updated.send(
sender=Device, old_device=old_instance, new_device=device
)
serializer = DeviceSerializer(device) serializer = DeviceSerializer(device)
return Response(serializer.data) return Response(serializer.data)
@@ -191,12 +182,9 @@ class UpdateView(APIView):
authentication_classes = (DeviceTokenAuthentication,) authentication_classes = (DeviceTokenAuthentication,)
def post(self, request, format=None): def post(self, request, format=None):
from pretix.base.signals import device_info_updated
serializer = UpdateRequestSerializer(data=request.data) serializer = UpdateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
device = request.auth device = request.auth
old_instance = copy.copy(device)
device.hardware_brand = serializer.validated_data.get('hardware_brand') device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model') device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name') device.os_name = serializer.validated_data.get('os_name')
@@ -212,8 +200,9 @@ class UpdateView(APIView):
device.save() device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device) device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
from ...base.signals import device_info_updated
device_info_updated.send( device_info_updated.send(
sender=Device, old_device=old_instance, new_device=device sender=Device, old_device=request.auth, new_device=device
) )
serializer = DeviceSerializer(device) serializer = DeviceSerializer(device)

View File

@@ -42,7 +42,6 @@ from pretix.base.models import (
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition, Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
ReusableMedium, ReusableMedium,
) )
from pretix.base.models.orders import PrintLog
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
@@ -80,7 +79,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
'order', 'order__event', 'order__event__organizer', 'seat', 'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related( ).prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
) )
), ),

View File

@@ -57,8 +57,7 @@ from pretix.api.serializers.order import (
OrderPaymentCreateSerializer, OrderPaymentSerializer, OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer, OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer, OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
PrintLogSerializer, RevokedTicketSecretSerializer, RevokedTicketSecretSerializer, SimulatedOrderSerializer,
SimulatedOrderSerializer,
) )
from pretix.api.serializers.orderchange import ( from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer, BlockNameSerializer, OrderChangeOperationSerializer,
@@ -76,7 +75,7 @@ from pretix.base.models import (
TeamAPIToken, generate_secret, TeamAPIToken, generate_secret,
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret, BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
) )
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
from pretix.base.pdf import get_images from pretix.base.pdf import get_images
@@ -215,7 +214,7 @@ class OrderViewSetMixin:
queryset = Order.objects.none() queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter) filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('datetime',) ordering = ('datetime',)
ordering_fields = ('datetime', 'code', 'status', 'last_modified', 'cancellation_date') ordering_fields = ('datetime', 'code', 'status', 'last_modified')
filterset_class = OrderFilter filterset_class = OrderFilter
lookup_field = 'code' lookup_field = 'code'
@@ -260,7 +259,6 @@ class OrderViewSetMixin:
'positions', 'positions',
opq.all().prefetch_related( opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
)), )),
@@ -282,7 +280,6 @@ class OrderViewSetMixin:
'positions', 'positions',
opq.all().prefetch_related( opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'item', 'variation', 'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')), Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat', 'seat',
@@ -1096,7 +1093,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
) )
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")), Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached') to_attr='meta_values_cached')
@@ -1140,7 +1136,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
else: else:
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")), Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
).select_related( ).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat' 'item', 'order', 'order__event', 'order__event__organizer', 'seat'
@@ -1259,34 +1254,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
) )
return resp return resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
pos = self.get_object()
serializer = PrintLogSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(
position=pos,
device=request.auth if isinstance(request.auth, Device) else None,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
)
pos.order.log_action(
"pretix.event.order.print",
data={
"position": pos.pk,
"positionid": pos.positionid,
**serializer.validated_data,
},
auth=request.auth,
user=request.user,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)') @action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
def pdf_image(self, request, key, **kwargs): def pdf_image(self, request, key, **kwargs):
pos = self.get_object() pos = self.get_object()

View File

@@ -152,7 +152,7 @@ class NativeAuthBackend(BaseAuthBackend):
to log in. to log in.
""" """
d = OrderedDict([ d = OrderedDict([
('email', forms.EmailField(label=_("Email"), max_length=254, ('email', forms.EmailField(label=_("E-mail"), max_length=254,
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))), widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput, ('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
max_length=4096)), max_length=4096)),

View File

@@ -68,7 +68,7 @@ def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
class BaseHTMLMailRenderer: class BaseHTMLMailRenderer:
""" """
This is the base class for all HTML email renderers. This is the base class for all HTML e-mail renderers.
""" """
def __init__(self, event: Event, organizer=None): def __init__(self, event: Event, organizer=None):

View File

@@ -64,7 +64,7 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
_('Customer ID'), _('Customer ID'),
_('SSO provider'), _('SSO provider'),
_('External identifier'), _('External identifier'),
_('Email'), _('E-mail'),
_('Phone number'), _('Phone number'),
_('Full name'), _('Full name'),
] ]

View File

@@ -199,7 +199,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Invoice number'), _('Invoice number'),
_('Date'), _('Date'),
_('Order code'), _('Order code'),
_('Email address'), _('E-mail address'),
_('Invoice type'), _('Invoice type'),
_('Cancellation of'), _('Cancellation of'),
_('Language'), _('Language'),
@@ -326,7 +326,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Event start date'), _('Event start date'),
_('Date'), _('Date'),
_('Order code'), _('Order code'),
_('Email address'), _('E-mail address'),
_('Invoice type'), _('Invoice type'),
_('Cancellation of'), _('Cancellation of'),
_('Invoice sender:') + ' ' + _('Name'), _('Invoice sender:') + ' ' + _('Name'),

View File

@@ -284,7 +284,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Comment')) headers.append(_('Comment'))
headers.append(_('Follow-up date')) headers.append(_('Follow-up date'))
headers.append(_('Positions')) headers.append(_('Positions'))
headers.append(_('Email address verified')) headers.append(_('E-mail address verified'))
headers.append(_('External customer ID')) headers.append(_('External customer ID'))
headers.append(_('Payment providers')) headers.append(_('Payment providers'))
if form_data.get('include_payment_amounts'): if form_data.get('include_payment_amounts'):
@@ -655,7 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
headers += [ headers += [
_('Sales channel'), _('Sales channel'),
_('Order locale'), _('Order locale'),
_('Email address verified'), _('E-mail address verified'),
_('External customer ID'), _('External customer ID'),
_('Check-in lists'), _('Check-in lists'),
_('Payment providers'), _('Payment providers'),

View File

@@ -254,7 +254,7 @@ class PasswordRecoverForm(forms.Form):
class PasswordForgotForm(forms.Form): class PasswordForgotForm(forms.Form):
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), label=_('E-mail'),
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -48,10 +48,10 @@ from pretix.control.forms import SingleLanguageWidget
class UserSettingsForm(forms.ModelForm): class UserSettingsForm(forms.ModelForm):
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 e-mail address. "
"Please choose a different one."), "Please choose a different one."),
'pw_current': _("Please enter your current password if you want to change your email address " 'pw_current': _("Please enter your current password if you want to change your e-mail "
"or password."), "address or password."),
'pw_current_wrong': _("The current password you entered was not correct."), 'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"), 'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."), 'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),

View File

@@ -289,7 +289,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
def _clean_text(self, text, tags=None): def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean( return self._normalize(bleach.clean(
text, text,
tags=set(tags) if tags else set() tags=tags or []
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')) ).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
@@ -461,7 +461,7 @@ 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)
txt = bleach.clean(txt, tags=set()).strip() txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal']) p = Paragraph(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)

View File

@@ -37,16 +37,6 @@ class BaseMediaType:
def verbose_name(self): def verbose_name(self):
raise NotImplementedError() raise NotImplementedError()
@property
def icon(self):
"""
This can be:
- The name of a Font Awesome icon to represent this channel type.
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
"""
return "circle"
def generate_identifier(self, organizer): def generate_identifier(self, organizer):
if self.medium_created_by_server: if self.medium_created_by_server:
raise NotImplementedError() raise NotImplementedError()
@@ -69,7 +59,6 @@ class BaseMediaType:
class BarcodePlainMediaType(BaseMediaType): class BarcodePlainMediaType(BaseMediaType):
identifier = 'barcode' identifier = 'barcode'
verbose_name = _('Barcode / QR-Code') verbose_name = _('Barcode / QR-Code')
icon = 'qrcode'
medium_created_by_server = True medium_created_by_server = True
supports_giftcard = False supports_giftcard = False
supports_orderposition = True supports_orderposition = True
@@ -86,7 +75,6 @@ class BarcodePlainMediaType(BaseMediaType):
class NfcUidMediaType(BaseMediaType): class NfcUidMediaType(BaseMediaType):
identifier = 'nfc_uid' identifier = 'nfc_uid'
verbose_name = _('NFC UID-based') verbose_name = _('NFC UID-based')
icon = 'pretixbase/img/media/nfc_uid.svg'
medium_created_by_server = False medium_created_by_server = False
supports_giftcard = True supports_giftcard = True
supports_orderposition = False supports_orderposition = False
@@ -126,7 +114,6 @@ class NfcUidMediaType(BaseMediaType):
class NfcMf0aesMediaType(BaseMediaType): class NfcMf0aesMediaType(BaseMediaType):
identifier = 'nfc_mf0aes' identifier = 'nfc_mf0aes'
verbose_name = 'NFC Mifare Ultralight AES' verbose_name = 'NFC Mifare Ultralight AES'
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 = False supports_orderposition = False

View File

@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
('password', models.CharField(verbose_name='password', max_length=128)), ('password', models.CharField(verbose_name='password', max_length=128)),
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)), ('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')), ('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='Email', null=True, ('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
db_index=True)), db_index=True)),
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)), ('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)), ('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),

View File

@@ -9,7 +9,6 @@ from decimal import Decimal
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
import i18nfield.fields import i18nfield.fields
from argon2.exceptions import HashingError
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.db import migrations, models from django.db import migrations, models
@@ -26,14 +25,7 @@ def initial_user(apps, schema_editor):
user = User(email='admin@localhost') user = User(email='admin@localhost')
user.is_staff = True user.is_staff = True
user.is_superuser = True user.is_superuser = True
try: user.password = make_password('admin')
user.password = make_password('admin')
except HashingError:
raise Exception(
"Could not hash password of initial user with argon2id. If this is a system with less than 8 CPU cores, "
"you might need to disable argon2id by setting `passwords_argon2=off` in the `[django]` section of the "
"pretix.cfg configuration file."
)
user.save() user.save()
@@ -56,7 +48,7 @@ class Migration(migrations.Migration):
('password', models.CharField(max_length=128, verbose_name='password')), ('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='Email')), ('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')), ('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')), ('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
@@ -240,7 +232,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=16, verbose_name='Order code')), ('code', models.CharField(max_length=16, verbose_name='Order code')),
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')), ('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')), ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')), ('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)), ('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
('datetime', models.DateTimeField(verbose_name='Date')), ('datetime', models.DateTimeField(verbose_name='Date')),

View File

@@ -187,7 +187,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=16, verbose_name='Order code')), ('code', models.CharField(max_length=16, verbose_name='Order code')),
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')), ('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')), ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')), ('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)), ('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
('datetime', models.DateTimeField(verbose_name='Date')), ('datetime', models.DateTimeField(verbose_name='Date')),

View File

@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
('email', models.EmailField(max_length=254, verbose_name='Email address')), ('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
('locale', models.CharField(default='en', max_length=190)), ('locale', models.CharField(default='en', max_length=190)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')), ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')), ('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),

View File

@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
('email', models.EmailField(max_length=254, verbose_name='Email address')), ('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
('locale', models.CharField(default='en', max_length=190)), ('locale', models.CharField(default='en', max_length=190)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')), ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')), ('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),

View File

@@ -163,7 +163,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)), ('action_type', models.CharField(max_length=255)),
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)), ('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, ('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='pretixbase.Event')), to='pretixbase.Event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),

View File

@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)), ('action_type', models.CharField(max_length=255)),
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)), ('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')), ('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('enabled', models.BooleanField(default=True)), ('enabled', models.BooleanField(default=True)),

View File

@@ -1,79 +0,0 @@
# Generated by Django 4.2.16 on 2024-09-19 10:41
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
("pretixbase", "0271_itemcategory_cross_selling"),
]
operations = [
migrations.CreateModel(
name="PrintLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("successful", models.BooleanField(default=True)),
("source", models.CharField(max_length=255)),
("type", models.CharField(max_length=255)),
("info", models.JSONField(default=dict)),
(
"api_token",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.teamapitoken",
),
),
(
"device",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="print_logs",
to="pretixbase.device",
),
),
(
"oauth_application",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
),
),
(
"position",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="print_logs",
to="pretixbase.orderposition",
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="print_logs",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ("-datetime",),
},
),
]

View File

@@ -1,48 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-29 15:03
from django.db import migrations
def migrate_autocheckin(apps, schema_editor):
CheckinList = apps.get_model("pretixbase", "CheckinList")
AutoCheckinRule = apps.get_model("autocheckin", "AutoCheckinRule")
for cl in CheckinList.objects.filter(auto_checkin_sales_channels__isnull=False).select_related("event", "event__organizer"):
sales_channels = cl.auto_checkin_sales_channels.all()
all_sales_channels = cl.event.organizer.sales_channels.all()
if "pretix.plugins.autocheckin" not in cl.event.plugins:
cl.event.plugins = cl.event.plugins + ",pretix.plugins.autocheckin"
cl.event.save()
r = AutoCheckinRule.objects.get_or_create(
list=cl,
event=cl.event,
all_products=True,
all_payment_methods=True,
defaults=dict(
mode="placed",
all_sales_channels=len(sales_channels) == len(all_sales_channels),
)
)[0]
if len(sales_channels) != len(all_sales_channels):
r.limit_sales_channels.set(sales_channels)
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0272_printlog"),
("autocheckin", "0001_initial"),
]
operations = [
migrations.RunPython(
migrate_autocheckin,
migrations.RunPython.noop,
),
migrations.RemoveField(
model_name="checkinlist",
name="auto_checkin_sales_channels",
),
]

View File

@@ -256,9 +256,6 @@ class SubeventColumnMixin:
] ]
def clean(self, value, previous_values): def clean(self, value, previous_values):
if not value:
return None
if value in self._subevent_cache: if value in self._subevent_cache:
return self._subevent_cache[value] return self._subevent_cache[value]

View File

@@ -56,7 +56,7 @@ from pretix.base.signals import order_import_columns
class EmailColumn(ImportColumn): class EmailColumn(ImportColumn):
identifier = 'email' identifier = 'email'
verbose_name = gettext_lazy('Email address') verbose_name = gettext_lazy('E-mail address')
def clean(self, value, previous_values): def clean(self, value, previous_values):
if value: if value:
@@ -322,7 +322,7 @@ class AttendeeNamePart(ImportColumn):
class AttendeeEmail(ImportColumn): class AttendeeEmail(ImportColumn):
identifier = 'attendee_email' identifier = 'attendee_email'
verbose_name = gettext_lazy('Attendee email address') verbose_name = gettext_lazy('Attendee e-mail address')
def clean(self, value, previous_values): def clean(self, value, previous_values):
if value: if value:

View File

@@ -241,7 +241,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
email = models.EmailField(unique=True, db_index=True, null=True, blank=True, email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('Email'), max_length=190) verbose_name=_('E-mail'), max_length=190)
fullname = models.CharField(max_length=255, blank=True, null=True, fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name')) verbose_name=_('Full name'))
is_active = models.BooleanField(default=True, is_active = models.BooleanField(default=True,

View File

@@ -99,6 +99,14 @@ class CheckinList(LoggedModel):
verbose_name=_('Automatically check out everyone at'), verbose_name=_('Automatically check out everyone at'),
null=True, blank=True null=True, blank=True
) )
auto_checkin_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_('Sales channels to automatically check in'),
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
'to use the new plugin.'),
blank=True,
)
rules = models.JSONField(default=dict, blank=True) rules = models.JSONField(default=dict, blank=True)
objects = ScopedManager(organizer='event__organizer') objects = ScopedManager(organizer='event__organizer')
@@ -133,7 +141,7 @@ class CheckinList(LoggedModel):
return self.positions_query(ignore_status=False) return self.positions_query(ignore_status=False)
@scopes_disabled() @scopes_disabled()
def _filter_positions_inside(self, qs, at_time=None): def positions_inside_query(self, ignore_status=False, at_time=None):
if at_time is None: if at_time is None:
c_q = [] c_q = []
else: else:
@@ -141,7 +149,7 @@ class CheckinList(LoggedModel):
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]: if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
# Use a simple approach that works on all databases # Use a simple approach that works on all databases
qs = qs.annotate( qs = self.positions_query(ignore_status=ignore_status).annotate(
last_entry=Subquery( last_entry=Subquery(
Checkin.objects.filter( Checkin.objects.filter(
*c_q, *c_q,
@@ -194,7 +202,7 @@ class CheckinList(LoggedModel):
.values("position_id", "type", "datetime", "cnt_exists_after") .values("position_id", "type", "datetime", "cnt_exists_after")
.query.sql_with_params() .query.sql_with_params()
) )
return qs.filter( return self.positions_query(ignore_status=ignore_status).filter(
pk__in=RawSQL( pk__in=RawSQL(
f""" f"""
SELECT "position_id" SELECT "position_id"
@@ -206,10 +214,6 @@ class CheckinList(LoggedModel):
) )
) )
@scopes_disabled()
def positions_inside_query(self, ignore_status=False, at_time=None):
return self._filter_positions_inside(self.positions_query(ignore_status=ignore_status), at_time=at_time)
@property @property
def positions_inside(self): def positions_inside(self):
return self.positions_inside_query(None) return self.positions_inside_query(None)

View File

@@ -91,7 +91,7 @@ class Customer(LoggedModel):
), ),
], ],
) )
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('Email'), max_length=190) email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number')) phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
password = models.CharField(verbose_name=_('Password'), max_length=128) password = models.CharField(verbose_name=_('Password'), max_length=128)
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True) name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
@@ -392,7 +392,7 @@ class CustomerSSOClient(LoggedModel):
SCOPE_CHOICES = ( SCOPE_CHOICES = (
('openid', _('OpenID Connect access (required)')), ('openid', _('OpenID Connect access (required)')),
('profile', _('Profile data (name, addresses)')), ('profile', _('Profile data (name, addresses)')),
('email', _('Email address')), ('email', _('E-mail address')),
('phone', _('Phone number')), ('phone', _('Phone number')),
) )

View File

@@ -28,6 +28,7 @@ from django.utils.crypto import get_random_string
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
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel
@@ -160,6 +161,7 @@ class Device(LoggedModel):
) )
security_profile = models.CharField( security_profile = models.CharField(
max_length=190, max_length=190,
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
default='full', default='full',
null=True, null=True,
blank=False blank=False

View File

@@ -1024,9 +1024,10 @@ class Event(EventMixin, LoggedModel):
checkin_list_map = {} checkin_list_map = {}
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related( for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
'limit_products' 'limit_products', 'auto_checkin_sales_channels'
): ):
items = list(cl.limit_products.all()) items = list(cl.limit_products.all())
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
checkin_list_map[cl.pk] = cl checkin_list_map[cl.pk] = cl
cl.pk = None cl.pk = None
cl._prefetched_objects_cache = {} cl._prefetched_objects_cache = {}
@@ -1038,6 +1039,8 @@ class Event(EventMixin, LoggedModel):
cl.log_action('pretix.object.cloned') cl.log_action('pretix.object.cloned')
for i in items: for i in items:
cl.limit_products.add(item_map[i.pk]) cl.limit_products.add(item_map[i.pk])
if auto_checkin_sales_channels:
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
if other.seating_plan: if other.seating_plan:
if other.seating_plan.organizer_id == self.organizer_id: if other.seating_plan.organizer_id == self.organizer_id:

View File

@@ -1118,12 +1118,13 @@ class ItemVariation(models.Model):
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown. :param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
:type original_price: decimal.Decimal :type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after :param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
approval by an administrator approval by an administrator
:type require_approval: bool :type require_approval: bool
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored. :param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
:type all_sales_channels: bool :type all_sales_channels: bool
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on. :param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
:type limit_sales_channels: list :type limit_sales_channels: list
""" """
item = models.ForeignKey( item = models.ForeignKey(
Item, Item,

View File

@@ -43,7 +43,7 @@ class NotificationSetting(models.Model):
:type enabled: bool :type enabled: bool
""" """
CHANNELS = ( CHANNELS = (
('mail', _('Email')), ('mail', _('E-mail')),
) )
user = models.ForeignKey('User', on_delete=models.CASCADE, user = models.ForeignKey('User', on_delete=models.CASCADE,
related_name='notification_settings') related_name='notification_settings')

View File

@@ -242,7 +242,7 @@ class Order(LockModel, LoggedModel):
) )
email = models.EmailField( email = models.EmailField(
null=True, blank=True, null=True, blank=True,
verbose_name=_('Email') verbose_name=_('E-mail')
) )
phone = PhoneNumberField( phone = PhoneNumberField(
null=True, blank=True, null=True, blank=True,
@@ -317,7 +317,7 @@ class Order(LockModel, LoggedModel):
) )
email_known_to_work = models.BooleanField( email_known_to_work = models.BooleanField(
default=False, default=False,
verbose_name=_('Email address verified') verbose_name=_('E-mail address verified')
) )
invoice_dirty = models.BooleanField( invoice_dirty = models.BooleanField(
# Invoice needs to be re-issued when the order is paid again # Invoice needs to be re-issued when the order is paid again
@@ -3391,74 +3391,6 @@ class BlockedTicketSecret(models.Model):
unique_together = (('event', 'secret'),) unique_together = (('event', 'secret'),)
class PrintLog(models.Model):
"""
A print log object is created when a ticket or badge is printed with our apps.
"""
TYPE_BADGE = 'badge'
TYPE_TICKET = 'ticket'
TYPE_CERTIFICATE = 'certificate'
TYPE_OTHER = 'other'
PRINT_TYPES = (
(TYPE_BADGE, _('Badge')),
(TYPE_TICKET, _('Ticket')),
(TYPE_CERTIFICATE, _('Certificate')),
(TYPE_OTHER, _('Other')),
)
position = models.ForeignKey(
'pretixbase.OrderPosition',
related_name='print_logs',
on_delete=models.CASCADE,
)
successful = models.BooleanField(
default=True,
)
# Datetime of checkin, might be different from created if past scans are uploaded
datetime = models.DateTimeField(default=now)
# Datetime of creation on server
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
# Who printed?
device = models.ForeignKey('Device', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
user = models.ForeignKey('User', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
# Source = Tag field with undefined values, e.g. name of app ("pretixscan")
source = models.CharField(max_length=255)
# Type = Type of object printed ("badge", "ticket")
type = models.CharField(max_length=255, choices=PRINT_TYPES)
info = models.JSONField(default=dict)
objects = ScopedManager(organizer='position__order__event__organizer')
class Meta:
ordering = (('-datetime'),)
def __repr__(self):
return "<PrintLog: pos {} at {} from {}>".format(
self.position, self.datetime, self.source
)
def save(self, **kwargs):
super().save(**kwargs)
if self.position:
self.position.order.touch()
def delete(self, **kwargs):
super().delete(**kwargs)
self.position.order.touch()
@property
def is_late_upload(self):
return self.created and abs(self.created - self.datetime) > timedelta(minutes=2)
@receiver(post_delete, sender=CachedTicket) @receiver(post_delete, sender=CachedTicket)
def cachedticket_delete(sender, instance, **kwargs): def cachedticket_delete(sender, instance, **kwargs):
if instance.file: if instance.file:

View File

@@ -53,30 +53,6 @@ class SeatingPlanLayoutValidator:
e = str(e).replace('%', '%%') e = str(e).replace('%', '%%')
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e)) raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
try:
seat_guids = set()
for z in val["zones"]:
for r in z["rows"]:
for s in r["seats"]:
if not s.get("seat_guid"):
raise ValidationError(
_("Seat with zone {zone}, row {row}, and number {number} has no seat ID.").format(
zone=z["name"],
row=r["row_number"],
number=s["seat_number"],
)
)
elif s["seat_guid"] in seat_guids:
raise ValidationError(
_("Multiple seats have the same ID: {id}").format(
id=s["seat_guid"],
)
)
seat_guids.add(s["seat_guid"])
except ValidationError as e:
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(", ".join(e.message for e in e.error_list)))
class SeatingPlan(LoggedModel): class SeatingPlan(LoggedModel):
""" """

View File

@@ -73,7 +73,7 @@ class WaitingListEntry(LoggedModel):
blank=True, default=dict blank=True, default=dict
) )
email = models.EmailField( email = models.EmailField(
verbose_name=_("Email address") verbose_name=_("E-mail address")
) )
phone = PhoneNumberField( phone = PhoneNumberField(
null=True, blank=True, null=True, blank=True,

View File

@@ -343,13 +343,11 @@ class CartManager:
err = error_messages['some_subevent_not_started'] err = error_messages['some_subevent_not_started']
cp.addons.all().delete() cp.addons.all().delete()
cp.delete() cp.delete()
continue
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end: if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
err = error_messages['some_subevent_ended'] err = error_messages['some_subevent_ended']
cp.addons.all().delete() cp.addons.all().delete()
cp.delete() cp.delete()
continue
if cp.subevent: if cp.subevent:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper) tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
@@ -362,7 +360,6 @@ class CartManager:
err = error_messages['some_subevent_ended'] err = error_messages['some_subevent_ended']
cp.addons.all().delete() cp.addons.all().delete()
cp.delete() cp.delete()
continue
return err return err
def _update_subevents_cache(self, se_ids: List[int]): def _update_subevents_cache(self, se_ids: List[int]):

View File

@@ -57,7 +57,7 @@ from pretix.base.models import (
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order, Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
OrderPosition, QuestionOption, OrderPosition, QuestionOption,
) )
from pretix.base.signals import checkin_created, periodic_task from pretix.base.signals import checkin_created, order_placed, periodic_task
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
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
@@ -1154,6 +1154,23 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
) )
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return
for op in order.positions.all():
for cl in cls:
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
checkin_created.send(event, checkin=ci)
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all") @receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
@scopes_disabled() @scopes_disabled()
def process_exit_all(sender, **kwargs): def process_exit_all(sender, **kwargs):

View File

@@ -315,7 +315,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
body_html = renderer.render(content_plain, signature, raw_subject, order, position) body_html = renderer.render(content_plain, signature, raw_subject, order, position)
else: else:
# Backwards compatibility # Backwards compatibility
warnings.warn('Email renderer called without position argument because position argument is not ' warnings.warn('E-mail renderer called without position argument because position argument is not '
'supported.', 'supported.',
DeprecationWarning) DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order) body_html = renderer.render(content_plain, signature, raw_subject, order)

View File

@@ -209,24 +209,13 @@ def get_best_name(position_or_address, parts=False):
def base_placeholders(sender, **kwargs): def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
def _event_sample(event):
if event.has_subevents:
se = event.subevents.first()
if se:
return se.name
return event.name
ph = [ ph = [
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name 'event', ['event'], lambda event: event.name, lambda event: event.name
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name, 'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
_event_sample, lambda event_or_subevent: event_or_subevent.name
),
SimpleFunctionalTextPlaceholder(
'event_series_name', ['event', 'event_or_subevent'], lambda event, event_or_subevent: event.name,
lambda event: event.name
), ),
SimpleFunctionalTextPlaceholder( SimpleFunctionalTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug 'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug

View File

@@ -550,7 +550,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'type': bool, 'type': bool,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Require a business address"), label=_("Require a business addresses"),
help_text=_('This will require users to enter a company name.'), help_text=_('This will require users to enter a company name.'),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}), widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}),
) )

View File

@@ -287,9 +287,9 @@ class PhoneNumberShredder(BaseDataShredder):
class EmailAddressShredder(BaseDataShredder): class EmailAddressShredder(BaseDataShredder):
verbose_name = _('Emails') verbose_name = _('E-mails')
identifier = 'order_emails' identifier = 'order_emails'
description = _('This will remove all email addresses from orders and attendees, as well as logged email ' description = _('This will remove all e-mail addresses from orders and attendees, as well as logged email '
'contents. This will also remove the association to customer accounts.') 'contents. This will also remove the association to customer accounts.')
def generate_files(self) -> List[Tuple[str, str, str]]: def generate_files(self) -> List[Tuple[str, str, str]]:

View File

@@ -367,7 +367,7 @@ validate_cart_addons = EventPluginSignal()
Arguments: ``addons``, ``base_position``, ``iao`` Arguments: ``addons``, ``base_position``, ``iao``
This signal is sent when a user tries to select a combination of addons. In contrast to This signal is sent when a user tries to select a combination of addons. In contrast to
``validate_cart``, this is executed before the cart is actually modified. You are passed ``validate_cart``, this is executed before the cart is actually modified. You are passed
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
``base_position``. ``base_position``.

View File

@@ -54,7 +54,7 @@ from tlds import tld_set
register = template.Library() register = template.Library()
ALLOWED_TAGS_SNIPPET = { ALLOWED_TAGS_SNIPPET = [
'a', 'a',
'abbr', 'abbr',
'acronym', 'acronym',
@@ -68,8 +68,8 @@ ALLOWED_TAGS_SNIPPET = {
'strike', 'strike',
's', 's',
# Update doc/user/markdown.rst if you change this! # Update doc/user/markdown.rst if you change this!
} ]
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET | { ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
'blockquote', 'blockquote',
'li', 'li',
'ol', 'ol',
@@ -91,7 +91,7 @@ ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET | {
'h6', 'h6',
'pre', 'pre',
# Update doc/user/markdown.rst if you change this! # Update doc/user/markdown.rst if you change this!
} ]
ALLOWED_ATTRIBUTES = { ALLOWED_ATTRIBUTES = {
'a': ['href', 'title', 'class'], 'a': ['href', 'title', 'class'],
@@ -106,7 +106,7 @@ ALLOWED_ATTRIBUTES = {
# Update doc/user/markdown.rst if you change this! # Update doc/user/markdown.rst if you change this!
} }
ALLOWED_PROTOCOLS = {'http', 'https', 'mailto', 'tel'} ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True))) URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
@@ -211,9 +211,9 @@ class CleanPostprocessor(Postprocessor):
def run(self, text): def run(self, text):
return bleach.clean( return bleach.clean(
text, text,
tags=set(self.tags), tags=self.tags,
attributes=self.attributes, attributes=self.attributes,
protocols=set(self.protocols), protocols=self.protocols,
strip=self.strip strip=self.strip
) )
@@ -308,7 +308,7 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
EmailNl2BrExtension(), EmailNl2BrExtension(),
LinkifyAndCleanExtension( LinkifyAndCleanExtension(
linker, linker,
tags=set(allowed_tags), tags=allowed_tags,
attributes=allowed_attributes, attributes=allowed_attributes,
protocols=ALLOWED_PROTOCOLS, protocols=ALLOWED_PROTOCOLS,
strip=False, strip=False,

View File

@@ -33,7 +33,9 @@ from django_scopes.forms import (
from pretix.base.forms.widgets import SplitDateTimePickerWidget from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Gate from pretix.base.models import Gate
from pretix.base.models.checkin import Checkin, CheckinList from pretix.base.models.checkin import Checkin, CheckinList
from pretix.control.forms import ItemMultipleChoiceField from pretix.control.forms import (
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
)
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
@@ -65,6 +67,10 @@ class CheckinListForm(forms.ModelForm):
kwargs.pop('locales', None) kwargs.pop('locales', None)
super().__init__(**kwargs) super().__init__(**kwargs)
self.fields['limit_products'].queryset = self.event.items.all() self.fields['limit_products'].queryset = self.event.items.all()
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
)
if not self.event.organizer.gates.exists(): if not self.event.organizer.gates.exists():
del self.fields['gates'] del self.fields['gates']
@@ -96,6 +102,7 @@ class CheckinListForm(forms.ModelForm):
'limit_products', 'limit_products',
'subevent', 'subevent',
'include_pending', 'include_pending',
'auto_checkin_sales_channels',
'allow_multiple_entries', 'allow_multiple_entries',
'allow_entry_after_exit', 'allow_entry_after_exit',
'rules', 'rules',
@@ -118,6 +125,7 @@ class CheckinListForm(forms.ModelForm):
'limit_products': ItemMultipleChoiceField, 'limit_products': ItemMultipleChoiceField,
'gates': SafeModelMultipleChoiceField, 'gates': SafeModelMultipleChoiceField,
'subevent': SafeModelChoiceField, 'subevent': SafeModelChoiceField,
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
'exit_all_at': NextTimeField, 'exit_all_at': NextTimeField,
} }

View File

@@ -136,11 +136,6 @@ class EventWizardBasicsForm(I18nModelForm):
choices=settings.LANGUAGES, choices=settings.LANGUAGES,
label=_("Default language"), label=_("Default language"),
) )
no_taxes = forms.BooleanField(
label=_("I don't want to specify taxes now"),
help_text=_("You can always configure tax rates later."),
required=False,
)
tax_rate = forms.DecimalField( tax_rate = forms.DecimalField(
label=_("Sales tax rate"), label=_("Sales tax rate"),
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate " help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
@@ -228,11 +223,6 @@ class EventWizardBasicsForm(I18nModelForm):
raise ValidationError({ raise ValidationError({
'timezone': _('Your default locale must be specified.') 'timezone': _('Your default locale must be specified.')
}) })
if not data.get("no_taxes") and not data.get("tax_rate"):
raise ValidationError({
'tax_rate': _('You have not specified a tax rate. If you do not want us to compute sales taxes, please '
'check "{field}" above.').format(field=self.fields["no_taxes"].label)
})
# change timezone # change timezone
zone = ZoneInfo(data.get('timezone')) zone = ZoneInfo(data.get('timezone'))

View File

@@ -549,7 +549,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
) )
email = forms.CharField( email = forms.CharField(
required=False, required=False,
label=_('Email address') label=_('E-mail address')
) )
comment = forms.CharField( comment = forms.CharField(
required=False, required=False,
@@ -563,7 +563,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
email_known_to_work = forms.NullBooleanField( email_known_to_work = forms.NullBooleanField(
required=False, required=False,
widget=FilterNullBooleanSelect, widget=FilterNullBooleanSelect,
label=_('Email address verified'), label=_('E-mail address verified'),
) )
total = forms.DecimalField( total = forms.DecimalField(
localize=True, localize=True,
@@ -648,7 +648,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
) )
self.fields['attendee_email'] = forms.CharField( self.fields['attendee_email'] = forms.CharField(
required=False, required=False,
label=_('Attendee email address') label=_('Attendee e-mail address')
) )
self.fields['attendee_address_company'] = forms.CharField( self.fields['attendee_address_company'] = forms.CharField(
required=False, required=False,
@@ -1967,7 +1967,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
if s == '1': if s == '1':
qs = qs.filter(last_entry__isnull=False) qs = qs.filter(last_entry__isnull=False)
elif s == '2': elif s == '2':
qs = self.list._filter_positions_inside(qs) qs = qs.filter(pk__in=self.list.positions_inside.values_list('pk'))
elif s == '3': elif s == '3':
qs = qs.filter(last_entry__isnull=False).filter( qs = qs.filter(last_entry__isnull=False).filter(
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry')) Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))

View File

@@ -128,7 +128,7 @@ class UpdateSettingsForm(SettingsForm):
) )
update_check_email = forms.EmailField( update_check_email = forms.EmailField(
required=False, required=False,
label=_("Email notifications"), label=_("E-mail notifications"),
help_text=_("We will notify you at this address if we detect that a new update is available. This " help_text=_("We will notify you at this address if we detect that a new update is available. This "
"address will not be transmitted to pretix.eu, the emails will be sent by this server " "address will not be transmitted to pretix.eu, the emails will be sent by this server "
"locally.") "locally.")

View File

@@ -609,49 +609,6 @@ class OrderFeeChangeForm(forms.Form):
change_decimal_field(self.fields['value'], instance.order.event.currency) change_decimal_field(self.fields['value'], instance.order.event.currency)
class OrderFeeAddForm(forms.Form):
fee_type = forms.ChoiceField(choices=OrderFee.FEE_TYPES)
value = forms.DecimalField(
max_digits=13, decimal_places=2,
localize=True,
label=_('Price'),
help_text=_("including all taxes"),
)
tax_rule = forms.ModelChoiceField(
TaxRule.objects.none(),
required=False,
)
description = forms.CharField(required=False)
def __init__(self, *args, **kwargs):
order = kwargs.pop('order')
super().__init__(*args, **kwargs)
self.fields['tax_rule'].queryset = order.event.tax_rules.all()
change_decimal_field(self.fields['value'], order.event.currency)
class OrderFeeAddFormset(forms.BaseFormSet):
def __init__(self, *args, **kwargs):
self.order = kwargs.pop('order', None)
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['order'] = self.order
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
order=self.order,
)
self.add_fields(form, None)
return form
class OrderContactForm(forms.ModelForm): class OrderContactForm(forms.ModelForm):
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'), regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
help_text=_('Regenerates the order and ticket secrets. You will ' help_text=_('Regenerates the order and ticket secrets. You will '

View File

@@ -54,7 +54,6 @@ from i18nfield.strings import LazyI18nString
from phonenumber_field.formfields import PhoneNumberField from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones from pytz import common_timezones
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.models import WebHook from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events from pretix.api.webhooks import get_all_webhook_events
from pretix.base.customersso.oidc import oidc_validate_and_complete_config from pretix.base.customersso.oidc import oidc_validate_and_complete_config
@@ -312,11 +311,6 @@ class DeviceForm(forms.ModelForm):
'-has_subevents', '-date_from' '-has_subevents', '-date_from'
) )
self.fields['gate'].queryset = organizer.gates.all() self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self): def clean(self):
d = super().clean() d = super().clean()
@@ -350,11 +344,6 @@ class DeviceBulkEditForm(forms.ModelForm):
'-has_subevents', '-date_from' '-has_subevents', '-date_from'
) )
self.fields['gate'].queryset = organizer.gates.all() self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self): def clean(self):
d = super().clean() d = super().clean()

View File

@@ -40,7 +40,7 @@ class StaffSessionForm(forms.ModelForm):
class UserEditForm(forms.ModelForm): class UserEditForm(forms.ModelForm):
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 e-mail address. "
"Please choose a different one."), "Please choose a different one."),
'pw_mismatch': _("Please enter the same password twice"), 'pw_mismatch': _("Please enter the same password twice"),
} }

View File

@@ -51,7 +51,6 @@ from pretix.base.models import (
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition, Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
TaxRule, TaxRule,
) )
from pretix.base.models.orders import PrintLog
from pretix.base.signals import logentry_display, orderposition_blocked_display from pretix.base.signals import logentry_display, orderposition_blocked_display
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
@@ -613,7 +612,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type == 'pretix.event.order.consent': if logentry.action_type == 'pretix.event.order.consent':
return _('The user confirmed the following message: "{}"').format( return _('The user confirmed the following message: "{}"').format(
bleach.clean(logentry.parsed_data.get('msg'), tags=set(), strip=True) bleach.clean(logentry.parsed_data.get('msg'), tags=[], strip=True)
) )
if logentry.action_type == 'pretix.event.order.canceled': if logentry.action_type == 'pretix.event.order.canceled':
@@ -640,16 +639,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if sender and logentry.action_type.startswith('pretix.event.checkin'): if sender and logentry.action_type.startswith('pretix.event.checkin'):
return _display_checkin(sender, logentry) return _display_checkin(sender, logentry)
if logentry.action_type == 'pretix.event.order.print':
return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
posid=data.get('positionid'),
datetime=date_format(
dateutil.parser.parse(data["datetime"]).astimezone(sender.timezone),
"SHORT_DATETIME_FORMAT"
),
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
)
if logentry.action_type == 'pretix.control.views.checkin': if logentry.action_type == 'pretix.control.views.checkin':
# deprecated # deprecated
dt = dateutil.parser.parse(data.get('datetime')) dt = dateutil.parser.parse(data.get('datetime'))

View File

@@ -78,7 +78,7 @@ def get_event_navigation(request: HttpRequest):
'active': url.url_name == 'event.settings.tickets', 'active': url.url_name == 'event.settings.tickets',
}, },
{ {
'label': _('Email'), 'label': _('E-mail'),
'url': reverse('control:event.settings.mail', kwargs={ 'url': reverse('control:event.settings.mail', kwargs={
'event': request.event.slug, 'event': request.event.slug,
'organizer': request.event.organizer.slug, 'organizer': request.event.organizer.slug,
@@ -132,6 +132,16 @@ def get_event_navigation(request: HttpRequest):
'icon': 'wrench', 'icon': 'wrench',
'children': event_settings 'children': event_settings
}) })
if request.event.has_subevents:
nav.append({
'label': pgettext_lazy('subevent', 'Dates'),
'url': reverse('control:event.subevents', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': ('event.subevent' in url.url_name),
'icon': 'calendar',
})
if 'can_change_items' in request.eventpermset: if 'can_change_items' in request.eventpermset:
nav.append({ nav.append({
@@ -187,18 +197,6 @@ def get_event_navigation(request: HttpRequest):
] ]
}) })
if 'can_change_event_settings' in request.eventpermset:
if request.event.has_subevents:
nav.append({
'label': pgettext_lazy('subevent', 'Dates'),
'url': reverse('control:event.subevents', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': ('event.subevent' in url.url_name),
'icon': 'calendar',
})
if 'can_view_orders' in request.eventpermset: if 'can_view_orders' in request.eventpermset:
children = [ children = [
{ {
@@ -498,7 +496,7 @@ def get_organizer_navigation(request):
'active': url.url_name.startswith('organizer.propert'), 'active': url.url_name.startswith('organizer.propert'),
}, },
{ {
'label': _('Email'), 'label': _('E-mail'),
'url': reverse('control:organizer.settings.mail', kwargs={ 'url': reverse('control:organizer.settings.mail', kwargs={
'organizer': request.organizer.slug, 'organizer': request.organizer.slug,
}), }),

View File

@@ -127,7 +127,6 @@
<strong> <strong>
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=c.position.order.code %}">{{ c.position.order.code }}</a>-{{ c.position.positionid }} <a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=c.position.order.code %}">{{ c.position.order.code }}</a>-{{ c.position.positionid }}
</strong> </strong>
{% include "pretixcontrol/checkin/fragment_checkin_source_type.html" with source_type=c.raw_source_type %}
{% if c.position.attendee_name %} {% if c.position.attendee_name %}
<br> <br>
<small> <small>
@@ -144,7 +143,7 @@
</small> </small>
{% endif %} {% endif %}
{% else %} {% else %}
{% include "pretixcontrol/checkin/fragment_checkin_source_type.html" with source_type=c.raw_source_type %} <span class="fa fa-qrcode fa-fw"></span>
<span title="{{ c.raw_barcode }}"> <span title="{{ c.raw_barcode }}">
{{ c.raw_barcode|slice:":16" }}{% if c.raw_barcode|length > 16 %}…{% endif %} {{ c.raw_barcode|slice:":16" }}{% if c.raw_barcode|length > 16 %}…{% endif %}
<button type="button" class="btn btn-xs btn-link btn-clipboard" data-clipboard-text="{{ c.raw_barcode }}"> <button type="button" class="btn btn-xs btn-link btn-clipboard" data-clipboard-text="{{ c.raw_barcode }}">

View File

@@ -1,15 +0,0 @@
{% load i18n %}
{% load static %}
{% load getitem %}
{% if source_type %}
{% with media_types|getitem:source_type as media_type %}
{% if "." in media_type.icon %}
<img src="{% static media_type.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ media_type.verbose_name }}">
{% else %}
<span class="fa fa-fw fa-{{ media_type.icon }} text-muted"
data-toggle="tooltip" title="{{ media_type.verbose_name }}"></span>
{% endif %}
{% endwith %}
{% endif %}

View File

@@ -185,7 +185,6 @@
<span class="fa fa-magic text-muted" <span class="fa fa-magic text-muted"
data-toggle="tooltip" title="{% trans "Checked in automatically" %}"></span> data-toggle="tooltip" title="{% trans "Checked in automatically" %}"></span>
{% endif %} {% endif %}
{% include "pretixcontrol/checkin/fragment_checkin_source_type.html" with source_type=e.last_entry_source_type %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>

View File

@@ -67,6 +67,7 @@
{% bootstrap_field form.allow_entry_after_exit layout="control" %} {% bootstrap_field form.allow_entry_after_exit layout="control" %}
{% bootstrap_field form.addon_match layout="control" %} {% bootstrap_field form.addon_match layout="control" %}
{% bootstrap_field form.exit_all_at layout="control" %} {% bootstrap_field form.exit_all_at layout="control" %}
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
{% if form.gates %} {% if form.gates %}
{% bootstrap_field form.gates layout="control" %} {% bootstrap_field form.gates layout="control" %}
{% endif %} {% endif %}

View File

@@ -101,6 +101,7 @@
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
{% endif %} {% endif %}
<th class="iconcol">{% trans "Automated check-in" %}</th>
<th>{% trans "Products" %}</th> <th>{% trans "Products" %}</th>
<th class="action-col-2"></th> <th class="action-col-2"></th>
</tr> </tr>
@@ -136,6 +137,17 @@
</td> </td>
{% endif %} {% endif %}
{% endif %} {% endif %}
<td>
{% for channel in cl.auto_checkin_sales_channels.all %}
{% if "." in channel.icon %}
<img src="{% static channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ channel.label }}">
{% else %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{{ channel.label }}"></span>
{% endif %}
{% endfor %}
</td>
<td> <td>
{% if cl.all_products %} {% if cl.all_products %}
<em>{% trans "All" %}</em> <em>{% trans "All" %}</em>

View File

@@ -5,7 +5,7 @@
{% load static %} {% load static %}
{% block title %}{% trans "Organizer" %}{% endblock %} {% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Email sending" %}</h1> <h1>{% trans "E-mail sending" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<div class="panel-group" id="email"> <div class="panel-group" id="email">
@@ -27,7 +27,7 @@
<div class="panel-body form-horizontal"> <div class="panel-body form-horizontal">
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
Emails will be sent through the system's default server. They will show the following E-mails will be sent through the system's default server. They will show the following
sender information: sender information:
{% endblocktrans %} {% endblocktrans %}
</p> </p>
@@ -62,7 +62,7 @@
<div class="panel-body form-horizontal"> <div class="panel-body form-horizontal">
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
Emails will be sent through the system's default server but with your own sender E-mails will be sent through the system's default server but with your own sender
address. address.
This will make your emails look more personalized and coming directly from you, but it This will make your emails look more personalized and coming directly from you, but it
also might require some extra steps to ensure good deliverability. also might require some extra steps to ensure good deliverability.

View File

@@ -5,7 +5,7 @@
{% load static %} {% load static %}
{% block title %}{% trans "Organizer" %}{% endblock %} {% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Email sending" %}</h1> <h1>{% trans "E-mail sending" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
{% for k, v in request.POST.items %} {% for k, v in request.POST.items %}

View File

@@ -5,7 +5,7 @@
{% load static %} {% load static %}
{% block title %}{% trans "Organizer" %}{% endblock %} {% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Email sending" %}</h1> <h1>{% trans "E-mail sending" %}</h1>
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
{% for k, v in request.POST.items %} {% for k, v in request.POST.items %}

View File

@@ -4,7 +4,7 @@
{% load hierarkey_form %} {% load hierarkey_form %}
{% load static %} {% load static %}
{% block inside %} {% block inside %}
<h1>{% trans "Email settings" %}</h1> <h1>{% trans "E-mail settings" %}</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data" <form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}"> mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
{% csrf_token %} {% csrf_token %}
@@ -63,7 +63,7 @@
{% bootstrap_field form.mail_attach_ical_description layout="control" %} {% bootstrap_field form.mail_attach_ical_description layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Email design" %}</legend> <legend>{% trans "E-mail design" %}</legend>
<div class="row"> <div class="row">
{% for r in renderers.values %} {% for r in renderers.values %}
<div class="col-md-3"> <div class="col-md-3">
@@ -84,7 +84,7 @@
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Email content" %}</legend> <legend>{% trans "E-mail content" %}</legend>
<h4>{% trans "Text" %}</h4> <h4>{% trans "Text" %}</h4>
<div class="panel-group" id="questions_group"> <div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %} {% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}

View File

@@ -67,7 +67,7 @@
<h4>{% trans "Customer data (once per order)" %}</h4> <h4>{% trans "Customer data (once per order)" %}</h4>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3"> <label class="control-label col-md-3">
{% trans "Email" %} {% trans "E-mail" %}
</label> </label>
<div class="col-md-9"> <div class="col-md-9">
<div class="checkbox"> <div class="checkbox">

View File

@@ -41,10 +41,7 @@
{% endif %} {% endif %}
{% include "pretixcontrol/event/fragment_geodata.html" %} {% include "pretixcontrol/event/fragment_geodata.html" %}
{% bootstrap_field form.currency layout="control" %} {% bootstrap_field form.currency layout="control" %}
{% bootstrap_field form.no_taxes layout="control" %} {% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
<div data-display-dependency="#{{ form.no_taxes.id_for_label }}" data-inverse>
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Display settings" %}</legend> <legend>{% trans "Display settings" %}</legend>

View File

@@ -12,22 +12,10 @@
{% endif %} {% endif %}
{% if object.id and not object.quotas.exists %} {% if object.id and not object.quotas.exists %}
<div class="alert alert-warning"> <div class="alert alert-warning">
<div class="row"> {% blocktrans trimmed %}
<div class="col-lg-8"> Please note that your product will <strong>not</strong> be available for sale until you have added your
{% blocktrans trimmed %} item to an existing or newly created quota.
Please note that your product will <strong>not</strong> be available for sale until you have added your {% endblocktrans %}
item to an existing or newly created quota.
{% endblocktrans %}
</div>
<div class="col-lg-4 text-right">
<a class="btn btn-default btn-sm" href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-wrench"></i> {% trans "Manage quotas" %}
</a>
<a class="btn btn-default btn-sm" href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?{% if object.has_variations %}{% for var in object.variations.all %}product={{ object.pk }}-{{ var.pk }}&{% endfor %}{% else %}product={{ object.pk }}{% endif %}">
<i class="fa fa-plus"></i> {% trans "Create a new quota" %}
</a>
</div>
</div>
</div> </div>
{% elif object.pk and not object.is_available_by_time %} {% elif object.pk and not object.is_available_by_time %}
<div class="alert alert-warning"> <div class="alert alert-warning">

View File

@@ -67,22 +67,10 @@
<div class="panel-body form-horizontal"> <div class="panel-body form-horizontal">
{% if form.instance.pk and not form.instance.quotas.exists %} {% if form.instance.pk and not form.instance.quotas.exists %}
<div class="alert alert-warning"> <div class="alert alert-warning">
<div class="row"> {% blocktrans trimmed %}
<div class="col-lg-8"> Please note that your variation will <strong>not</strong> be available for sale
{% blocktrans trimmed %} until you have added it to an existing or newly created quota.
Please note that your variation will <strong>not</strong> be available for sale {% endblocktrans %}
until you have added it to an existing or newly created quota.
{% endblocktrans %}
</div>
<div class="col-lg-4 text-right">
<a class="btn btn-default btn-xs" href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-wrench"></i> {% trans "Manage quotas" %}
</a>
<a class="btn btn-default btn-xs" href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?product={{ form.instance.item.pk }}-{{ form.instance.pk }}">
<i class="fa fa-plus"></i> {% trans "Create a new quota" %}
</a>
</div>
</div>
</div> </div>
{% endif %} {% endif %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}

View File

@@ -16,18 +16,8 @@
{% bootstrap_field form.internal_name layout="control" %} {% bootstrap_field form.internal_name layout="control" %}
</div> </div>
{% bootstrap_field form.description layout="control" %} {% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-md-9" %} {% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-lg-9" %}
<div class="row" data-display-dependency="#id_category_type_2"> {% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-lg-9" %}
<div class="col-md-offset-3 col-md-9">
<div class="alert alert-info">
{% blocktrans trimmed %}
Please note that cross-selling categories are intended as a marketing feature and are not
suitable for strictly ensuring that products are only available in certain combinations.
{% endblocktrans %}
</div>
</div>
</div>
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-md-9" %}
{% bootstrap_field form.cross_selling_match_products layout="control" %} {% bootstrap_field form.cross_selling_match_products layout="control" %}
</fieldset> </fieldset>
</div> </div>

View File

@@ -296,11 +296,11 @@
{% endfor %} {% endfor %}
<div class="formset" data-formset data-formset-prefix="{{ add_position_formset.prefix }}"> <div class="formset" data-formset data-formset-prefix="{{ add_formset.prefix }}">
{{ add_position_formset.management_form }} {{ add_formset.management_form }}
{% bootstrap_formset_errors add_position_formset %} {% bootstrap_formset_errors add_formset %}
<div data-formset-body> <div data-formset-body>
{% for add_form in add_position_formset %} {% for add_form in add_formset %}
<div class="panel panel-default items" data-formset-form data-subevent="0"> <div class="panel panel-default items" data-formset-form data-subevent="0">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"> <h3 class="panel-title">
@@ -351,25 +351,25 @@
</button> </button>
{% trans "Add product" %} {% trans "Add product" %}
<div class="sr-only"> <div class="sr-only">
{{ add_position_formset.empty_form.id }} {{ add_formset.empty_form.id }}
{% bootstrap_field add_position_formset.empty_form.DELETE form_group_class="" layout="inline" %} {% bootstrap_field add_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div> </div>
</h3> </h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="form-horizontal"> <div class="form-horizontal">
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %} {% bootstrap_field add_formset.empty_form.itemvar layout="control" %}
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %} {% bootstrap_field add_formset.empty_form.price addon_after=request.event.currency layout="control" %}
{% if add_position_formset.empty_form.addon_to %} {% if add_formset.empty_form.addon_to %}
{% bootstrap_field add_position_formset.empty_form.addon_to layout="control" %} {% bootstrap_field add_formset.empty_form.addon_to layout="control" %}
{% endif %} {% endif %}
{% if add_position_formset.empty_form.subevent %} {% if add_formset.empty_form.subevent %}
{% bootstrap_field add_position_formset.empty_form.subevent layout="control" %} {% bootstrap_field add_formset.empty_form.subevent layout="control" %}
{% endif %} {% endif %}
{% if add_position_formset.empty_form.used_membership %} {% if add_formset.empty_form.used_membership %}
{% bootstrap_field add_position_formset.empty_form.used_membership layout="control" %} {% bootstrap_field add_formset.empty_form.used_membership layout="control" %}
{% endif %} {% endif %}
{% bootstrap_field add_position_formset.empty_form.seat layout="control" %} {% bootstrap_field add_formset.empty_form.seat layout="control" %}
</div> </div>
</div> </div>
</div> </div>
@@ -431,77 +431,13 @@
{% bootstrap_field fee.form.operation_cancel layout='inline' %} {% bootstrap_field fee.form.operation_cancel layout='inline' %}
{% if fee.fee_type == "payment" %} {% if fee.fee_type == "payment" %}
<em class="text-danger"> <em class="text-danger">
{% trans "Manually modifying payment fees is discouraged since they might automatically be updated on subsequent order changes or when choosing a different payment method." %} {% trans "Manually modifying payment fees is discouraged since they might automatically be on subsequent order changes or when choosing a different payment method." %}
</em> </em>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div class="formset" data-formset data-formset-prefix="{{ add_fee_formset.prefix }}">
{{ add_fee_formset.management_form }}
{% bootstrap_formset_errors add_fee_formset %}
<div data-formset-body>
{% for add_form in add_fee_formset %}
<div class="panel panel-default items" data-formset-form data-subevent="0">
<div class="panel-heading">
<h3 class="panel-title">
<button type="button" class="btn btn-danger btn-xs pull-right flip"
data-formset-delete-button>
<i class="fa fa-trash"></i>
</button>
{% trans "Add fee" %}
<div class="sr-only">
{{ add_form.id }}
{% bootstrap_field add_form.DELETE form_group_class="" layout="inline" %}
</div>
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_form.fee_type layout='control' %}
{% bootstrap_field add_form.value addon_after=request.event.currency layout='control' %}
{% bootstrap_field add_form.tax_rule layout='control' %}
{% bootstrap_field add_form.description layout='control' %}
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default items" data-formset-form data-subevent="0">
<div class="panel-heading">
<h3 class="panel-title">
<button type="button" class="btn btn-danger btn-xs pull-right flip"
data-formset-delete-button>
<i class="fa fa-trash"></i>
</button>
{% trans "Add fee" %}
<div class="sr-only">
{{ add_fee_formset.empty_form.id }}
{% bootstrap_field add_fee_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_fee_formset.empty_form.fee_type layout='control' %}
{% bootstrap_field add_fee_formset.empty_form.value addon_after=request.event.currency layout='control' %}
{% bootstrap_field add_fee_formset.empty_form.tax_rule layout='control' %}
{% bootstrap_field add_fee_formset.empty_form.description layout='control' %}
</div>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-primary" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add fee" %}</button>
</p>
</div>
<div class="panel panel-default items"> <div class="panel panel-default items">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"> <h3 class="panel-title">

View File

@@ -198,7 +198,7 @@
</a> </a>
{% endif %} {% endif %}
</dd> </dd>
{% if order.status == "n" and not order.require_approval %} {% if order.status == "n" %}
<dt>{% trans "Expiry date" %}</dt> <dt>{% trans "Expiry date" %}</dt>
<dd> <dd>
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }} {{ order.expires|date:"SHORT_DATETIME_FORMAT" }}

View File

@@ -24,7 +24,7 @@
{% endif %} {% endif %}
{% if request.method == "POST" %} {% if request.method == "POST" %}
<fieldset> <fieldset>
<legend>{% trans "Email preview" %}</legend> <legend>{% trans "E-mail preview" %}</legend>
<div class="tab-pane mail-preview-group"> <div class="tab-pane mail-preview-group">
<div lang="{{ order.locale }}" class="mail-preview"> <div lang="{{ order.locale }}" class="mail-preview">
<strong>{{ preview_output.subject }}</strong><br><br> <strong>{{ preview_output.subject }}</strong><br><br>

View File

@@ -46,7 +46,7 @@
{% trans "active" %} {% trans "active" %}
{% endif %} {% endif %}
</dd> </dd>
<dt>{% trans "Email" %}</dt> <dt>{% trans "E-mail" %}</dt>
<dd> <dd>
{{ customer.email|default_if_none:"" }} {{ customer.email|default_if_none:"" }}
{% if customer.email and not customer.provider %} {% if customer.email and not customer.provider %}

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