forked from CGM_Public/pretix_original
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9f2443e9 | ||
|
|
1647c82be1 | ||
|
|
981a445b49 | ||
|
|
6d883fc37d |
@@ -276,8 +276,7 @@ Restarting the service can take a few seconds, especially if the update requires
|
||||
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
|
||||
version, if you want to.
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
|
||||
attention to the "Runtime and server environment" section of all release notes between your current and new version.
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
|
||||
|
||||
.. _`docker_plugininstall`:
|
||||
|
||||
|
||||
@@ -286,8 +286,7 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
|
||||
attention to the "Runtime and server environment" section of all release notes between your current and new version.
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
|
||||
|
||||
.. _`manual_plugininstall`:
|
||||
|
||||
|
||||
@@ -47,30 +47,5 @@ Or, with a docker installation::
|
||||
|
||||
$ docker exec -it pretix.service pretix create_order_transactions
|
||||
|
||||
Upgrade to 2023.6.0 or newer
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
MariaDB and MySQL are no longer supported.
|
||||
|
||||
Upgrade to 2023.8.0 or newer
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
PostgreSQL 11 is now required.
|
||||
|
||||
Upgrade to 2023.9.0 or newer
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
This release includes a migration that changes the `id` column of all core database tables from `integer`
|
||||
to `bigint`. If you have a large database, the migration step of the upgrade might take significantly longer than
|
||||
usual, so plan the update accordingly.
|
||||
|
||||
The default value for the `registration` setting in `pretix.cfg` has changed to `false`.
|
||||
|
||||
Upgrade to 2023.10.0 or newer
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
This release includes a migration that changes retroactively fills an `organizer` column in the table
|
||||
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
||||
longer than usual, so plan the update accordingly.
|
||||
|
||||
.. _blog: https://pretix.eu/about/en/blog/
|
||||
|
||||
@@ -31,7 +31,6 @@ Checking a ticket in
|
||||
This endpoint supports passing multiple check-in lists to perform a multi-event scan. However, each check-in list
|
||||
passed needs to be from a distinct event.
|
||||
|
||||
:query string expand: Expand a field inside the ``position`` object into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
|
||||
:<json string secret: Scanned QR code corresponding to the ``secret`` attribute of a ticket.
|
||||
:<json string source_type: Type of source the ``secret`` was obtained form. Defaults to ``"barcode"``.
|
||||
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
|
||||
@@ -64,7 +63,6 @@ Checking a ticket in
|
||||
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
|
||||
back to values from a parent product or from invoice addresses.
|
||||
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
|
||||
:>json list checkin_texts: List of additional texts to show to the user.
|
||||
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
||||
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
|
||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||
@@ -105,7 +103,6 @@ Checking a ticket in
|
||||
…
|
||||
},
|
||||
"require_attention": false,
|
||||
"checkin_texts": [],
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
@@ -128,7 +125,6 @@ Checking a ticket in
|
||||
…
|
||||
},
|
||||
"require_attention": false,
|
||||
"checkin_texts": [],
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
@@ -146,7 +142,6 @@ Checking a ticket in
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": true,
|
||||
"show_during_checkin": true,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -183,8 +178,7 @@ Checking a ticket in
|
||||
"status": "error",
|
||||
"reason": "invalid",
|
||||
"reason_explanation": null,
|
||||
"require_attention": false,
|
||||
"checkin_texts": []
|
||||
"require_attention": false
|
||||
}
|
||||
|
||||
**Example error response (known, but invalid ticket)**:
|
||||
@@ -199,7 +193,6 @@ Checking a ticket in
|
||||
"reason": "unpaid",
|
||||
"reason_explanation": null,
|
||||
"require_attention": false,
|
||||
"checkin_texts": [],
|
||||
"list": {
|
||||
"id": 1,
|
||||
"name": "Default check-in list",
|
||||
@@ -224,7 +217,6 @@ Checking a ticket in
|
||||
* ``rules`` - Check-in prevented by a user-defined rule.
|
||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||
* ``revoked`` - Ticket code has been revoked.
|
||||
* ``unapproved`` - Order has not yet been approved.
|
||||
* ``error`` - Internal error.
|
||||
|
||||
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||
|
||||
@@ -498,7 +498,7 @@ Order position endpoints
|
||||
``attendee_name,positionid``
|
||||
:query string order: Only return positions of the order with the given order code
|
||||
:query string search: Fuzzy search matching the attendee name, order code, invoice address name as well as to the beginning of the secret.
|
||||
:query string expand: Expand a field into a full object. Currently ``subevent``, ``item``, ``variation``, and ``answers.question`` are supported. Can be passed multiple times.
|
||||
:query string expand: Expand a field into a full object. Currently only ``subevent``, ``item``, and ``variation`` are supported. Can be passed multiple times.
|
||||
:query integer item: Only return positions with the purchased item matching the given ID.
|
||||
:query integer item__in: Only return positions with the purchased item matching one of the given comma-separated IDs.
|
||||
:query integer variation: Only return positions with the purchased item variation matching the given ID.
|
||||
@@ -632,8 +632,7 @@ Order position endpoints
|
||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||
to ``true``.
|
||||
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
|
||||
returned. Otherwise, canceled orders will return ``unpaid``. (**Deprecated**, in
|
||||
the future, this will be ignored and ``canceled`` may always be returned.)
|
||||
returned. Otherwise, canceled orders will return ``unpaid``.
|
||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
|
||||
questions that have not been filled. This is usually used to upload offline scans that already happened,
|
||||
@@ -707,7 +706,6 @@ Order position endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": true,
|
||||
"show_during_checkin": true,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -760,7 +758,6 @@ Order position endpoints
|
||||
* ``rules`` - Check-in prevented by a user-defined rule.
|
||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||
* ``revoked`` - Ticket code has been revoked.
|
||||
* ``unapproved`` - Order has not yet been approved.
|
||||
|
||||
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
|
||||
|
||||
@@ -343,8 +343,8 @@ Endpoints
|
||||
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
|
||||
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||
|
||||
If the ``plugins``, ``has_subevents``, ``meta_data`` and/or ``is_public`` fields are present in the post body this will
|
||||
determine their value. Otherwise their value will be copied from the existing event.
|
||||
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
|
||||
value. Otherwise their value will be copied from the existing event.
|
||||
|
||||
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
||||
when creating a new event for this instead.
|
||||
|
||||
@@ -29,8 +29,6 @@ position integer An integer, use
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a variation is being scanned.
|
||||
checkin_text string Text that will be shown if a ticket of this type is
|
||||
scanned (or ``null``).
|
||||
require_approval boolean If ``true``, orders with this variation will need to be
|
||||
approved by the event organizer before they can be
|
||||
paid.
|
||||
@@ -60,8 +58,6 @@ meta_data object Values set for
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``free_price_suggestion`` attribute has been added.
|
||||
The ``checkin_text`` attribute has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -98,7 +94,6 @@ Endpoints
|
||||
},
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
@@ -124,7 +119,6 @@ Endpoints
|
||||
},
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
@@ -185,7 +179,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
@@ -225,7 +218,6 @@ Endpoints
|
||||
"default_price": "10.00",
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
@@ -256,7 +248,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
@@ -318,7 +309,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": false,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
|
||||
@@ -74,8 +74,6 @@ max_per_order integer This product
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a product is being scanned.
|
||||
checkin_text string Text that will be shown if a ticket of this type is
|
||||
scanned (or ``null``).
|
||||
original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations (or ``null``).
|
||||
require_approval boolean If ``true``, orders with this product will need to be
|
||||
@@ -139,8 +137,6 @@ variations list of objects A list with o
|
||||
├ checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a variation is being scanned.
|
||||
├ checkin_text string Text that will be shown if a ticket of this type is
|
||||
scanned (or ``null``).
|
||||
├ require_approval boolean If ``true``, orders with this variation will need to be
|
||||
approved by the event organizer before they can be
|
||||
paid.
|
||||
@@ -209,7 +205,6 @@ meta_data object Values set fo
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``checkin_text`` and ``variations[x].checkin_text`` attributes have been added.
|
||||
The ``free_price_suggestion`` and ``variations[x].free_price_suggestion`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
@@ -288,7 +283,6 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": false,
|
||||
"generate_tickets": null,
|
||||
"allow_waitinglist": true,
|
||||
@@ -318,7 +312,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -338,7 +331,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -429,7 +421,6 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": false,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
@@ -456,7 +447,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -476,7 +466,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -548,7 +537,6 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"require_membership": false,
|
||||
@@ -574,7 +562,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -594,7 +581,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -654,7 +640,6 @@ Endpoints
|
||||
"allow_waitinglist": true,
|
||||
"show_quota_left": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
@@ -681,7 +666,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -701,7 +685,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -792,7 +775,6 @@ Endpoints
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
@@ -819,7 +801,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
@@ -839,7 +820,6 @@ Endpoints
|
||||
"free_price_suggestion": null,
|
||||
"active": true,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
|
||||
@@ -46,8 +46,6 @@ custom_followup_at date Internal date f
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if a ticket
|
||||
of this order is scanned.
|
||||
checkin_text string Text that will be shown if a ticket of this order is
|
||||
scanned (or ``null``).
|
||||
invoice_address object Invoice address information (can be ``null``)
|
||||
├ last_modified datetime Last modification date of the address
|
||||
├ company string Customer company name
|
||||
@@ -141,14 +139,6 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``customer`` query parameter has been added.
|
||||
|
||||
.. versionchanged:: 2023.10
|
||||
|
||||
The ``checkin_text`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2024.1
|
||||
|
||||
The ``expires`` attribute can now be passed during order creation.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -328,7 +318,6 @@ List of all orders
|
||||
"comment": "",
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"valid_if_pending": false,
|
||||
"invoice_address": {
|
||||
@@ -550,7 +539,6 @@ Fetching individual orders
|
||||
"comment": "",
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
"require_approval": false,
|
||||
"valid_if_pending": false,
|
||||
"invoice_address": {
|
||||
@@ -721,8 +709,6 @@ Updating order fields
|
||||
|
||||
* ``checkin_attention``
|
||||
|
||||
* ``checkin_text``
|
||||
|
||||
* ``locale``
|
||||
|
||||
* ``comment``
|
||||
@@ -733,8 +719,6 @@ Updating order fields
|
||||
|
||||
* ``valid_if_pending``
|
||||
|
||||
* ``expires``
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -940,7 +924,6 @@ Creating orders
|
||||
* ``comment`` (optional)
|
||||
* ``custom_followup_at`` (optional)
|
||||
* ``checkin_attention`` (optional)
|
||||
* ``checkin_text`` (optional)
|
||||
* ``require_approval`` (optional)
|
||||
* ``valid_if_pending`` (optional)
|
||||
* ``invoice_address`` (optional)
|
||||
|
||||
@@ -44,8 +44,6 @@ identifier string An arbitrary st
|
||||
ask_during_checkin boolean If ``true``, this question will not be asked while
|
||||
buying the ticket, but will show up when redeeming
|
||||
the ticket instead.
|
||||
show_during_checkin boolean If ``true``, the answer to the question will be shown
|
||||
during check-in (if the check-in client supports it).
|
||||
hidden boolean If ``true``, the question will only be shown in the
|
||||
backend.
|
||||
print_on_invoice boolean If ``true``, the question will only be shown on
|
||||
@@ -79,10 +77,6 @@ dependency_value string An old version
|
||||
for one value. **Deprecated.**
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2023.8
|
||||
|
||||
The ``show_during_checkin`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -121,7 +115,6 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"valid_number_min": null,
|
||||
@@ -201,7 +194,6 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"valid_number_min": null,
|
||||
@@ -265,7 +257,6 @@ Endpoints
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
@@ -302,7 +293,6 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
@@ -386,7 +376,6 @@ Endpoints
|
||||
"position": 2,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"show_during_checkin": false,
|
||||
"hidden": false,
|
||||
"print_on_invoice": false,
|
||||
"dependency_question": null,
|
||||
|
||||
@@ -11,7 +11,7 @@ Core
|
||||
----
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators, gift_card_transaction_display
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 278 KiB |
@@ -25,27 +25,27 @@ partition "data-based check" {
|
||||
else
|
||||
-down->[yes] "Is one or more block set on the ticket?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return error BLOCKED"
|
||||
-right->[no] "Return error BLOCKED"
|
||||
else
|
||||
-down->[no] "Is the order in status PENDING and not yet approved?"
|
||||
-down->[yes] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
--> if "" then
|
||||
-right->[yes] "Return error UNAPPROVED"
|
||||
-right->[no] "Return error INVALID_TIME"
|
||||
else
|
||||
-down->[no] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error INVALID_TIME"
|
||||
-right->[no] "Return error PRODUCT"
|
||||
else
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
-down->[yes] "Is the subevent part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error PRODUCT"
|
||||
-right->[no] "Return error INVALID"
|
||||
note bottom: TODO\ninconsistent\nwith online\ncheck
|
||||
else
|
||||
-down->[yes] "Is the subevent part of the check-in list?"
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
--> if "" then
|
||||
-right->[no] "Return error INVALID"
|
||||
note bottom: TODO\ninconsistent\nwith online\ncheck
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
-right->[no] "Is Order.require_approval set?"
|
||||
--> if "" then
|
||||
-->[yes] "Return error UNPAID "
|
||||
else
|
||||
-right->[no] "Is Order.valid_if_pending set?"
|
||||
--> if "" then
|
||||
-->[yes] "Is this an entry or exit?"
|
||||
@@ -62,9 +62,9 @@ partition "data-based check" {
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?"
|
||||
endif
|
||||
else
|
||||
-down->[yes] "Is this an entry or exit?"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 180 KiB |
@@ -42,25 +42,23 @@ endif
|
||||
else
|
||||
-down->[yes || force] "Is one or more block set on the ticket?"
|
||||
--> if "" then
|
||||
-right->[yes && !force] "Return error BLOCKED"
|
||||
-right->[no && !force] "Return error BLOCKED"
|
||||
else
|
||||
-down->[no || force] "Is the order in status PENDING and not yet approved?"
|
||||
-down->[yes || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
--> if "" then
|
||||
-right->[yes && !force] "Return error UNAPPROVED"
|
||||
-right->[no && !force] "Return error INVALID_TIME"
|
||||
else
|
||||
-down->[no || force] "If this is not an exit, is the valid_from/valid_until\nconstraint on the ticket fulfilled?"
|
||||
-down->[yes || force] "Is the product part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Return error INVALID_TIME"
|
||||
-right->[no && !force] "Return error PRODUCT"
|
||||
else
|
||||
-down->[yes || force] "Is the product part of the check-in list?"
|
||||
-down->[yes || force] "Is the subevent part of the check-in list?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Return error PRODUCT"
|
||||
-right->[no && !force] "Return error PRODUCT "
|
||||
else
|
||||
-down->[yes || force] "Is the subevent part of the check-in list?"
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
--> if "" then
|
||||
-right->[no && !force] "Return error PRODUCT "
|
||||
else
|
||||
-down->[yes] "Is the order in status PAID?"
|
||||
-right->[no && !force] "Is Order.require_approval set?"
|
||||
--> if "" then
|
||||
-->[no] "Is Order.valid_if_pending set?"
|
||||
--> if "" then
|
||||
@@ -79,8 +77,10 @@ else
|
||||
endif
|
||||
endif
|
||||
else
|
||||
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
|
||||
-->[yes] "Return error UNPAID "
|
||||
endif
|
||||
else
|
||||
-down->[yes || force] "Is this an entry or exit?\nIs the upload forced?"
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -32,7 +32,6 @@ transactions list of objects Transactions in
|
||||
├ checksum string Checksum computed from payer, reference, amount and
|
||||
date
|
||||
├ payer string Payment source
|
||||
├ external_id string Unique ID of the payment from an external source
|
||||
├ reference string Payment reference
|
||||
├ amount string Payment amount
|
||||
├ iban string Payment IBAN
|
||||
@@ -86,7 +85,6 @@ Endpoints
|
||||
"date": "26.06.2017",
|
||||
"payer": "John Doe",
|
||||
"order": null,
|
||||
"external_id": null,
|
||||
"iban": "",
|
||||
"bic": "",
|
||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||
@@ -141,7 +139,6 @@ Endpoints
|
||||
"iban": "",
|
||||
"bic": "",
|
||||
"order": null,
|
||||
"external_id": null,
|
||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
|
||||
"state": "nomatch",
|
||||
|
||||
@@ -194,23 +194,17 @@ A complete record could look like this::
|
||||
|
||||
v=spf1 a mx include:_spf.pretix.eu ~all
|
||||
|
||||
Make sure to read up on the `SPF specification`_.
|
||||
Make sure to read up on the `SPF specification`_. If you want to authenticate your emails with DKIM, set up a DNS TXT
|
||||
record for the subdomain ``pretix._domainkey`` with the following contents::
|
||||
|
||||
If you want to authenticate your emails with `DKIM`_, set up a ``CNAME`` record for the subdomain ``pretix._domainkey``
|
||||
pointing to ``dkim.pretix.eu``::
|
||||
|
||||
pretix._domainkey.mydomain.com. CNAME dkim.pretix.eu.
|
||||
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXrDk6lwOWX00e2MbiiJac6huI+gnzLf9N4G1FnBv3PXq8fz3i2q1szH72OF5mAlKm3zXO4cl/uxx+lfidS1ERbX6Bn9BRstBTQUKWC4JFj8Yk9+fwT7LWehDURazLdTzfsIjJFudLLvxtOKSaOCtMhbPX05DIhziaqVCBqgz/NQIDAQAB
|
||||
|
||||
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
|
||||
|
||||
For senders with larger volumes, Google Mail also requires you to have a `DMARC`_ policy (that may however be ``p=none``).
|
||||
|
||||
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
|
||||
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
|
||||
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
|
||||
in this case, as pretix only retries email delivery for a certain time period.
|
||||
|
||||
.. _DKIM: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
|
||||
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
||||
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
||||
.. _DMARC: https://en.wikipedia.org/wiki/DMARC
|
||||
@@ -138,7 +138,7 @@ the button-style of that checkbox with the one in the pretix shop, you can use t
|
||||
|
||||
.. note::
|
||||
|
||||
Due to compatibility with existing widget installations, the default value for ``single-item-select``
|
||||
Due to compatibilty with existing widget installations, the default value for ``single-item-select``
|
||||
is ``checkbox``. This might change in the future, so make sure, to set the attribute to
|
||||
``single-item-select="checkbox"`` if you need it.
|
||||
|
||||
@@ -196,7 +196,7 @@ settings. For example, if you set up a meta data property called "Promoted" that
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" list-type="list" filter="attr[Promoted]=Yes"></pretix-widget>
|
||||
|
||||
If you have enabled public filters in your meta data attribute configuration, a filter-form shows up. To disable, use::
|
||||
If you have enabled public filters in your meta data attribute configuration, a filter formshows up. To disable, use::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-filters></pretix-widget>
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ to get a better plain text representation of your text. Note however, that for
|
||||
security reasons you can only use the following HTML elements::
|
||||
|
||||
a, abbr, acronym, b, br, code, div, em, h1, h2,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, pre, s, span, strong,
|
||||
h3, h4, h5, h6, hr, i, li, ol, p, pre, span, strong,
|
||||
table, tbody, td, thead, tr, ul
|
||||
|
||||
Additionally, only the following attributes are allowed on them::
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ dependencies = [
|
||||
"dnspython==2.3.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==4.*",
|
||||
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.3.*",
|
||||
@@ -97,7 +97,7 @@ dependencies = [
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"static3==0.7.*",
|
||||
"stripe==7.9.*",
|
||||
"stripe==5.4.*",
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2020041600",
|
||||
"tqdm==4.*",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2023.11.0.dev0"
|
||||
__version__ = "2023.10.0.dev0"
|
||||
|
||||
@@ -109,7 +109,7 @@ LANGUAGES_RTL = {
|
||||
'ar', 'hw'
|
||||
}
|
||||
LANGUAGES_INCUBATING = {
|
||||
'fi', 'pt-br', 'gl',
|
||||
'pl', 'fi', 'pt-br', 'gl',
|
||||
}
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(os.path.dirname(__file__), 'locale'),
|
||||
@@ -267,10 +267,9 @@ CACHE_LARGE_VALUES_ALIAS = 'default'
|
||||
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
|
||||
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", ".jpg", ".gif", ".jpeg")
|
||||
PILLOW_FORMATS_QUESTIONS_FAVICON = ('PNG', 'GIF', 'JPEG', 'ICO')
|
||||
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", ".jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
||||
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
||||
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||
@@ -279,5 +278,3 @@ FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||
".bmp", ".tif", ".tiff"
|
||||
)
|
||||
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
||||
|
||||
PRETIX_MAX_ORDER_SIZE = 500
|
||||
|
||||
@@ -38,7 +38,6 @@ MAIL_FROM_ORGANIZERS = 'invalid@invalid'
|
||||
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
|
||||
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
|
||||
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
|
||||
FILE_UPLOAD_MAX_SIZE_FAVICON = 10
|
||||
DEFAULT_CURRENCY = 'EUR'
|
||||
SECRET_KEY = "build-time-secret-key"
|
||||
HAS_REDIS = False
|
||||
|
||||
@@ -424,7 +424,7 @@ class CloneEventSerializer(EventSerializer):
|
||||
new_event = super().create({**validated_data, 'plugins': None})
|
||||
|
||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
|
||||
new_event.copy_data_from(event)
|
||||
|
||||
if plugins is not None:
|
||||
new_event.set_active_plugins(plugins)
|
||||
@@ -796,8 +796,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'cancel_allow_user_paid_require_approval_fee_unknown',
|
||||
'cancel_terms_paid',
|
||||
'cancel_terms_unpaid',
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_until',
|
||||
|
||||
@@ -61,7 +61,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'checkin_attention', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -85,7 +85,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'checkin_attention', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -237,7 +237,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
'personalized', 'position', 'picture', 'available_from', 'available_until',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
|
||||
'issue_giftcard', 'meta_data',
|
||||
@@ -440,7 +440,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_file_portrait')
|
||||
@@ -486,9 +486,6 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
Question.clean_items(event, full_data.get('items'))
|
||||
return data
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
InlineItemVariationSerializer, ItemSerializer,
|
||||
)
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -585,9 +585,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
|
||||
|
||||
class OrderPaymentTypeField(serializers.Field):
|
||||
# TODO: Remove after pretix 2.2
|
||||
@@ -718,7 +715,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
fields = (
|
||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'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', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer', 'valid_if_pending'
|
||||
)
|
||||
read_only_fields = (
|
||||
@@ -774,8 +771,8 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
||||
'phone', 'valid_if_pending']
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone',
|
||||
'valid_if_pending']
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1035,14 +1032,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||
self.fields['expires'].required = False
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||
'require_approval', 'valid_if_pending', 'expires')
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'require_approval',
|
||||
'valid_if_pending')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
@@ -1051,11 +1047,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('The given payment provider is not known.')
|
||||
return pp
|
||||
|
||||
def validate_expires(self, expires):
|
||||
if expires < now():
|
||||
raise ValidationError('Expiration date must be in the future.')
|
||||
return expires
|
||||
|
||||
def validate_sales_channel(self, channel):
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError('Unknown sales channel.')
|
||||
@@ -1077,10 +1068,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(
|
||||
'An order cannot be empty.'
|
||||
)
|
||||
if len(data) > settings.PRETIX_MAX_ORDER_SIZE:
|
||||
raise ValidationError(
|
||||
'Orders cannot have more than %(max)s positions.' % {'max': settings.PRETIX_MAX_ORDER_SIZE}
|
||||
)
|
||||
errs = [{} for p in data]
|
||||
if any([p.get('positionid') for p in data]):
|
||||
if not all([p.get('positionid') for p in data]):
|
||||
@@ -1366,8 +1353,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if validated_data.get('locale', None) is None:
|
||||
validated_data['locale'] = self.context['event'].settings.locale
|
||||
order = Order(event=self.context['event'], **validated_data)
|
||||
if not validated_data.get('expires'):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
order.meta_info = "{}"
|
||||
order.total = Decimal('0.00')
|
||||
if validated_data.get('require_approval') is not None:
|
||||
|
||||
@@ -536,7 +536,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_ALREADY_REDEEMED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
|
||||
}, status=400)
|
||||
except: # we don't care e.g. about invalid version numbers
|
||||
@@ -548,7 +547,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_INVALID,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
@@ -578,7 +576,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_REVOKED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[
|
||||
0].event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||
@@ -634,7 +631,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
@@ -683,7 +679,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
return Response({
|
||||
'status': 'incomplete',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'questions': [
|
||||
QuestionSerializer(q).data for q in e.questions
|
||||
@@ -714,7 +709,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': e.code,
|
||||
'reason_explanation': e.reason,
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
@@ -722,7 +716,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
return Response({
|
||||
'status': 'ok',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=201)
|
||||
|
||||
@@ -254,7 +254,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event = serializer.save(organizer=self.request.organizer)
|
||||
|
||||
if copy_from:
|
||||
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
||||
new_event.copy_data_from(copy_from)
|
||||
|
||||
if plugins is not None:
|
||||
new_event.set_active_plugins(plugins)
|
||||
|
||||
@@ -222,8 +222,6 @@ class OrderViewSetMixin:
|
||||
qs = qs.prefetch_related('refunds', 'refunds__payment')
|
||||
if 'invoice_address' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('invoice_address')
|
||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('customer')
|
||||
|
||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
||||
return qs
|
||||
@@ -829,16 +827,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
}
|
||||
)
|
||||
|
||||
if 'checkin_text' in self.request.data and serializer.instance.checkin_text != self.request.data.get('checkin_text'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.checkin_text',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_value': self.request.data.get('checkin_text')
|
||||
}
|
||||
)
|
||||
|
||||
if 'valid_if_pending' in self.request.data and serializer.instance.valid_if_pending != self.request.data.get('valid_if_pending'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.valid_if_pending',
|
||||
|
||||
@@ -384,7 +384,7 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
def notify_webhooks(logentry_ids: list):
|
||||
if not isinstance(logentry_ids, list):
|
||||
logentry_ids = [logentry_ids]
|
||||
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
|
||||
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
|
||||
_org, _at, webhooks = None, None, None
|
||||
for logentry in qs:
|
||||
if not logentry.organizer:
|
||||
|
||||
@@ -28,5 +28,4 @@ from .items import * # noqa
|
||||
from .json import * # noqa
|
||||
from .mail import * # noqa
|
||||
from .orderlist import * # noqa
|
||||
from .reusablemedia import * # noqa
|
||||
from .waitinglist import * # noqa
|
||||
|
||||
@@ -88,7 +88,6 @@ class ItemDataExporter(ListExporter):
|
||||
_("Minimum amount per order"),
|
||||
_("Maximum amount per order"),
|
||||
_("Requires special attention"),
|
||||
_("Check-in text"),
|
||||
_("Original price"),
|
||||
_("This product is a gift card"),
|
||||
_("Require a valid membership"),
|
||||
@@ -163,7 +162,6 @@ class ItemDataExporter(ListExporter):
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
i.checkin_text or "",
|
||||
v.original_price or i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership or v.require_membership else "",
|
||||
@@ -208,7 +206,6 @@ class ItemDataExporter(ListExporter):
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
i.checkin_text or "",
|
||||
i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership else "",
|
||||
|
||||
@@ -96,7 +96,6 @@ class JSONExporter(BaseExporter):
|
||||
'min_per_order': item.min_per_order,
|
||||
'max_per_order': item.max_per_order,
|
||||
'checkin_attention': item.checkin_attention,
|
||||
'checkin_text': item.checkin_text,
|
||||
'original_price': item.original_price,
|
||||
'issue_giftcard': item.issue_giftcard,
|
||||
'meta_data': item.meta_data,
|
||||
@@ -111,7 +110,6 @@ class JSONExporter(BaseExporter):
|
||||
'description': str(variation.description),
|
||||
'position': variation.position,
|
||||
'checkin_attention': variation.checkin_attention,
|
||||
'checkin_text': variation.checkin_text,
|
||||
'require_approval': variation.require_approval,
|
||||
'require_membership': variation.require_membership,
|
||||
'sales_channels': variation.sales_channels,
|
||||
@@ -166,7 +164,6 @@ class JSONExporter(BaseExporter):
|
||||
'custom_followup_at': order.custom_followup_at,
|
||||
'require_approval': order.require_approval,
|
||||
'checkin_attention': order.checkin_attention,
|
||||
'checkin_text': order.checkin_text,
|
||||
'sales_channel': order.sales_channel,
|
||||
'expires': order.expires,
|
||||
'datetime': order.datetime,
|
||||
|
||||
@@ -275,7 +275,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Invoice numbers'))
|
||||
headers.append(_('Sales channel'))
|
||||
headers.append(_('Requires special attention'))
|
||||
headers.append(_('Check-in text'))
|
||||
headers.append(_('Comment'))
|
||||
headers.append(_('Follow-up date'))
|
||||
headers.append(_('Positions'))
|
||||
@@ -333,7 +332,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
order.code,
|
||||
order.total,
|
||||
order.get_extended_status_display(),
|
||||
order.get_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -385,7 +384,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row.append(order.invoice_numbers)
|
||||
row.append(order.sales_channel)
|
||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
||||
row.append(order.checkin_text or "")
|
||||
row.append(order.comment or "")
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
row.append(order.pcnt)
|
||||
@@ -465,7 +463,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row = [
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
order.code,
|
||||
_("canceled") if op.canceled else order.get_extended_status_display(),
|
||||
_("canceled") if op.canceled else order.get_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -640,7 +638,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
order.code,
|
||||
op.positionid,
|
||||
_("canceled") if op.canceled else order.get_extended_status_display(),
|
||||
_("canceled") if op.canceled else order.get_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
|
||||
|
||||
from ..exporter import ListExporter, OrganizerLevelExportMixin
|
||||
from ..models import ReusableMedium
|
||||
from ..signals import register_multievent_data_exporters
|
||||
|
||||
|
||||
class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'reusablemedia'
|
||||
verbose_name = _('Reusable media')
|
||||
category = pgettext_lazy('export_category', 'Reusable media')
|
||||
description = _('Download a spread sheet with the data of all reusable medias on your account.')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
media = ReusableMedium.objects.filter(
|
||||
organizer=self.organizer,
|
||||
).select_related(
|
||||
'customer', 'linked_orderposition', 'linked_giftcard',
|
||||
).order_by('created')
|
||||
|
||||
headers = [
|
||||
pgettext('reusable_medium', 'Media type'),
|
||||
pgettext('reusable_medium', 'Identifier'),
|
||||
_('Active'),
|
||||
_('Expiration date'),
|
||||
_('Customer account'),
|
||||
_('Linked ticket'),
|
||||
_('Linked gift card'),
|
||||
_('Notes'),
|
||||
]
|
||||
|
||||
yield headers
|
||||
yield self.ProgressSetTotal(total=media.count())
|
||||
|
||||
for medium in media.iterator(chunk_size=1000):
|
||||
row = [
|
||||
medium.type,
|
||||
medium.identifier,
|
||||
_('Yes') if medium.active else _('No'),
|
||||
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
|
||||
medium.customer.identifier if medium.customer_id else '',
|
||||
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
|
||||
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
|
||||
medium.notes,
|
||||
]
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return f'{self.organizer.slug}_media'
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_reusablemedia")
|
||||
def register_multievent_i_reusable_media_exporter(sender, **kwargs):
|
||||
return ReusableMediaExporter
|
||||
@@ -125,7 +125,7 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
if fname == 'title' and self.titles:
|
||||
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
|
||||
elif fname == 'salutation':
|
||||
widgets.append(Select(attrs=a, choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS))
|
||||
widgets.append(Select(attrs=a, choices=[('', '---')] + PERSON_NAME_SALUTATIONS))
|
||||
else:
|
||||
widgets.append(self.widget(attrs=a))
|
||||
super().__init__(widgets, attrs)
|
||||
@@ -136,10 +136,7 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
data = []
|
||||
for i, field in enumerate(self.scheme['fields']):
|
||||
fname, label, size = field
|
||||
fval = value.get(fname, "")
|
||||
if fname == "salutation" and fname in value and fval == "":
|
||||
fval = "empty"
|
||||
data.append(fval)
|
||||
data.append(value.get(fname, ""))
|
||||
if '_legacy' in value and not data[-1]:
|
||||
data[-1] = value.get('_legacy', '')
|
||||
elif not any(d for d in data) and '_scheme' in value:
|
||||
@@ -193,8 +190,7 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
data = {}
|
||||
data['_scheme'] = self.scheme_name
|
||||
for i, value in enumerate(data_list):
|
||||
key = self.scheme['fields'][i][0]
|
||||
data[key] = value or ''
|
||||
data[self.scheme['fields'][i][0]] = value or ''
|
||||
return data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -243,7 +239,7 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
d.pop('validators', None)
|
||||
field = forms.ChoiceField(
|
||||
**d,
|
||||
choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS
|
||||
choices=[('', '---')] + PERSON_NAME_SALUTATIONS
|
||||
)
|
||||
else:
|
||||
field = forms.CharField(**defaults)
|
||||
@@ -269,9 +265,6 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
if sum(len(v) for v in value.values() if v) > 250:
|
||||
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
|
||||
|
||||
if value.get("salutation") == "empty":
|
||||
value["salutation"] = ""
|
||||
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -209,10 +209,7 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
|
||||
date_attrs['placeholder'] = lazy(date_placeholder, str)
|
||||
time_attrs['placeholder'] = lazy(time_placeholder, str)
|
||||
date_attrs['aria-label'] = _('Date')
|
||||
time_attrs['aria-label'] = _('Time')
|
||||
if 'aria-label' in attrs:
|
||||
del attrs['aria-label']
|
||||
|
||||
widgets = (
|
||||
forms.DateInput(attrs=date_attrs, format=date_format),
|
||||
forms.TimeInput(attrs=time_attrs, format=time_format),
|
||||
|
||||
@@ -104,7 +104,7 @@ class Command(BaseCommand):
|
||||
with language(locale), override(timezone):
|
||||
for receiver, response in signal_result:
|
||||
if not response:
|
||||
continue
|
||||
return None
|
||||
ex = response(e, o, report_status)
|
||||
if ex.identifier == options['export_provider']:
|
||||
params = json.loads(options.get('parameters') or '{}')
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 4.2.4 on 2023-11-13 16:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0250_eventmetaproperty_filter_public"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="invoice_dirty",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,56 +0,0 @@
|
||||
# Generated by Django 4.2.4 on 2023-11-20 12:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F, OuterRef, Subquery
|
||||
|
||||
|
||||
def backfill_organizer(apps, schema_editor):
|
||||
LogEntry = apps.get_model("pretixbase", "LogEntry")
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
|
||||
LogEntry.objects.filter(
|
||||
organizer_link__isnull=True, event__isnull=False
|
||||
).update(organizer_link_id=Subquery(
|
||||
Event.objects.filter(pk=OuterRef('event_id')).values('organizer_id'),
|
||||
)
|
||||
)
|
||||
for ct in ContentType.objects.all():
|
||||
try:
|
||||
model = apps.get_model(ct.app_label, ct.model)
|
||||
except LookupError:
|
||||
continue
|
||||
if "organizer" in model._meta.fields:
|
||||
LogEntry.objects.filter(
|
||||
organizer_link__isnull=True, event__isnull=True, content_type=ct,
|
||||
).update(
|
||||
organizer_link_id=Subquery(model.objects.filter(pk=OuterRef('object_id')).values('organizer_id'))
|
||||
)
|
||||
elif "event" in model._meta.fields:
|
||||
LogEntry.objects.filter(
|
||||
organizer_link__isnull=True, event__isnull=True, content_type=ct,
|
||||
).update(
|
||||
organizer_link_id=Subquery(model.objects.filter(pk=OuterRef('object_id')).values('event__organizer_id'))
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0251_order_invoice_dirty"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="logentry",
|
||||
name="organizer_link",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
backfill_organizer,
|
||||
)
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
# Generated by Django 4.2.4 on 2023-09-06 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0252_logentry_organizer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="checkin_text",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="checkin_text",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="checkin_text",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="question",
|
||||
name="show_during_checkin",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.2.4 on 2023-12-06 14:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0253_checkin_info"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="logentry",
|
||||
name="organizer_link",
|
||||
field=models.ForeignKey(
|
||||
db_column="organizer_link_id",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="logentry",
|
||||
old_name="organizer_link",
|
||||
new_name="organizer",
|
||||
),
|
||||
]
|
||||
@@ -84,21 +84,13 @@ class LoggingMixin:
|
||||
from .devices import Device
|
||||
from .event import Event
|
||||
from .log import LogEntry
|
||||
from .organizer import Organizer, TeamAPIToken
|
||||
from .organizer import TeamAPIToken
|
||||
|
||||
event = None
|
||||
organizer_id = None
|
||||
if isinstance(self, Organizer):
|
||||
organizer_id = self.pk
|
||||
elif isinstance(self, Event):
|
||||
if isinstance(self, Event):
|
||||
event = self
|
||||
organizer_id = self.organizer_id
|
||||
elif hasattr(self, 'event'):
|
||||
event = self.event
|
||||
organizer_id = self.event.organizer_id
|
||||
elif hasattr(self, 'organizer_id'):
|
||||
organizer_id = self.organizer_id
|
||||
|
||||
if user and not user.is_authenticated:
|
||||
user = None
|
||||
|
||||
@@ -114,8 +106,7 @@ class LoggingMixin:
|
||||
elif isinstance(api_token, TeamAPIToken):
|
||||
kwargs['api_token'] = api_token
|
||||
|
||||
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
|
||||
organizer_id=organizer_id, **kwargs)
|
||||
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, **kwargs)
|
||||
if isinstance(data, dict):
|
||||
sensitivekeys = ['password', 'secret', 'api_key']
|
||||
|
||||
|
||||
@@ -280,8 +280,7 @@ class CheckinList(LoggedModel):
|
||||
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
||||
}
|
||||
allowed_operators = top_level_operators | {
|
||||
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before', 'entries_days_since',
|
||||
'entries_days_before',
|
||||
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
|
||||
}
|
||||
allowed_vars = {
|
||||
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
||||
@@ -310,7 +309,7 @@ class CheckinList(LoggedModel):
|
||||
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
||||
return rules
|
||||
|
||||
if operator in ('entries_since', 'entries_before', 'entries_days_since', 'entries_days_before'):
|
||||
if operator in ('entries_since', 'entries_before'):
|
||||
if len(values) != 1 or "buildTime" not in values[0]:
|
||||
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
|
||||
|
||||
@@ -353,7 +352,6 @@ class Checkin(models.Model):
|
||||
REASON_AMBIGUOUS = 'ambiguous'
|
||||
REASON_ERROR = 'error'
|
||||
REASON_BLOCKED = 'blocked'
|
||||
REASON_UNAPPROVED = 'unapproved'
|
||||
REASON_INVALID_TIME = 'invalid_time'
|
||||
REASONS = (
|
||||
(REASON_CANCELED, _('Order canceled')),
|
||||
@@ -367,7 +365,6 @@ class Checkin(models.Model):
|
||||
(REASON_AMBIGUOUS, _('Ticket code is ambiguous on list')),
|
||||
(REASON_ERROR, _('Server error')),
|
||||
(REASON_BLOCKED, _('Ticket blocked')),
|
||||
(REASON_UNAPPROVED, _('Order not approved')),
|
||||
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
|
||||
)
|
||||
|
||||
|
||||
@@ -344,7 +344,7 @@ class Discount(LoggedModel):
|
||||
|
||||
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
||||
def key(idx):
|
||||
return positions[idx][1] or 0 # subevent_id
|
||||
return positions[idx][1] # subevent_id
|
||||
|
||||
# Build groups of candidates with the same subevent, then apply our regular algorithm
|
||||
# to each group
|
||||
@@ -424,10 +424,5 @@ class Discount(LoggedModel):
|
||||
break
|
||||
|
||||
for g in candidate_groups:
|
||||
self._apply_min_count(
|
||||
positions,
|
||||
[idx for idx in g if idx in condition_candidates],
|
||||
[idx for idx in g if idx in benefit_candidates],
|
||||
result
|
||||
)
|
||||
self._apply_min_count(positions, g, g, result)
|
||||
return result
|
||||
|
||||
@@ -775,7 +775,7 @@ class Event(EventMixin, LoggedModel):
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
|
||||
def copy_data_from(self, other, skip_meta_data=False):
|
||||
def copy_data_from(self, other):
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
from ..signals import event_copy_data
|
||||
@@ -798,12 +798,6 @@ class Event(EventMixin, LoggedModel):
|
||||
self.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
|
||||
if not skip_meta_data:
|
||||
for emv in EventMetaValue.objects.filter(event=other):
|
||||
emv.pk = None
|
||||
emv.event = self
|
||||
emv.save(force_insert=True)
|
||||
|
||||
for fl in EventFooterLink.objects.filter(event=other):
|
||||
fl.pk = None
|
||||
fl.event = self
|
||||
@@ -1064,7 +1058,7 @@ class Event(EventMixin, LoggedModel):
|
||||
providers[pp.identifier] = pp
|
||||
|
||||
self._cached_payment_providers = OrderedDict(sorted(
|
||||
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name).title())
|
||||
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name))
|
||||
))
|
||||
return self._cached_payment_providers
|
||||
|
||||
|
||||
@@ -336,8 +336,6 @@ class Item(LoggedModel):
|
||||
:type min_per_order: int
|
||||
:param checkin_attention: Requires special attention at check-in
|
||||
:type checkin_attention: bool
|
||||
:param checkin_text: Additional text to show at check-in
|
||||
:type checkin_text: bool
|
||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||
:type original_price: decimal.Decimal
|
||||
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
|
||||
@@ -568,11 +566,6 @@ class Item(LoggedModel):
|
||||
'attention. You can use this for example for student tickets to indicate to the person at '
|
||||
'check-in that the student ID card still needs to be checked.')
|
||||
)
|
||||
checkin_text = models.TextField(
|
||||
verbose_name=_('Check-in text'),
|
||||
null=True, blank=True,
|
||||
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
|
||||
)
|
||||
original_price = models.DecimalField(
|
||||
verbose_name=_('Original price'),
|
||||
blank=True, null=True,
|
||||
@@ -631,7 +624,7 @@ class Item(LoggedModel):
|
||||
null=True, blank=True, max_length=16,
|
||||
verbose_name=_('Validity'),
|
||||
help_text=_(
|
||||
'When setting up a regular event, or an event series with time slots, you typically do NOT need to change '
|
||||
'When setting up a regular event, or an event series with time slots, you typically to NOT need to change '
|
||||
'this value. The default setting means that the validity time of tickets will not be decided by the '
|
||||
'product, but by the event and check-in configuration. Only use the other options if you need them to '
|
||||
'realize e.g. a booking of a year-long ticket with a dynamic start date. Note that the validity will be '
|
||||
@@ -1103,11 +1096,6 @@ class ItemVariation(models.Model):
|
||||
'attention. You can use this for example for student tickets to indicate to the person at '
|
||||
'check-in that the student ID card still needs to be checked.')
|
||||
)
|
||||
checkin_text = models.TextField(
|
||||
verbose_name=_('Check-in text'),
|
||||
null=True, blank=True,
|
||||
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='item__event__organizer')
|
||||
|
||||
@@ -1462,8 +1450,6 @@ class Question(LoggedModel):
|
||||
:param items: A set of ``Items`` objects that this question should be applied to
|
||||
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
||||
:type ask_during_checkin: bool
|
||||
:param show_during_checkin: Whether to show the answer to this question during check-in.
|
||||
:type show_during_checkin: bool
|
||||
:param hidden: Whether to only show the question in the backend
|
||||
:type hidden: bool
|
||||
:param identifier: An arbitrary, internal identifier
|
||||
@@ -1501,7 +1487,6 @@ class Question(LoggedModel):
|
||||
)
|
||||
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
|
||||
ASK_DURING_CHECKIN_UNSUPPORTED = []
|
||||
SHOW_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE]
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -1553,11 +1538,6 @@ class Question(LoggedModel):
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
show_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Show answer during check-in'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
hidden = models.BooleanField(
|
||||
verbose_name=_('Hidden question'),
|
||||
help_text=_('This question will only show up in the backend.'),
|
||||
|
||||
@@ -78,7 +78,6 @@ class LogEntry(models.Model):
|
||||
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
|
||||
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
organizer = models.ForeignKey('Organizer', null=True, blank=True, on_delete=models.PROTECT, db_column='organizer_link_id')
|
||||
action_type = models.CharField(max_length=255)
|
||||
data = models.TextField(default='{}')
|
||||
visible = models.BooleanField(default=True)
|
||||
@@ -123,6 +122,20 @@ class LogEntry(models.Model):
|
||||
typepath = typepath.rsplit('.', 1)[0]
|
||||
return no_type
|
||||
|
||||
@cached_property
|
||||
def organizer(self):
|
||||
from .organizer import Organizer
|
||||
|
||||
if self.event:
|
||||
return self.event.organizer
|
||||
elif hasattr(self.content_object, 'event'):
|
||||
return self.content_object.event.organizer
|
||||
elif hasattr(self.content_object, 'organizer'):
|
||||
return self.content_object.organizer
|
||||
elif isinstance(self.content_object, Organizer):
|
||||
return self.content_object
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def display_object(self):
|
||||
from . import (
|
||||
|
||||
@@ -244,11 +244,6 @@ class Order(LockModel, LoggedModel):
|
||||
'special attention. This will not show any details or custom message, so you need to brief your '
|
||||
'check-in staff how to handle these cases.')
|
||||
)
|
||||
checkin_text = models.TextField(
|
||||
verbose_name=_('Check-in text'),
|
||||
null=True, blank=True,
|
||||
help_text=_('This text will be shown by the check-in app if a ticket of this order is scanned.')
|
||||
)
|
||||
expiry_reminder_sent = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
@@ -271,10 +266,6 @@ class Order(LockModel, LoggedModel):
|
||||
default=False,
|
||||
verbose_name=_('E-mail address verified')
|
||||
)
|
||||
invoice_dirty = models.BooleanField(
|
||||
# Invoice needs to be re-issued when the order is paid again
|
||||
default=False,
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
@@ -334,18 +325,6 @@ class Order(LockModel, LoggedModel):
|
||||
def email_confirm_hash(self):
|
||||
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
|
||||
|
||||
def get_extended_status_display(self):
|
||||
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
|
||||
# and pretixpresale/event/fragment_order_status.html
|
||||
if self.status == Order.STATUS_PENDING:
|
||||
if self.require_approval:
|
||||
return _("approval pending")
|
||||
elif self.valid_if_pending:
|
||||
return pgettext_lazy("order state", "pending (confirmed)")
|
||||
elif self.status == Order.STATUS_PAID and self.count_positions == 0:
|
||||
return _("canceled (paid fee)")
|
||||
return self.get_status_display()
|
||||
|
||||
@property
|
||||
def fees(self):
|
||||
"""
|
||||
@@ -1844,7 +1823,7 @@ class OrderPayment(models.Model):
|
||||
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
||||
ignore_date=False, lock=True, payment_refund_sum=0, allow_generate_invoice=True):
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_qualified,
|
||||
generate_invoice, invoice_qualified,
|
||||
)
|
||||
from pretix.base.services.locking import LOCK_TRUST_WINDOW
|
||||
|
||||
@@ -1862,14 +1841,9 @@ class OrderPayment(models.Model):
|
||||
cancellations = self.order.invoices.filter(is_cancellation=True).count()
|
||||
gen_invoice = (
|
||||
(invoices == 0 and self.order.event.settings.get('invoice_generate') in ('True', 'paid')) or
|
||||
0 < invoices <= cancellations or
|
||||
self.order.invoice_dirty
|
||||
0 < invoices <= cancellations
|
||||
)
|
||||
if gen_invoice:
|
||||
if invoices:
|
||||
last_i = self.order.invoices.filter(is_cancellation=False).last()
|
||||
if not last_i.canceled:
|
||||
generate_cancellation(last_i)
|
||||
invoice = generate_invoice(
|
||||
self.order,
|
||||
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
|
||||
@@ -2143,12 +2117,6 @@ class OrderRefund(models.Model):
|
||||
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
||||
if 'update_fields' in kwargs:
|
||||
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
|
||||
|
||||
if self.state == OrderRefund.REFUND_STATE_DONE and not self.execution_date:
|
||||
self.execution_date = now()
|
||||
if 'update_fields' in kwargs:
|
||||
kwargs['update_fields'] = {'execution_date'}.union(kwargs['update_fields'])
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -2436,17 +2404,6 @@ class OrderPosition(AbstractPosition):
|
||||
return True
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def checkin_texts(self):
|
||||
texts = []
|
||||
if self.order.checkin_text:
|
||||
texts.append(self.order.checkin_text)
|
||||
if self.variation_id and self.variation.checkin_text:
|
||||
texts.append(self.variation.checkin_text)
|
||||
if self.item.checkin_text:
|
||||
texts.append(self.item.checkin_text)
|
||||
return texts
|
||||
|
||||
@property
|
||||
def checkins(self):
|
||||
"""
|
||||
|
||||
@@ -41,7 +41,6 @@ from typing import List, Optional
|
||||
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import DatabaseError, transaction
|
||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
|
||||
@@ -379,9 +378,8 @@ class CartManager:
|
||||
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
|
||||
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
|
||||
not op.position.addon_to_id])
|
||||
limit = min(int(self.event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
|
||||
if cartsize > limit:
|
||||
raise CartError(error_messages['max_items'] % limit)
|
||||
if cartsize > int(self.event.settings.max_items_per_order):
|
||||
raise CartError(error_messages['max_items'] % self.event.settings.max_items_per_order)
|
||||
|
||||
def _check_item_constraints(self, op, current_ops=[]):
|
||||
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from functools import partial, reduce
|
||||
@@ -66,8 +65,6 @@ from pretix.helpers.jsonlogic_query import (
|
||||
MinutesSince, tolerance,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_time(t=None, value=None, ev=None, now_dt=None):
|
||||
now_dt = now_dt or now()
|
||||
@@ -202,7 +199,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
'var': values[0]["var"],
|
||||
'rhs': values[1:],
|
||||
}
|
||||
elif any(t in values[0] for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
|
||||
elif "entries_since" in values[0] or "entries_before" in values[0]:
|
||||
_var_explanations[new_var_name] = {
|
||||
'operator': operator,
|
||||
'var': values[0],
|
||||
@@ -280,13 +277,11 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
var_weights[vname] = (500, 0)
|
||||
var_texts[vname] = _('Wrong entrance gate')
|
||||
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday') \
|
||||
or (isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before"))):
|
||||
or (isinstance(var, dict) and ("entries_since" in var or "entries_before" in var)):
|
||||
w = {
|
||||
'minutes_since_first_entry': 80,
|
||||
'minutes_since_last_entry': 90,
|
||||
'entries_days': 100,
|
||||
'entries_days_since': 105,
|
||||
'entries_days_before': 105,
|
||||
'entries_since': 110,
|
||||
'entries_before': 110,
|
||||
'entries_number': 120,
|
||||
@@ -309,12 +304,10 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
'entries_today': _('number of entries today'),
|
||||
'entries_since': _('number of entries since {datetime}'),
|
||||
'entries_before': _('number of entries before {datetime}'),
|
||||
'entries_days_since': _('number of days with an entry since {datetime}'),
|
||||
'entries_days_before': _('number of days with an entry before {datetime}'),
|
||||
'now_isoweekday': _('week day'),
|
||||
}
|
||||
|
||||
if isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
|
||||
if isinstance(var, dict) and ("entries_since" in var or "entries_before" in var):
|
||||
varname = list(var.keys())[0]
|
||||
cutoff = _build_time(*var[varname][0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
|
||||
if abs(now_dt - cutoff) < timedelta(hours=12):
|
||||
@@ -414,8 +407,6 @@ def _get_logic_environment(ev, rule_data, now_dt):
|
||||
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
||||
logic.add_operation('entries_since', lambda t1: rule_data.entries_since(t1))
|
||||
logic.add_operation('entries_before', lambda t1: rule_data.entries_before(t1))
|
||||
logic.add_operation('entries_days_since', lambda t1: rule_data.entries_days_since(t1))
|
||||
logic.add_operation('entries_days_before', lambda t1: rule_data.entries_days_before(t1))
|
||||
return logic
|
||||
|
||||
|
||||
@@ -473,32 +464,6 @@ class LazyRuleVars:
|
||||
self.__cache['entries_before', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__lt=cutoff).count()
|
||||
return self.__cache['entries_before', cutoff]
|
||||
|
||||
def entries_days_since(self, cutoff):
|
||||
tz = self._clist.event.timezone
|
||||
with override(tz):
|
||||
if ('entries_days_since', cutoff) not in self.__cache:
|
||||
self.__cache['entries_days_since', cutoff] = self._position.checkins.filter(
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list=self._clist,
|
||||
datetime__gte=cutoff
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('day').distinct().count()
|
||||
return self.__cache['entries_days_since', cutoff]
|
||||
|
||||
def entries_days_before(self, cutoff):
|
||||
tz = self._clist.event.timezone
|
||||
with override(tz):
|
||||
if ('entries_days_before', cutoff) not in self.__cache:
|
||||
self.__cache['entries_days_before', cutoff] = self._position.checkins.filter(
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list=self._clist,
|
||||
datetime__lt=cutoff
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('day').distinct().count()
|
||||
return self.__cache['entries_days_before', cutoff]
|
||||
|
||||
@cached_property
|
||||
def entries_days(self):
|
||||
tz = self._clist.event.timezone
|
||||
@@ -565,8 +530,7 @@ class SQLLogic:
|
||||
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
|
||||
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
|
||||
}
|
||||
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before',
|
||||
'entries_days_since', 'entries_days_before'}
|
||||
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'}
|
||||
|
||||
def operation_to_expression(self, rule):
|
||||
if not isinstance(rule, dict):
|
||||
@@ -644,42 +608,6 @@ class SQLLogic:
|
||||
Value(0),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
elif operator == 'entries_days_since':
|
||||
tz = self.list.event.timezone
|
||||
return Coalesce(
|
||||
Subquery(
|
||||
Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list_id=self.list.pk,
|
||||
datetime__gte=self.operation_to_expression(values[0]),
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('position_id').order_by().annotate(
|
||||
c=Count('day', distinct=True)
|
||||
).values('c')
|
||||
),
|
||||
Value(0),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
elif operator == 'entries_days_before':
|
||||
tz = self.list.event.timezone
|
||||
return Coalesce(
|
||||
Subquery(
|
||||
Checkin.objects.filter(
|
||||
position_id=OuterRef('pk'),
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
list_id=self.list.pk,
|
||||
datetime__lt=self.operation_to_expression(values[0]),
|
||||
).annotate(
|
||||
day=TruncDate('datetime', tzinfo=tz)
|
||||
).values('position_id').order_by().annotate(
|
||||
c=Count('day', distinct=True)
|
||||
).values('c')
|
||||
),
|
||||
Value(0),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
elif operator == 'var':
|
||||
if values[0] == 'now':
|
||||
return Value(now().astimezone(timezone.utc))
|
||||
@@ -946,15 +874,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
'blocked'
|
||||
)
|
||||
|
||||
if op.order.status == Order.STATUS_PENDING and op.order.require_approval:
|
||||
if force:
|
||||
force_used = True
|
||||
else:
|
||||
raise CheckInError(
|
||||
_('This order is not yet approved.'),
|
||||
'unapproved',
|
||||
)
|
||||
|
||||
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > dt:
|
||||
if force:
|
||||
force_used = True
|
||||
@@ -1022,6 +941,14 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
'product'
|
||||
)
|
||||
|
||||
if op.order.status != Order.STATUS_PAID and op.order.require_approval:
|
||||
if force:
|
||||
force_used = True
|
||||
else:
|
||||
raise CheckInError(
|
||||
_('This order is not yet approved.'),
|
||||
'unpaid'
|
||||
)
|
||||
elif op.order.status != Order.STATUS_PAID and not op.order.valid_if_pending and not (
|
||||
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
|
||||
):
|
||||
@@ -1036,16 +963,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
if type == Checkin.TYPE_ENTRY and clist.rules:
|
||||
rule_data = LazyRuleVars(op, clist, dt, gate=gate)
|
||||
logic = _get_logic_environment(op.subevent or clist.event, rule_data, now_dt=dt)
|
||||
try:
|
||||
logic_result = logic.apply(clist.rules, rule_data)
|
||||
except Exception:
|
||||
logger.exception("Check-in rule evaluation failed")
|
||||
raise CheckInError(
|
||||
_('Evaluation of custom rules has failed.'),
|
||||
'rules',
|
||||
)
|
||||
|
||||
if not logic_result:
|
||||
if not logic.apply(clist.rules, rule_data):
|
||||
if force:
|
||||
force_used = True
|
||||
else:
|
||||
|
||||
@@ -395,10 +395,6 @@ def generate_invoice(order: Order, trigger_pdf=True):
|
||||
if order.status == Order.STATUS_CANCELED:
|
||||
generate_cancellation(invoice, trigger_pdf)
|
||||
|
||||
if order.invoice_dirty:
|
||||
order.invoice_dirty = False
|
||||
order.save(update_fields=['invoice_dirty'])
|
||||
|
||||
return invoice
|
||||
|
||||
|
||||
|
||||
@@ -100,8 +100,7 @@ def lock_objects(objects, *, shared_lock_objects=None, replace_exclusive_with_sh
|
||||
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
|
||||
shared_keys = set(pg_lock_key(obj) for obj in shared_lock_objects) if shared_lock_objects else set()
|
||||
exclusive_keys = set(pg_lock_key(obj) for obj in objects)
|
||||
if replace_exclusive_with_shared_when_exclusive_are_more_than and shared_keys and \
|
||||
len(exclusive_keys) > replace_exclusive_with_shared_when_exclusive_are_more_than:
|
||||
if replace_exclusive_with_shared_when_exclusive_are_more_than and len(exclusive_keys) > replace_exclusive_with_shared_when_exclusive_are_more_than:
|
||||
exclusive_keys = shared_keys
|
||||
keys = sorted(list(shared_keys | exclusive_keys))
|
||||
calls = ", ".join([
|
||||
|
||||
@@ -183,7 +183,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if auto_email:
|
||||
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
|
||||
headers['Auto-Submitted'] = 'auto-generated'
|
||||
headers.setdefault('X-Mailer', 'pretix')
|
||||
|
||||
with language(locale):
|
||||
if isinstance(context, dict) and event:
|
||||
@@ -574,11 +573,8 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
except smtplib.SMTPRecipientsRefused as e:
|
||||
smtp_codes = [a[0] for a in e.recipients.values()]
|
||||
|
||||
if not any(c >= 500 for c in smtp_codes) or any(b'Message is too large' in a[1] for a in e.recipients.values()):
|
||||
# This is not a permanent failure (mailbox full, service unavailable), retry later, but with large
|
||||
# intervals. One would think that "Message is too lage" is a permanent failure, but apparently it is not.
|
||||
# We have documented cases of emails to Microsoft returning the error occasionally and then later
|
||||
# allowing the very same email.
|
||||
if not any(c >= 500 for c in smtp_codes):
|
||||
# Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals
|
||||
try:
|
||||
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800, 1800][self.request.retries])
|
||||
except MaxRetriesExceededError:
|
||||
|
||||
@@ -30,7 +30,6 @@ from pretix.base.models import LogEntry, NotificationSetting, User
|
||||
from pretix.base.notifications import Notification, get_all_notification_types
|
||||
from pretix.base.services.mail import mail_send_task
|
||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
||||
from pretix.base.signals import notification
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
@@ -91,8 +90,6 @@ def notify(logentry_ids: list):
|
||||
if enabled and um not in notify_specific:
|
||||
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
|
||||
|
||||
notification.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, acks_late=True, max_retries=9, default_retry_delay=900)
|
||||
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
|
||||
|
||||
@@ -23,7 +23,6 @@ import csv
|
||||
import io
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings as django_settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.timezone import now
|
||||
@@ -92,15 +91,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
||||
user = User.objects.get(pk=user)
|
||||
with language(locale, event.settings.region):
|
||||
cols = get_all_columns(event)
|
||||
try:
|
||||
parsed = parse_csv(cf.file, charset=charset)
|
||||
except UnicodeDecodeError as e:
|
||||
raise DataImportError(
|
||||
_(
|
||||
'Error decoding special characters in your file: {message}').format(
|
||||
message=str(e)
|
||||
)
|
||||
)
|
||||
parsed = parse_csv(cf.file, charset=charset)
|
||||
orders = []
|
||||
order = None
|
||||
data = []
|
||||
@@ -125,11 +116,6 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
||||
)
|
||||
data.append(values)
|
||||
|
||||
if settings['orders'] == 'one' and len(data) > django_settings.PRETIX_MAX_ORDER_SIZE:
|
||||
raise DataImportError(
|
||||
_('Orders cannot have more than %(max)s positions.') % {'max': django_settings.PRETIX_MAX_ORDER_SIZE}
|
||||
)
|
||||
|
||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||
# shorter. We'll see what works better in reality…
|
||||
lock_seats = []
|
||||
|
||||
@@ -1512,7 +1512,6 @@ class OrderChangeManager:
|
||||
"You need to select at least %(min)s items of the product %(product)s.",
|
||||
"min"
|
||||
),
|
||||
'max_order_size': gettext_lazy('Orders cannot have more than %(max)s positions.'),
|
||||
}
|
||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||
@@ -2600,14 +2599,6 @@ class OrderChangeManager:
|
||||
self.order.total = total + payment_fee
|
||||
self.order.save()
|
||||
|
||||
def _check_order_size(self):
|
||||
if (len(self.order.positions.all()) + len([op for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
|
||||
raise OrderError(
|
||||
self.error_messages['max_order_size'] % {
|
||||
'max': settings.PRETIX_MAX_ORDER_SIZE,
|
||||
}
|
||||
)
|
||||
|
||||
def _payment_fee_diff(self):
|
||||
total = self.order.total + self._totaldiff
|
||||
if self.open_payment:
|
||||
@@ -2628,37 +2619,14 @@ class OrderChangeManager:
|
||||
def _reissue_invoice(self):
|
||||
i = self.order.invoices.filter(is_cancellation=False).last()
|
||||
if self.reissue_invoice and self._invoice_dirty:
|
||||
order_now_qualified = invoice_qualified(self.order)
|
||||
invoice_should_be_generated_now = (
|
||||
self.event.settings.invoice_generate == "True" or (
|
||||
self.event.settings.invoice_generate == "paid" and
|
||||
self.open_payment is not None and
|
||||
self.open_payment.payment_provider.requires_invoice_immediately
|
||||
) or (
|
||||
self.event.settings.invoice_generate == "paid" and
|
||||
self.order.status == Order.STATUS_PAID
|
||||
) or (
|
||||
# Backwards-compatible behaviour
|
||||
self.event.settings.invoice_generate not in ("True", "paid") and
|
||||
i and
|
||||
not i.canceled
|
||||
)
|
||||
)
|
||||
invoice_should_be_generated_later = not invoice_should_be_generated_now and (
|
||||
self.event.settings.invoice_generate in ("True", "paid")
|
||||
)
|
||||
|
||||
if order_now_qualified:
|
||||
if invoice_should_be_generated_now:
|
||||
if i and not i.canceled:
|
||||
self._invoices.append(generate_cancellation(i))
|
||||
self._invoices.append(generate_invoice(self.order))
|
||||
elif invoice_should_be_generated_later:
|
||||
self.order.invoice_dirty = True
|
||||
self.order.save(update_fields=["invoice_dirty"])
|
||||
else:
|
||||
if i and not i.canceled:
|
||||
self._invoices.append(generate_cancellation(i))
|
||||
if i and not i.refered.exists():
|
||||
self._invoices.append(generate_cancellation(i))
|
||||
if invoice_qualified(self.order) and \
|
||||
(i or
|
||||
self.event.settings.invoice_generate == 'True' or (
|
||||
self.open_payment is not None and self.event.settings.invoice_generate == 'paid' and
|
||||
self.open_payment.payment_provider.requires_invoice_immediately)):
|
||||
self._invoices.append(generate_invoice(self.order))
|
||||
|
||||
def _check_complete_cancel(self):
|
||||
current = self.order.positions.count()
|
||||
@@ -2748,7 +2716,6 @@ class OrderChangeManager:
|
||||
|
||||
# finally, incorporate difference in payment fees
|
||||
self._payment_fee_diff()
|
||||
self._check_order_size()
|
||||
|
||||
with transaction.atomic():
|
||||
locked_instance = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
|
||||
|
||||
@@ -112,7 +112,7 @@ def dictsum(*dicts) -> dict:
|
||||
|
||||
def order_overview(
|
||||
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
|
||||
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None
|
||||
admission_only=False, base_qs=None, base_fees_qs=None,
|
||||
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
|
||||
items = event.items.all().select_related(
|
||||
'category', # for re-grouping
|
||||
@@ -125,11 +125,6 @@ def order_overview(
|
||||
qs = qs.filter(subevent__in=subevent)
|
||||
elif subevent:
|
||||
qs = qs.filter(subevent=subevent)
|
||||
if subevent_date_from:
|
||||
qs = qs.filter(subevent__date_from__gte=subevent_date_from)
|
||||
if subevent_date_until:
|
||||
qs = qs.filter(subevent__date_from__lt=subevent_date_until)
|
||||
|
||||
if admission_only:
|
||||
qs = qs.filter(item__admission=True)
|
||||
items = items.filter(admission=True)
|
||||
@@ -237,7 +232,7 @@ def order_overview(
|
||||
payment_cat_obj.name = _('Fees')
|
||||
payment_items = []
|
||||
|
||||
if subevent is None and not subevent_date_from and not subevent_date_until and fees:
|
||||
if subevent is None and fees:
|
||||
qs = OrderFee.all if base_fees_qs is None else base_fees_qs
|
||||
qs = qs.filter(
|
||||
order__event=event
|
||||
|
||||
@@ -306,11 +306,9 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'serializer_kwargs': dict(
|
||||
min_value=1,
|
||||
max_value=settings.PRETIX_MAX_ORDER_SIZE,
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
min_value=1,
|
||||
max_value=settings.PRETIX_MAX_ORDER_SIZE,
|
||||
required=True,
|
||||
label=_("Maximum number of items per order"),
|
||||
help_text=_("Add-on products will not be counted.")
|
||||
@@ -1938,32 +1936,6 @@ DEFAULTS = {
|
||||
label=_("Do not allow cancellations after"),
|
||||
)
|
||||
},
|
||||
'cancel_terms_paid': {
|
||||
'default': None,
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Terms of cancellation"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown when cancellation is allowed for a paid order. Leave empty if you "
|
||||
"want pretix to automatically generate the terms of cancellation based on your settings.")
|
||||
)
|
||||
},
|
||||
'cancel_terms_unpaid': {
|
||||
'default': None,
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Terms of cancellation"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown when cancellation is allowed for an unpaid or free order. Leave empty "
|
||||
"if you want pretix to automatically generate the terms of cancellation based on your settings.")
|
||||
)
|
||||
},
|
||||
'contact_mail': {
|
||||
'default': None,
|
||||
'type': str,
|
||||
@@ -2912,25 +2884,6 @@ Your {organizer} team""")) # noqa: W291
|
||||
label=_('Use header image also for events without an individually uploaded logo'),
|
||||
)
|
||||
},
|
||||
'favicon': {
|
||||
'default': None,
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Favicon'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
||||
'We recommend a size of at least 200x200px to accommodate most devices.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif', 'image/x-icon', 'image/vnd.microsoft.icon',
|
||||
],
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||
)
|
||||
},
|
||||
'og_image': {
|
||||
'default': None,
|
||||
'type': File,
|
||||
|
||||
+11
-39
@@ -153,47 +153,19 @@ class BaseDataShredder:
|
||||
|
||||
|
||||
def shred_log_fields(logentry, banlist=None, whitelist=None):
|
||||
def _shred(d, banlist, whitelist):
|
||||
shredded = False
|
||||
if whitelist:
|
||||
for k, v in d.items():
|
||||
if k not in whitelist:
|
||||
if isinstance(d[k], list):
|
||||
newlist = []
|
||||
for i in d[k]:
|
||||
if isinstance(i, dict):
|
||||
_shred(i, None, [None])
|
||||
else:
|
||||
i = '█'
|
||||
newlist.append(i)
|
||||
d[k] = newlist
|
||||
elif isinstance(d[k], dict):
|
||||
_shred(d[k], None, [None])
|
||||
elif d[k]:
|
||||
d[k] = '█'
|
||||
shredded = True
|
||||
elif banlist:
|
||||
for k in banlist:
|
||||
if k in d:
|
||||
if isinstance(d[k], list):
|
||||
newlist = []
|
||||
for i in d[k]:
|
||||
if isinstance(i, dict):
|
||||
_shred(i, None, [None])
|
||||
else:
|
||||
i = '█'
|
||||
newlist.append(i)
|
||||
d[k] = newlist
|
||||
elif isinstance(d[k], dict):
|
||||
_shred(d[k], None, [None])
|
||||
elif d[k]:
|
||||
d[k] = '█'
|
||||
shredded = True
|
||||
return shredded
|
||||
|
||||
d = logentry.parsed_data
|
||||
initial_data = copy.copy(d)
|
||||
shredded = _shred(d, banlist, whitelist)
|
||||
shredded = False
|
||||
if whitelist:
|
||||
for k, v in d.items():
|
||||
if k not in whitelist:
|
||||
d[k] = '█'
|
||||
shredded = True
|
||||
elif banlist:
|
||||
for f in banlist:
|
||||
if f in d:
|
||||
d[f] = '█'
|
||||
shredded = True
|
||||
if d != initial_data:
|
||||
logentry.data = json.dumps(d)
|
||||
logentry.shredded = logentry.shredded or shredded
|
||||
|
||||
@@ -279,15 +279,6 @@ however for this signal, the ``sender`` **may also be None** to allow creating t
|
||||
notification settings!
|
||||
"""
|
||||
|
||||
notification = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``logentry_id``, ``notification_type``
|
||||
|
||||
This signal is sent out when a notification is sent.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_sales_channels = django.dispatch.Signal()
|
||||
"""
|
||||
This signal is sent out to get all known sales channels types. Receivers should return an
|
||||
|
||||
@@ -65,8 +65,6 @@ ALLOWED_TAGS_SNIPPET = [
|
||||
'i',
|
||||
'strong',
|
||||
'span',
|
||||
'strike',
|
||||
's',
|
||||
# Update doc/user/markdown.rst if you change this!
|
||||
]
|
||||
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
||||
|
||||
@@ -34,7 +34,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse, JsonResponse, QueryDict
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import redirect, render
|
||||
from django.test import RequestFactory
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
@@ -47,7 +47,6 @@ from redis import ResponseError
|
||||
from pretix.base.models import CachedFile, User
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
|
||||
logger = logging.getLogger('pretix.base.tasks')
|
||||
|
||||
@@ -153,7 +152,7 @@ class AsyncMixin:
|
||||
'redirect': self.get_success_url(value),
|
||||
'message': str(self.get_success_message(value))
|
||||
})
|
||||
return redirect_to_url(self.get_success_url(value))
|
||||
return redirect(self.get_success_url(value))
|
||||
|
||||
def error(self, exception):
|
||||
if isinstance(exception, PermissionDenied):
|
||||
@@ -166,7 +165,7 @@ class AsyncMixin:
|
||||
'redirect': self.get_error_url(),
|
||||
'message': str(self.get_error_message(exception))
|
||||
})
|
||||
return redirect_to_url(self.get_error_url())
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
def get_error_message(self, exception):
|
||||
if isinstance(exception, dict) and exception['exc_type'] in self.known_errortypes:
|
||||
@@ -204,7 +203,7 @@ class AsyncAction(AsyncMixin):
|
||||
return self.success(res.info)
|
||||
else:
|
||||
return self.error(res.info)
|
||||
return redirect_to_url(self.get_check_url(res.id, False))
|
||||
return redirect(self.get_check_url(res.id, False))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
@@ -376,7 +375,7 @@ class AsyncFormView(AsyncMixin, FormView):
|
||||
return self.success(res.info)
|
||||
else:
|
||||
return self.error(res.info)
|
||||
return redirect_to_url(self.get_check_url(res.id, False))
|
||||
return redirect(self.get_check_url(res.id, False))
|
||||
|
||||
|
||||
class AsyncPostView(AsyncMixin, View):
|
||||
@@ -479,4 +478,4 @@ class AsyncPostView(AsyncMixin, View):
|
||||
return self.success(res.info)
|
||||
else:
|
||||
return self.error(res.info)
|
||||
return redirect_to_url(self.get_check_url(res.id, False))
|
||||
return redirect(self.get_check_url(res.id, False))
|
||||
|
||||
@@ -730,8 +730,6 @@ class CancelSettingsForm(SettingsForm):
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'cancel_allow_user_paid_require_approval_fee_unknown',
|
||||
'cancel_terms_paid',
|
||||
'cancel_terms_unpaid',
|
||||
'change_allow_user_variation',
|
||||
'change_allow_user_price',
|
||||
'change_allow_user_until',
|
||||
|
||||
@@ -57,7 +57,7 @@ from pretix.base.forms.widgets import (
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
|
||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SubEvent,
|
||||
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent,
|
||||
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
@@ -230,7 +230,6 @@ class OrderFilterForm(FilterForm):
|
||||
('partially_paid', _('Partially paid')),
|
||||
('underpaid', _('Underpaid (but confirmed)')),
|
||||
('pendingpaid', _('Pending (but fully paid)')),
|
||||
('pendingnopayment', _('Pending (but no current payment)')),
|
||||
)),
|
||||
(_('Approval process'), (
|
||||
('na', _('Approved, payment pending')),
|
||||
@@ -328,18 +327,6 @@ class OrderFilterForm(FilterForm):
|
||||
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
|
||||
& Q(require_approval=False)
|
||||
)
|
||||
elif s == 'pendingnopayment':
|
||||
qs = qs.exclude(
|
||||
Exists(
|
||||
OrderPayment.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)
|
||||
)
|
||||
)
|
||||
).filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=False,
|
||||
)
|
||||
elif s == 'partially_paid':
|
||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
||||
qs = qs.filter(
|
||||
@@ -591,10 +578,11 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
widget=FilterNullBooleanSelect,
|
||||
label=_('At least one ticket with check-in'),
|
||||
)
|
||||
quota = SafeModelChoiceField(
|
||||
queryset=Quota.objects.none(),
|
||||
label=_('Affected quota'),
|
||||
checkin_attention = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=FilterNullBooleanSelect,
|
||||
label=_('Requires special attention'),
|
||||
help_text=_('Only matches orders with the attention checkbox set directly for the order, not based on the product.'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -679,17 +667,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
label=_('Ticket secret'),
|
||||
required=False
|
||||
)
|
||||
self.fields['quota'].queryset = self.event.quotas.all()
|
||||
self.fields['quota'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
}
|
||||
)
|
||||
self.fields['quota'].widget.choices = self.fields['quota'].choices
|
||||
for q in self.event.questions.all():
|
||||
self.fields['question_{}'.format(q.pk)] = forms.CharField(
|
||||
label=q.question,
|
||||
@@ -783,12 +760,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
qs = qs.filter(
|
||||
all_positions__secret__icontains=fdata.get('ticket_secret')
|
||||
).distinct()
|
||||
if fdata.get('quota'):
|
||||
quota = fdata['quota']
|
||||
qs = qs.filter(
|
||||
Q(all_positions__item__in=quota.items.all(), all_positions__variation__isnull=True) |
|
||||
Q(all_positions__variation__in=quota.variations.all())
|
||||
).distinct()
|
||||
for q in self.event.questions.all():
|
||||
if fdata.get(f'question_{q.pk}'):
|
||||
answers = QuestionAnswer.objects.filter(
|
||||
|
||||
@@ -133,14 +133,6 @@ class QuestionForm(I18nModelForm):
|
||||
|
||||
return val
|
||||
|
||||
def clean_show_during_checkin(self):
|
||||
val = self.cleaned_data.get('show_during_checkin')
|
||||
|
||||
if val and self.cleaned_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
return val
|
||||
|
||||
def clean_identifier(self):
|
||||
val = self.cleaned_data.get('identifier')
|
||||
Question._clean_identifier(self.instance.event, val, self.instance)
|
||||
@@ -163,7 +155,6 @@ class QuestionForm(I18nModelForm):
|
||||
'type',
|
||||
'required',
|
||||
'ask_during_checkin',
|
||||
'show_during_checkin',
|
||||
'hidden',
|
||||
'identifier',
|
||||
'items',
|
||||
@@ -388,7 +379,6 @@ class ItemCreateForm(I18nModelForm):
|
||||
'max_per_order',
|
||||
'generate_tickets',
|
||||
'checkin_attention',
|
||||
'checkin_text',
|
||||
'free_price',
|
||||
'original_price',
|
||||
'sales_channels',
|
||||
@@ -614,15 +604,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
|
||||
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
|
||||
|
||||
self.fields['validity_dynamic_start_choice'] = forms.TypedChoiceField(
|
||||
label=_("Start of validity"),
|
||||
choices=(
|
||||
("False", _("Purchase date")),
|
||||
("True", _("Date chosen by customer")),
|
||||
),
|
||||
coerce=lambda x: x == 'True',
|
||||
)
|
||||
|
||||
qs = self.event.organizer.membership_types.all()
|
||||
if qs:
|
||||
self.fields['require_membership_types'].queryset = qs
|
||||
@@ -670,7 +651,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
if d.get('grant_membership_type'):
|
||||
if not d['grant_membership_type'].transferable and not d['personalized']:
|
||||
self.add_error(
|
||||
'personalized' if d.get('admission') else 'admission',
|
||||
'personalized' if d['admission'] else 'admission',
|
||||
_("Your product grants a non-transferable membership and should therefore be a personalized "
|
||||
"admission ticket. Otherwise customers might not be able to use the membership later. If you "
|
||||
"want the membership to be non-personalized, set the membership type to be transferable.")
|
||||
@@ -722,7 +703,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'max_per_order',
|
||||
'min_per_order',
|
||||
'checkin_attention',
|
||||
'checkin_text',
|
||||
'generate_tickets',
|
||||
'original_price',
|
||||
'require_bundling',
|
||||
@@ -771,7 +751,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'show_quota_left': ShowQuotaNullBooleanSelect(),
|
||||
'max_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
|
||||
'min_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
|
||||
'checkin_text': forms.TextInput(),
|
||||
}
|
||||
|
||||
|
||||
@@ -890,7 +869,6 @@ class ItemVariationForm(I18nModelForm):
|
||||
'require_membership_hidden',
|
||||
'require_membership_types',
|
||||
'checkin_attention',
|
||||
'checkin_text',
|
||||
'available_from',
|
||||
'available_until',
|
||||
'sales_channels',
|
||||
@@ -906,7 +884,6 @@ class ItemVariationForm(I18nModelForm):
|
||||
'require_membership_types': forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
'checkin_text': forms.TextInput(),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -265,13 +265,12 @@ class ExporterForm(forms.Form):
|
||||
class CommentForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['comment', 'checkin_attention', 'checkin_text', 'custom_followup_at']
|
||||
fields = ['comment', 'checkin_attention', 'custom_followup_at']
|
||||
widgets = {
|
||||
'comment': forms.Textarea(attrs={
|
||||
'rows': 3,
|
||||
'class': 'helper-width-100',
|
||||
}),
|
||||
'checkin_text': forms.TextInput(),
|
||||
'custom_followup_at': DatePickerWidget(),
|
||||
}
|
||||
|
||||
@@ -705,7 +704,6 @@ class OrderMailForm(forms.Form):
|
||||
)
|
||||
self.fields['attach_invoices'].queryset = order.invoices.all()
|
||||
self._set_field_placeholders('message', ['event', 'order'])
|
||||
self._set_field_placeholders('subject', ['event', 'order'])
|
||||
|
||||
|
||||
class OrderPositionMailForm(OrderMailForm):
|
||||
@@ -721,7 +719,6 @@ class OrderPositionMailForm(OrderMailForm):
|
||||
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
|
||||
)
|
||||
self._set_field_placeholders('message', ['event', 'order', 'position'])
|
||||
self._set_field_placeholders('subject', ['event', 'order'])
|
||||
|
||||
|
||||
class OrderRefundForm(forms.Form):
|
||||
|
||||
@@ -423,7 +423,6 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
'organizer_link_back',
|
||||
'organizer_logo_image_large',
|
||||
'organizer_logo_image_inherit',
|
||||
'favicon',
|
||||
'giftcard_length',
|
||||
'giftcard_expiry_years',
|
||||
'locales',
|
||||
@@ -465,6 +464,14 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
)
|
||||
favicon = ExtFileField(
|
||||
label=_('Favicon'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
||||
required=False,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
||||
'We recommend a size of at least 200x200px to accommodate most devices.')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
is_admin = kwargs.pop('is_admin', False)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from bootstrap3.text import text_value
|
||||
from django.forms import CheckboxInput, CheckboxSelectMultiple, RadioSelect
|
||||
from django.forms import CheckboxInput
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -30,7 +30,7 @@ from i18nfield.forms import I18nFormField
|
||||
from pretix.base.forms.renderers import FieldRenderer, InlineFieldRenderer
|
||||
|
||||
|
||||
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False):
|
||||
def render_label(content, label_for=None, label_class=None, label_title='', optional=False):
|
||||
"""
|
||||
Render a label with content
|
||||
"""
|
||||
@@ -41,8 +41,6 @@ def render_label(content, label_for=None, label_class=None, label_title='', labe
|
||||
attrs['class'] = label_class
|
||||
if label_title:
|
||||
attrs['title'] = label_title
|
||||
if label_id:
|
||||
attrs['id'] = label_id
|
||||
|
||||
if text_value(content) == ' ':
|
||||
# Empty label, e.g. checkbox
|
||||
@@ -63,7 +61,6 @@ class ControlFieldRenderer(FieldRenderer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['layout'] = 'horizontal'
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_group_widget = isinstance(self.widget, (CheckboxSelectMultiple, RadioSelect, )) or (self.is_multi_widget and len(self.widget.widgets) > 1)
|
||||
|
||||
def add_label(self, html):
|
||||
label = self.get_label()
|
||||
@@ -76,29 +73,14 @@ class ControlFieldRenderer(FieldRenderer):
|
||||
else:
|
||||
required = self.field.field.required
|
||||
|
||||
if self.is_group_widget:
|
||||
label_for = ""
|
||||
label_id = "legend-{}".format(self.field.html_name)
|
||||
else:
|
||||
label_for = self.field.id_for_label
|
||||
label_id = ""
|
||||
|
||||
html = render_label(
|
||||
label,
|
||||
label_for=label_for,
|
||||
label_for=self.field.id_for_label,
|
||||
label_class=self.get_label_class(),
|
||||
label_id=label_id,
|
||||
optional=not required and not isinstance(self.widget, CheckboxInput)
|
||||
) + html
|
||||
return html
|
||||
|
||||
def wrap_label_and_field(self, html):
|
||||
if self.is_group_widget:
|
||||
attrs = ' role="group" aria-labelledby="legend-{}"'.format(self.field.html_name)
|
||||
else:
|
||||
attrs = ''
|
||||
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
|
||||
|
||||
|
||||
class BulkEditMixin:
|
||||
|
||||
|
||||
@@ -463,8 +463,7 @@ class RRuleFormSetForm(RRuleForm):
|
||||
|
||||
RRuleFormSet = formset_factory(
|
||||
RRuleFormSetForm,
|
||||
min_num=1, validate_min=True,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
can_order=False, can_delete=True, extra=1
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -201,8 +201,6 @@ class VoucherForm(I18nModelForm):
|
||||
cnt = len(data['codes']) * data.get('max_usages', 0)
|
||||
else:
|
||||
cnt = data.get('max_usages', 0)
|
||||
if self.instance and self.instance.pk:
|
||||
cnt -= self.instance.redeemed # these do not need quota any more
|
||||
|
||||
Voucher.clean_item_properties(
|
||||
data, self.instance.event,
|
||||
|
||||
@@ -407,7 +407,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.custom_followup_at': _('The order\'s follow-up date has been updated.'),
|
||||
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
|
||||
'toggled.'),
|
||||
'pretix.event.order.checkin_text': _('The order\'s check-in text has been changed.'),
|
||||
'pretix.event.order.pretix.event.order.valid_if_pending': _('The order\'s flag to be considered valid even if '
|
||||
'unpaid has been toggled.'),
|
||||
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
|
||||
|
||||
@@ -37,7 +37,7 @@ from urllib.parse import quote, urljoin, urlparse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, resolve_url
|
||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import get_script_prefix, resolve, reverse
|
||||
from django.utils.encoding import force_str
|
||||
@@ -46,7 +46,6 @@ from django_scopes import scope
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.base.models.auth import SuperuserPermissionSet, User
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
@@ -119,7 +118,7 @@ class PermissionMiddleware:
|
||||
|
||||
if hasattr(request, 'organizer'):
|
||||
# If the user is on a organizer's subdomain, he should be redirected to pretix
|
||||
return redirect_to_url(urljoin(settings.SITE_URL, request.get_full_path()))
|
||||
return redirect(urljoin(settings.SITE_URL, request.get_full_path()))
|
||||
if url_name in self.EXCEPTIONS:
|
||||
return self.get_response(request)
|
||||
if not request.user.is_authenticated:
|
||||
@@ -133,14 +132,14 @@ class PermissionMiddleware:
|
||||
return self._login_redirect(request)
|
||||
except SessionReauthRequired:
|
||||
if url_name not in ('user.reauth', 'auth.logout'):
|
||||
return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
|
||||
return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
|
||||
return redirect(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \
|
||||
and url_name not in self.EXCEPTIONS_2FA and not request.user.needs_password_change:
|
||||
return redirect_to_url(reverse('control:user.settings.2fa'))
|
||||
and url_name not in self.EXCEPTIONS_2FA:
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-':
|
||||
@@ -153,7 +152,7 @@ class PermissionMiddleware:
|
||||
k = dict(url.kwargs)
|
||||
k['organizer'] = ev.organizer.slug
|
||||
k['event'] = ev.slug
|
||||
return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args))
|
||||
return redirect(reverse(url.view_name, kwargs=k, args=url.args))
|
||||
|
||||
with scope(organizer=None):
|
||||
request.event = Event.objects.filter(
|
||||
@@ -179,7 +178,7 @@ class PermissionMiddleware:
|
||||
"have no permission to administrate it."))
|
||||
k = dict(url.kwargs)
|
||||
k['organizer'] = org.slug
|
||||
return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args))
|
||||
return redirect(reverse(url.view_name, kwargs=k, args=url.args))
|
||||
|
||||
request.organizer = Organizer.objects.filter(
|
||||
slug=url.kwargs['organizer'],
|
||||
|
||||
@@ -35,11 +35,10 @@
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
|
||||
|
||||
def current_url(request):
|
||||
if request.GET:
|
||||
@@ -136,7 +135,7 @@ def administrator_permission_required():
|
||||
raise PermissionDenied()
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
if request.user.is_staff:
|
||||
return redirect_to_url(reverse('control:user.sudo') + '?next=' + quote(current_url(request)))
|
||||
return redirect(reverse('control:user.sudo') + '?next=' + quote(current_url(request)))
|
||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||
return function(request, *args, **kw)
|
||||
return wrapper
|
||||
|
||||
@@ -85,31 +85,11 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if result.position %}
|
||||
{% if result.require_attention %}
|
||||
{% if result.position.require_attention %}
|
||||
<p>
|
||||
<strong>
|
||||
<span class="fa fa-info-circle text-info fa-fw"></span>
|
||||
{% trans "Special attention required" %}
|
||||
</strong>
|
||||
<span class="fa fa-info-circle fa-fw"></span> {% trans "Special attention required" %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% for t in result.checkin_texts %}
|
||||
<p>
|
||||
<span class="fa fa-info-circle text-muted fa-fw"></span>
|
||||
{{ t }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% if result.position_object %}
|
||||
{% for a in result.position_object.answers.all %}
|
||||
{% if a.question.show_during_checkin %}
|
||||
<p>
|
||||
<span class="fa fa-question-circle text-muted fa-fw"></span>
|
||||
<strong>{{ a.question.question }}</strong>
|
||||
{{ a }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<p>
|
||||
<span class="fa fa-ticket fa-fw"></span>
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=result.position.order %}">
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
{% bootstrap_field form.cancel_allow_user_unpaid_keep layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_unpaid_keep_percentage layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_unpaid_keep_fees layout="control" %}
|
||||
{% bootstrap_field form.cancel_terms_unpaid layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Paid orders" %}</legend>
|
||||
@@ -33,7 +32,6 @@
|
||||
{% bootstrap_field form.cancel_allow_user_paid_adjust_fees_step layout="control" %}
|
||||
</div>
|
||||
{% bootstrap_field form.cancel_allow_user_paid_refund_as_giftcard layout="control" %}
|
||||
{% bootstrap_field form.cancel_terms_paid layout="control" %}
|
||||
{% if not gets_notification %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load escapejson %}
|
||||
{% load eventsignal %}
|
||||
{% block title %}{{ request.event.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div id="header-area" class="col-lg-8">
|
||||
<h1>
|
||||
{{ request.event.name }}
|
||||
<small>
|
||||
{% if request.event.has_subevents %}
|
||||
{% trans "Event series" %}
|
||||
{% else %}
|
||||
{{ request.event.get_date_range_display }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</h1>
|
||||
|
||||
<div class="helper-space-below">
|
||||
{% trans "Shop URL:" %}
|
||||
<span id="shop_url" class="text-muted">{% abseventurl request.event "presale:event.index" %}</span>
|
||||
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
|
||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
||||
</button>
|
||||
<div class="btn-group helper-display-inline-block">
|
||||
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-qrcode" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=0 %}
|
||||
</div>
|
||||
<br>
|
||||
<span id="shop_state" class="shop_state">
|
||||
{{ shop_state.content|safe }}
|
||||
<a href="{{ shop_state.url }}">
|
||||
<button type="button" class="btn btn-default btn-xs btn-wrench js-only" href="{{ shop_state.url }}">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||
<span class="sr-only">{% trans "Change shop state" %}</span>
|
||||
</button>
|
||||
</a>
|
||||
</span>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Comments ----------------------------------------------------------------------------------------------------->
|
||||
<div class="event-comment col-lg-4">
|
||||
{% trans "Internal comment" %}
|
||||
<form class="form" method="post"
|
||||
action="{% url "control:event.comment" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
{% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
|
||||
</div>
|
||||
{% if not comment_form.readonly %}
|
||||
<p class="text-right flip">
|
||||
<button class="btn btn-default">
|
||||
{% trans "Update comment" %}
|
||||
</button>
|
||||
</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Comments ----------------------------------------------------------------------------------------------------->
|
||||
|
||||
<!-- Warnings ----------------------------------------------------------------------------------------------------->
|
||||
<div class="row-form-errors">
|
||||
{% if has_overpaid_orders %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
|
||||
You should review the cases and consider refunding the overpaid amount to the user.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
|
||||
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_refunds %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending refunds</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_cancellation_requests %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>requested cancellations</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=rc"
|
||||
class="btn btn-primary">{% trans "Show orders requesting cancellation" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_approvals %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>pending approvals</strong> that you should take care of.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
|
||||
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if has_pending_orders_with_full_payment %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This event contains <strong>fully paid orders</strong> that are not marked as paid, probably
|
||||
because no quota was left at the time their payment arrived. You should review the cases and consider
|
||||
either refunding the customer or creating more space.
|
||||
{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pendingpaid"
|
||||
class="btn btn-primary">{% trans "Show affected orders" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Warnings ----------------------------------------------------------------------------------------------------->
|
||||
|
||||
{% eventsignal request.event "pretix.control.signals.event_dashboard_top" request=request %}
|
||||
<!-- Timeline ----------------------------------------------------------------------------------------------------->
|
||||
{% if request.event.has_subevents %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not request.event.has_subevents or subevent %}
|
||||
{% include "pretixcontrol/event/fragment_timeline.html" %}
|
||||
{% endif %}
|
||||
<!-- Timeline ----------------------------------------------------------------------------------------------------->
|
||||
<!-- Big numbers ---------------------------------------------------------------------------------------------->
|
||||
<div class="dashboard">
|
||||
<div class="widget-container widget-small event-dashboard">
|
||||
<a href="{{ attendees_paid_ordered.url }}" class="widget">
|
||||
{{ attendees_paid_ordered.content|safe }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="widget-container widget-small event-dashboard">
|
||||
<a href="{{ total_revenue.url }}" class="widget">
|
||||
{{ total_revenue.content|safe }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Big numbers -------------------------------------------------------------------------------------------------->
|
||||
<!-- Diagram ------------------------------------------------------------------------------------------------------>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Revenue over time" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="rev_chart" class="chart"></div>
|
||||
{% if request.GET.subevent %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed context "subevent" %}
|
||||
If you select a single date, payment method fees will not be listed here as it might not be clear which
|
||||
date they belong to.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="help-block">
|
||||
<small>
|
||||
{% blocktrans trimmed %}
|
||||
Only fully paid orders are counted.
|
||||
Orders paid in multiple payments are shown with the date of their last payment.
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Diagram ------------------------------------------------------------------------------------------------------>
|
||||
<!-- Check-Ins ---------------------------------------------------------------------------------------------------->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Check-In" %}</h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "Name" %}
|
||||
</th>
|
||||
<th>{% trans "Checked in" %}</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>
|
||||
{% trans "Date" context "subevent" %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th class="iconcol">{% trans "Automated check-in" %}</th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cl in checkinlists %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a
|
||||
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="quotabox availability">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ cl.checkin_count|default_if_none:"0" }} /
|
||||
{{ cl.position_count|default_if_none:"0" }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
{% if cl.subevent %}
|
||||
<td>
|
||||
{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }}
|
||||
{{ cl.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<em>{% trans "All" %}</em>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td>
|
||||
{% for channel in cl.auto_checkin_sales_channels %}
|
||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if cl.all_products %}
|
||||
<em>{% trans "All" %}</em>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for item in cl.limit_products.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Check-Ins ---------------------------------------------------------------------------------------------------->
|
||||
<!-- Quotas ------------------------------------------------------------------------------------------------------->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Quotas" %}</h3>
|
||||
</div>
|
||||
<div class="table-responsive table-quotas">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "Name" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Begin" %}
|
||||
<a href="? url_replace request 'filter-ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="? url_replace request 'filter-ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Paid tickets per quota" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Capacity left" %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if request.event.has_subevents and not subevent %}
|
||||
{% for s in subevents %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}">
|
||||
{{ s.name }}</a></strong><br>
|
||||
<small class="text-muted">
|
||||
#{{ s.pk }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ s.get_date_from_display }}<br>
|
||||
<span class="text-muted">
|
||||
{{ s.date_from|date:"l" }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% for q in s.first_quotas|slice:":3" %}
|
||||
{% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %}
|
||||
{% endfor %}
|
||||
{% if s.first_quotas|length > 3 %}
|
||||
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}"
|
||||
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
|
||||
data-placement="top">
|
||||
···
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% for q in s.first_quotas %}
|
||||
{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if subevent %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=subevent.id %}?returnto={{ request.GET.urlencode|urlencode }}">
|
||||
{{ subevent.name }}</a></strong><br>
|
||||
<small class="text-muted">
|
||||
#{{ subevent.pk }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ subevent.get_date_from_display }}<br>
|
||||
<span class="text-muted">
|
||||
{{ subevent.date_from|date:"l" }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% for q in subevent.first_quotas|slice:":3" %}
|
||||
{% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %}
|
||||
{% endfor %}
|
||||
{% if subevent.first_quotas|length > 3 %}
|
||||
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ subevent.id }}"
|
||||
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
|
||||
data-placement="top">
|
||||
···
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% for q in subevent.first_quotas %}
|
||||
{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Quotas ------------------------------------------------------------------------------------------------------->
|
||||
<!-- Logs --------------------------------------------------------------------------------------------------------->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Event logs" %}
|
||||
</h3>
|
||||
</div>
|
||||
<ul class="list-group" id="logs_target">
|
||||
<div class="logs-lazy-loading">
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
</div>
|
||||
</ul>
|
||||
<div class="panel-footer">
|
||||
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||
{% trans "Show more logs" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Logs --------------------------------------------------------------------------------------------------------->
|
||||
<script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
|
||||
<script type="application/text" id="currency">{{ request.event.currency }}</script>
|
||||
{% endblock %}
|
||||
@@ -108,7 +108,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field form.checkin_attention layout="control" %}
|
||||
{% bootstrap_field form.checkin_text layout="control" %}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
@@ -207,7 +206,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field formset.empty_form.checkin_attention layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.checkin_text layout="control" %}
|
||||
</div>
|
||||
</details>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -202,14 +202,12 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in & Validity" %}</legend>
|
||||
{% bootstrap_field form.checkin_attention layout="control" %}
|
||||
{% bootstrap_field form.checkin_text layout="control" %}
|
||||
{% bootstrap_field form.validity_mode layout="control" %}
|
||||
<div data-display-dependency="#{{ form.validity_mode.id_for_label }}" data-display-dependency-value="fixed">
|
||||
{% bootstrap_field form.validity_fixed_from layout="control" %}
|
||||
{% bootstrap_field form.validity_fixed_until layout="control" %}
|
||||
</div>
|
||||
<div data-display-dependency="#{{ form.validity_mode.id_for_label }}" data-display-dependency-value="dynamic">
|
||||
{% bootstrap_field form.validity_dynamic_start_choice layout="control" %}
|
||||
<div class="form-group metadata-group">
|
||||
<label class="col-md-3 control-label">{% trans "Duration" %}</label>
|
||||
<div class="col-md-9">
|
||||
@@ -232,7 +230,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-display-dependency="#{{ form.validity_dynamic_start_choice.id_for_label }}" data-display-dependency-value="True">
|
||||
{% bootstrap_field form.validity_dynamic_start_choice layout="control" %}
|
||||
<div data-display-dependency="#{{ form.validity_dynamic_start_choice.id_for_label }}">
|
||||
{% trans "days" as t_days %}
|
||||
{% bootstrap_field form.validity_dynamic_start_choice_day_limit addon_after=t_days layout="control" %}
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
|
||||
|
||||
@@ -997,7 +997,6 @@
|
||||
{% bootstrap_field comment_form.comment show_help=True show_label=False %}
|
||||
{% bootstrap_field comment_form.custom_followup_at %}
|
||||
{% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %}
|
||||
{% bootstrap_field comment_form.checkin_text show_help=True show_label=False %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
<button class="btn btn-default">
|
||||
{% trans "Update comment" %}
|
||||
|
||||
@@ -25,17 +25,6 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Export options" %}</legend>
|
||||
{% bootstrap_form exporter.form layout='control' %}
|
||||
|
||||
{% if "_format" in exporter.form.fields and exporter.multisheet_warning %}
|
||||
<div data-display-dependency="#id_{{ exporter.form.prefix }}-_format" data-display-dependency-value="xlsx">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Your generated Excel file will have <strong>multiple sheets</strong>. Some data you are
|
||||
looking for might not be on the first sheet.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% if schedule_form %}
|
||||
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{# Changes should be replicated in pretixpresale/event/fragment_order_status.html and in pretix/base/models/orders.py #}
|
||||
{% if order.status == "n" %}
|
||||
{% if order.require_approval %}
|
||||
<span class="label label-warning {{ class }}">
|
||||
|
||||
@@ -299,13 +299,6 @@
|
||||
{% trans "Deny" %}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" class="btn"
|
||||
formaction="{% url "control:event.orders.bulk.refund_overpaid" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-money fa-fw text-danger"></i>
|
||||
{% trans "Refund overpaid amount" %}
|
||||
</button>
|
||||
</li>
|
||||
{% if not request.event.settings.payment_term_expire_automatically %}
|
||||
<li>
|
||||
<button type="submit" class="btn"
|
||||
|
||||
@@ -26,17 +26,6 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Export options" %}</legend>
|
||||
{% bootstrap_form exporter.form layout='control' %}
|
||||
|
||||
{% if "_format" in exporter.form.fields and exporter.multisheet_warning %}
|
||||
<div data-display-dependency="#id_{{ exporter.form.prefix }}-_format" data-display-dependency-value="xlsx">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Your generated Excel file will have <strong>multiple sheets</strong>. Some data you are
|
||||
looking for might not be on the first sheet.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% if schedule_form %}
|
||||
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
|
||||
|
||||
@@ -10,93 +10,78 @@
|
||||
{% endif %}
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.default layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Usage" %}</legend>
|
||||
{% bootstrap_field form.filter_allowed layout="control" %}
|
||||
{% bootstrap_field form.filter_public layout="control" %}
|
||||
{% bootstrap_field form.public_label layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Validation" %}</legend>
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
{% bootstrap_field form.protected layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Allowed values" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<p class="help-block">{% trans "If you keep this empty, all input will be allowed." %}</p>
|
||||
<div class="formset tax-rules-formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="row tax-rule-line" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field formset.empty_form.key layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field formset.empty_form.label layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
{% bootstrap_form form layout="control" %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Allowed values" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<p class="help-block">{% trans "If you keep this empty, all input will be allowed." %}</p>
|
||||
<div class="formset tax-rules-formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="row tax-rule-line" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<div data-formset-body class="tax-rule-lines">
|
||||
{% for form in formset %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="row tax-rule-line" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field form.key layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field form.label layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field formset.empty_form.key layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field formset.empty_form.label layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row tax-rule-line" data-formset-form>
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<div data-formset-body class="tax-rule-lines">
|
||||
{% for form in formset %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="row tax-rule-line" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field form.key layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||
{% bootstrap_field form.label layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row tax-rule-line" data-formset-form>
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new value" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% else %}
|
||||
<h1>{% trans "Date" context "subevent" %}</h1>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-datai">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% for f in itemvar_forms %}
|
||||
|
||||
@@ -38,8 +38,8 @@ from django.views.generic.base import RedirectView
|
||||
|
||||
from pretix.control.views import (
|
||||
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
|
||||
main, new_dashboard, oauth, orderimport, orders, organizer, pdf, search,
|
||||
shredder, subevents, typeahead, user, users, vouchers, waitinglist,
|
||||
main, oauth, orderimport, orders, organizer, pdf, search, shredder,
|
||||
subevents, typeahead, user, users, vouchers, waitinglist,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -239,7 +239,7 @@ urlpatterns = [
|
||||
re_path(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'),
|
||||
re_path(r'^search/payments/$', search.PaymentSearch.as_view(), name='search.payments'),
|
||||
re_path(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
|
||||
re_path(r'^$', new_dashboard.IndexView.as_view(), name='event.index'),
|
||||
re_path(r'^$', dashboards.event_index, name='event.index'),
|
||||
re_path(r'^qrcode.(?P<filetype>(png|jpeg|gif|svg))$', event.EventQRCode.as_view(), name='event.qrcode'),
|
||||
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
|
||||
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
|
||||
@@ -423,7 +423,6 @@ urlpatterns = [
|
||||
re_path(r'^orders/bulk/approve$', orders.OrderApproveBulkActionView.as_view(), name='event.orders.bulk.approve'),
|
||||
re_path(r'^orders/bulk/deny$', orders.OrderDenyBulkActionView.as_view(), name='event.orders.bulk.deny'),
|
||||
re_path(r'^orders/bulk/expire$', orders.OrderExpireBulkActionView.as_view(), name='event.orders.bulk.expire'),
|
||||
re_path(r'^orders/bulk/refund_overpaid$', orders.OrderOverpaidRefundBulkActionView.as_view(), name='event.orders.bulk.refund_overpaid'),
|
||||
re_path(r'^orders/bulk/delete$', orders.OrderDeleteBulkActionView.as_view(), name='event.orders.bulk.delete'),
|
||||
re_path(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'),
|
||||
re_path(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
||||
|
||||
@@ -87,8 +87,8 @@ def process_login(request, user, keep_logged_in):
|
||||
auth_login(request, user)
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect_to_url(next_url)
|
||||
return redirect('control:index')
|
||||
return redirect(next_url)
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
|
||||
def login(request):
|
||||
@@ -149,10 +149,7 @@ def register(request):
|
||||
raise PermissionDenied('Registration is disabled')
|
||||
ctx = {}
|
||||
if request.user.is_authenticated:
|
||||
next_url = request.GET.get("next") or reverse("control:index")
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect_to_url(next_url)
|
||||
return redirect("control:index")
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
if request.method == 'POST':
|
||||
form = RegistrationForm(data=request.POST)
|
||||
if form.is_valid():
|
||||
@@ -259,10 +256,7 @@ class Forgot(TemplateView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
next_url = request.GET.get("next") or reverse("control:index")
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect_to_url(next_url)
|
||||
return redirect("control:index")
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -328,17 +322,13 @@ class Recover(TemplateView):
|
||||
}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# settings.PRETIX_PASSWORD_RESET is not checked here to allow admin-sent recovery links
|
||||
if 'native' not in get_auth_backends():
|
||||
if not settings.PRETIX_PASSWORD_RESET or 'native' not in get_auth_backends():
|
||||
raise PermissionDenied('Registration is disabled')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
next_url = request.GET.get("next") or reverse("control:index")
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect_to_url(next_url)
|
||||
return redirect("control:index")
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
try:
|
||||
user = User.objects.get(id=self.request.GET.get('id'), is_active=True, auth_backend='native')
|
||||
except User.DoesNotExist:
|
||||
@@ -462,7 +452,7 @@ class Login2FAView(TemplateView):
|
||||
del request.session['pretix_auth_2fa_time']
|
||||
if "next" in request.GET and url_has_allowed_host_and_scheme(request.GET.get("next"), allowed_hosts=None):
|
||||
return redirect_to_url(request.GET.get("next"))
|
||||
return redirect('control:index')
|
||||
return redirect(reverse('control:index'))
|
||||
else:
|
||||
messages.error(request, _('Invalid code, please try again.'))
|
||||
return redirect('control:auth.login.2fa')
|
||||
|
||||
@@ -551,12 +551,9 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
||||
gate=form.cleaned_data.get("gate"),
|
||||
).data
|
||||
|
||||
if self.result.get("position"):
|
||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||
self.result["position_object"] = op
|
||||
|
||||
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
|
||||
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"], form.cleaned_data.get("gate"))
|
||||
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
|
||||
form.cleaned_data["datetime"])
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import hashlib
|
||||
import logging
|
||||
from urllib.parse import quote
|
||||
|
||||
@@ -37,8 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
class GeoCodeView(LoginRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
q = self.request.GET.get('q')
|
||||
cache_key = 'geocode:{}'.format(hashlib.sha256(q.encode()).hexdigest())
|
||||
cd = cache.get(cache_key)
|
||||
cd = cache.get('geocode:{}'.format(q))
|
||||
if cd:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
@@ -63,7 +61,7 @@ class GeoCodeView(LoginRequiredMixin, View):
|
||||
'results': []
|
||||
}, status=200)
|
||||
|
||||
cache.set(cache_key, res, timeout=3600 * 6)
|
||||
cache.set('geocode:{}'.format(q), res, timeout=3600 * 6)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'results': res
|
||||
|
||||
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
def get_spf_record(hostname):
|
||||
try:
|
||||
r = dns.resolver.Resolver()
|
||||
for resp in r.resolve(hostname, 'TXT'):
|
||||
for resp in r.query(hostname, 'TXT'):
|
||||
data = b''.join(resp.strings).decode()
|
||||
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
|
||||
return data
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
import datetime
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
import dateutil
|
||||
from django.db.models import (
|
||||
DateTimeField, F, Max, OuterRef, Prefetch, Q, Subquery, Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
from django.utils import formats, timezone
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import SubEvent
|
||||
from pretix.base.models.orders import (
|
||||
CancellationRequest, Order, OrderPayment, OrderPosition, OrderRefund,
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.timeline import timeline_for_event
|
||||
from pretix.control.forms.event import CommentForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import ChartContainingView
|
||||
|
||||
NUM_WIDGET = str('<div class="numwidget">'
|
||||
'<span class="num">{num}</span>'
|
||||
'<span class="text"><span class="label-primary">{text}</span></span>'
|
||||
'<span class="text-add">{text_add}</span></div>')
|
||||
|
||||
|
||||
class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView):
|
||||
template_name = 'pretixcontrol/event/new_index.html'
|
||||
permission = 'can_view_orders'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
subevent = None
|
||||
if self.request.GET.get("subevent", "") != "" and self.request.event.has_subevents:
|
||||
i = self.request.GET.get("subevent", "")
|
||||
try:
|
||||
subevent = self.request.event.subevents.get(pk=i)
|
||||
except SubEvent.DoesNotExist:
|
||||
pass
|
||||
|
||||
can_view_orders = self.request.user.has_event_permission(self.request.organizer, self.request.event,
|
||||
'can_view_orders',
|
||||
request=self.request)
|
||||
# can_change_event_settings = self.request.user.has_event_permission(self.request.organizer, self.request.event,
|
||||
# 'can_change_event_settings',
|
||||
# request=self.request)
|
||||
|
||||
ctx = {
|
||||
'subevent': subevent,
|
||||
'comment_form': CommentForm(initial={'comment': self.request.event.comment},
|
||||
readonly=True), # not can_change_event_settings),
|
||||
}
|
||||
|
||||
if subevent:
|
||||
opqs = OrderPosition.objects.filter(subevent=subevent)
|
||||
else:
|
||||
opqs = OrderPosition.objects
|
||||
|
||||
ctx['shop_state'] = {
|
||||
'display_size': 'small',
|
||||
'priority': 1000,
|
||||
'content': '<span class="{cls}">{t1} {state} <span class="fa {icon}"></span></span>'.format(
|
||||
t1=_('Your ticket shop is'),
|
||||
state=_('live') if self.request.event.live and not self.request.event.testmode else (
|
||||
_('live and in test mode') if self.request.event.live else (
|
||||
_('not yet public') if not self.request.event.testmode else (
|
||||
_('in private test mode')
|
||||
)
|
||||
)
|
||||
),
|
||||
icon='fa-check-circle' if self.request.event.live and not self.request.event.testmode else (
|
||||
'fa-warning' if self.request.event.live else (
|
||||
'fa-times-circle' if not self.request.event.testmode else (
|
||||
'fa-lock'
|
||||
)
|
||||
)
|
||||
),
|
||||
cls='live' if self.request.event.live else 'off'
|
||||
),
|
||||
'url': reverse('control:event.live', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
})
|
||||
}
|
||||
|
||||
qs = self.request.event.checkin_lists.filter(subevent=subevent)
|
||||
sales_channels = get_all_sales_channels()
|
||||
for cl in qs:
|
||||
if cl.subevent:
|
||||
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
|
||||
cl.auto_checkin_sales_channels = [sales_channels[channel] for channel in cl.auto_checkin_sales_channels]
|
||||
ctx['checkinlists'] = qs
|
||||
|
||||
qs = self.request.event.subevents
|
||||
if list:
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch('quotas',
|
||||
queryset=self.request.event.quotas.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
|
||||
to_attr='first_quotas')
|
||||
)
|
||||
ctx['subevents'] = qs
|
||||
quotas = []
|
||||
for s in ctx['subevents']:
|
||||
s.first_quotas = s.first_quotas[:4]
|
||||
quotas += list(s.first_quotas)
|
||||
|
||||
qa = QuotaAvailability(early_out=False)
|
||||
for q in quotas:
|
||||
qa.queue(q)
|
||||
qa.compute()
|
||||
|
||||
for q in quotas:
|
||||
q.cached_avail = qa.results[q]
|
||||
q.cached_availability_paid_orders = qa.count_paid_orders.get(q, 0)
|
||||
if q.size is not None:
|
||||
q.percent_paid = min(
|
||||
100,
|
||||
round(q.cached_availability_paid_orders / q.size * 100) if q.size > 0 else 100
|
||||
)
|
||||
|
||||
tickc = opqs.filter(
|
||||
order__event=self.request.event, item__admission=True,
|
||||
order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
|
||||
).count()
|
||||
paidc = opqs.filter(
|
||||
order__event=self.request.event, item__admission=True,
|
||||
order__status=Order.STATUS_PAID,
|
||||
).count()
|
||||
ctx['attendees_paid_ordered'] = {
|
||||
'content': NUM_WIDGET.format(
|
||||
num=f'{tickc}',
|
||||
text=_('<span class="fa fa-user icon"></span> Attendees'),
|
||||
text_add=f'{paidc} paid, {tickc - paidc} pending'),
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.orders.overview', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||
}
|
||||
|
||||
if subevent:
|
||||
rev = opqs.filter(
|
||||
order__event=self.request.event, order__status=Order.STATUS_PAID
|
||||
).aggregate(
|
||||
sum=Sum('price')
|
||||
)['sum'] or Decimal('0.00')
|
||||
else:
|
||||
rev = Order.objects.filter(
|
||||
event=self.request.event,
|
||||
status=Order.STATUS_PAID
|
||||
).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
|
||||
|
||||
ctx['total_revenue'] = {
|
||||
'content': NUM_WIDGET.format(
|
||||
num='<span class="icon">{currency}</span> {amount}'.format(
|
||||
currency=self.request.event.currency,
|
||||
amount=formats.localize(round_decimal(rev, self.request.event.currency))
|
||||
),
|
||||
text=_('Total revenue'),
|
||||
text_add=''
|
||||
),
|
||||
'priority': 100,
|
||||
'url': reverse('control:event.orders.overview', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||
}
|
||||
|
||||
cache = self.request.event.cache
|
||||
ckey = str(subevent.pk) if subevent else 'all'
|
||||
tz = timezone.get_current_timezone()
|
||||
op_date = OrderPayment.objects.filter(
|
||||
order=OuterRef('order'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
payment_date__isnull=False
|
||||
).values('order').annotate(
|
||||
m=Max('payment_date')
|
||||
).values(
|
||||
'm'
|
||||
).order_by()
|
||||
p_date = OrderPayment.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
payment_date__isnull=False
|
||||
).values('order').annotate(
|
||||
m=Max('payment_date')
|
||||
).values(
|
||||
'm'
|
||||
).order_by()
|
||||
ctx['rev_data'] = cache.get('statistics_rev_data' + ckey)
|
||||
if not ctx['rev_data']:
|
||||
rev_by_day = {}
|
||||
if subevent:
|
||||
for o in OrderPosition.objects.annotate(
|
||||
payment_date=Subquery(op_date, output_field=DateTimeField())
|
||||
).filter(order__event=self.request.event,
|
||||
subevent=subevent,
|
||||
order__status=Order.STATUS_PAID,
|
||||
payment_date__isnull=False).values('payment_date', 'price'):
|
||||
day = o['payment_date'].astimezone(tz).date()
|
||||
rev_by_day[day] = rev_by_day.get(day, 0) + o['price']
|
||||
else:
|
||||
for o in Order.objects.annotate(
|
||||
payment_date=Subquery(p_date, output_field=DateTimeField())
|
||||
).filter(event=self.request.event,
|
||||
status=Order.STATUS_PAID,
|
||||
payment_date__isnull=False).values('payment_date', 'total'):
|
||||
day = o['payment_date'].astimezone(tz).date()
|
||||
rev_by_day[day] = rev_by_day.get(day, 0) + o['total']
|
||||
|
||||
data = []
|
||||
total = 0
|
||||
for d in dateutil.rrule.rrule(
|
||||
dateutil.rrule.DAILY,
|
||||
dtstart=min(rev_by_day.keys() if rev_by_day else [datetime.date.today()]),
|
||||
until=max(rev_by_day.keys() if rev_by_day else [datetime.date.today()])):
|
||||
d = d.date()
|
||||
rev = float(rev_by_day.get(d, 0))
|
||||
if True: # rev != 0:
|
||||
total += rev
|
||||
data.append({
|
||||
'date': d.strftime('%Y-%m-%d'),
|
||||
'revenue': round(total, 2),
|
||||
})
|
||||
ctx['rev_data'] = json.dumps(data)
|
||||
cache.set('statistics_rev_data' + ckey, ctx['rev_data'])
|
||||
|
||||
ctx['has_overpaid_orders'] = can_view_orders and Order.annotate_overpayments(self.request.event.orders).filter(
|
||||
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
|
||||
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
|
||||
).exists()
|
||||
ctx['has_pending_orders_with_full_payment'] = can_view_orders and Order.annotate_overpayments(
|
||||
self.request.event.orders).filter(
|
||||
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0) & Q(
|
||||
require_approval=False)
|
||||
).exists()
|
||||
ctx['has_pending_refunds'] = can_view_orders and OrderRefund.objects.filter(
|
||||
order__event=self.request.event,
|
||||
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_EXTERNAL)
|
||||
).exists()
|
||||
ctx['has_pending_approvals'] = can_view_orders and self.request.event.orders.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True
|
||||
).exists()
|
||||
ctx['has_cancellation_requests'] = can_view_orders and CancellationRequest.objects.filter(
|
||||
order__event=self.request.event
|
||||
).exists()
|
||||
|
||||
ctx['timeline'] = [
|
||||
{
|
||||
'date': t.datetime.astimezone(self.request.event.timezone).date(),
|
||||
'entry': t,
|
||||
'time': t.datetime.astimezone(self.request.event.timezone)
|
||||
}
|
||||
for t in timeline_for_event(self.request.event, subevent)
|
||||
]
|
||||
ctx['today'] = now().astimezone(self.request.event.timezone).date()
|
||||
ctx['nearly_now'] = now().astimezone(self.request.event.timezone) - datetime.timedelta(seconds=20)
|
||||
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
|
||||
return ctx
|
||||
@@ -50,7 +50,6 @@ from pretix.base.services.orderimport import import_orders, parse_csv
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.orderimport import ProcessForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ENCODINGS = (
|
||||
@@ -70,19 +69,19 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'file' not in request.FILES:
|
||||
return redirect_to_url(reverse('control:event.orders.import', kwargs={
|
||||
return redirect(reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}))
|
||||
if not request.FILES['file'].name.lower().endswith('.csv'):
|
||||
messages.error(request, _('Please only upload CSV files.'))
|
||||
return redirect_to_url(reverse('control:event.orders.import', kwargs={
|
||||
return redirect(reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}))
|
||||
if request.FILES['file'].size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
|
||||
messages.error(request, _('Please do not upload files larger than 10 MB.'))
|
||||
return redirect_to_url(reverse('control:event.orders.import', kwargs={
|
||||
return redirect(reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}))
|
||||
|
||||
@@ -74,7 +74,6 @@ from i18nfield.strings import LazyI18nString
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.exporter import MultiSheetListExporter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedFile, CachedTicket, Checkin, Invoice,
|
||||
@@ -213,14 +212,10 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
|
||||
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
||||
qs = self.allowed_for(self.allowed_for(self.get_queryset()))
|
||||
total = qs.count()
|
||||
orders_with_successful_action = 0
|
||||
for i, o in enumerate(qs):
|
||||
res = self.execute_single(o, form)
|
||||
if res:
|
||||
orders_with_successful_action += 1
|
||||
self.execute_single(o, form)
|
||||
if i % 100 == 0:
|
||||
self.async_set_progress(i / total * 100)
|
||||
return orders_with_successful_action, total
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_success_url(None)
|
||||
@@ -236,9 +231,6 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
})
|
||||
|
||||
def get_success_message(self, value):
|
||||
return _("Successfully executed the action \"{label}\" on {success} of {total} orders.").format(success=value[0], label=self.label, total=value[1])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['total'] = self.get_queryset().count()
|
||||
@@ -278,7 +270,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
|
||||
|
||||
@transaction.atomic()
|
||||
def async_form_valid(self, task, form):
|
||||
return self.execute_bulk(self.allowed_for(self.get_queryset()), form)
|
||||
self.execute_bulk(self.allowed_for(self.get_queryset()), form)
|
||||
|
||||
|
||||
class OrderApproveBulkActionView(BaseOrderBulkActionView):
|
||||
@@ -292,7 +284,6 @@ class OrderApproveBulkActionView(BaseOrderBulkActionView):
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
approve_order(instance, user=self.request.user)
|
||||
return True
|
||||
|
||||
|
||||
class OrderDenyBulkActionView(BaseOrderBulkActionView):
|
||||
@@ -309,7 +300,6 @@ class OrderDenyBulkActionView(BaseOrderBulkActionView):
|
||||
deny_order(instance, user=self.request.user,
|
||||
comment=form.cleaned_data.get('comment') or None,
|
||||
send_mail=form.cleaned_data['send_email'])
|
||||
return True
|
||||
|
||||
|
||||
class OrderExpireBulkActionView(BaseOrderBulkActionView):
|
||||
@@ -324,33 +314,6 @@ class OrderExpireBulkActionView(BaseOrderBulkActionView):
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
mark_order_expired(instance, user=self.request.user)
|
||||
return True
|
||||
|
||||
|
||||
class OrderOverpaidRefundBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Refund overpaid amount")
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return Order.annotate_overpayments(queryset).filter(is_overpaid=True)
|
||||
|
||||
def execute_single(self, instance: Order, form: forms.Form):
|
||||
if instance.pending_sum < 0:
|
||||
try:
|
||||
proposals = instance.propose_auto_refunds(instance.pending_sum * -1)
|
||||
for payment, amount in proposals.items():
|
||||
refund = OrderRefund.objects.create(
|
||||
order=instance,
|
||||
payment=payment,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=amount,
|
||||
comment=_("Refund for overpayment"),
|
||||
provider=payment.provider
|
||||
)
|
||||
payment.payment_provider.execute_refund(refund)
|
||||
return True
|
||||
except (ValueError, PaymentException):
|
||||
return False
|
||||
|
||||
|
||||
class OrderDeleteBulkActionView(BaseOrderBulkActionView):
|
||||
@@ -513,8 +476,7 @@ class OrderDetail(OrderView):
|
||||
ctx['comment_form'] = CommentForm(initial={
|
||||
'comment': self.order.comment,
|
||||
'custom_followup_at': self.order.custom_followup_at,
|
||||
'checkin_attention': self.order.checkin_attention,
|
||||
'checkin_text': self.order.checkin_text,
|
||||
'checkin_attention': self.order.checkin_attention
|
||||
})
|
||||
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
|
||||
|
||||
@@ -748,13 +710,7 @@ class OrderComment(OrderView):
|
||||
self.order.log_action('pretix.event.order.checkin_attention', user=self.request.user, data={
|
||||
'new_value': form.cleaned_data.get('checkin_attention')
|
||||
})
|
||||
|
||||
if form.cleaned_data.get('checkin_text') != self.order.checkin_text:
|
||||
self.order.checkin_text = form.cleaned_data.get('checkin_text')
|
||||
self.order.log_action('pretix.event.order.checkin_text', user=self.request.user, data={
|
||||
'new_value': form.cleaned_data.get('checkin_text')
|
||||
})
|
||||
self.order.save(update_fields=['checkin_attention', 'checkin_text', 'comment', 'custom_followup_at'])
|
||||
self.order.save(update_fields=['checkin_attention', 'comment', 'custom_followup_at'])
|
||||
self.order.refresh_from_db()
|
||||
messages.success(self.request, _('The comment has been updated.'))
|
||||
else:
|
||||
@@ -2523,7 +2479,6 @@ class ExportMixin:
|
||||
prefix=ex.identifier,
|
||||
initial=initial
|
||||
)
|
||||
ex.multisheet_warning = isinstance(ex, MultiSheetListExporter) and len(ex.sheets) > 1
|
||||
ex.form.fields = ex.export_form_fields
|
||||
return ex
|
||||
|
||||
|
||||
@@ -72,9 +72,7 @@ from pretix.api.models import ApiCall, WebHook
|
||||
from pretix.api.webhooks import manually_retry_all_calls
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.exporter import (
|
||||
MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||
)
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry,
|
||||
@@ -1667,7 +1665,6 @@ class ExportMixin:
|
||||
prefix=ex.identifier,
|
||||
initial=initial
|
||||
)
|
||||
ex.multisheet_warning = isinstance(ex, MultiSheetListExporter) and len(ex.sheets) > 1
|
||||
ex.form.fields = ex.export_form_fields
|
||||
if not isinstance(ex, OrganizerLevelExportMixin):
|
||||
ex.form.fields.update([
|
||||
|
||||
@@ -267,7 +267,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
|
||||
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
wle.availability = (
|
||||
Quota.AVAILABILITY_GONE if free_seats == 0 else wle.availability[0],
|
||||
min(free_seats, wle.availability[1]) if wle.availability[1] is not None else free_seats,
|
||||
min(free_seats, wle.availability[1])
|
||||
)
|
||||
|
||||
itemvar_cache[(wle.item, wle.variation, wle.subevent)] = wle.availability
|
||||
|
||||
@@ -74,11 +74,7 @@ class InList(Func):
|
||||
raise TypeError(f'Dynamic right-hand-site currently not implemented, found {type(self.source_expressions[1])}')
|
||||
rhs, rhs_params = ['%s' for _ in self.source_expressions[1].value], [d for d in self.source_expressions[1].value]
|
||||
|
||||
if rhs:
|
||||
return '%s IN (%s)' % (lhs, ', '.join(rhs)), lhs_params + rhs_params
|
||||
else:
|
||||
# "IN ()" is not considered valid SQL by PostgreSQL (unlike SQLite)
|
||||
return 'FALSE', []
|
||||
return '%s IN (%s)' % (lhs, ', '.join(rhs)), lhs_params + rhs_params
|
||||
|
||||
|
||||
def tolerance(b, tol=None, sign=1):
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import types
|
||||
from datetime import datetime
|
||||
|
||||
from PIL import Image
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
|
||||
def monkeypatch_vobject_performance():
|
||||
@@ -69,26 +67,6 @@ def monkeypatch_pillow_safer():
|
||||
Image.ID.remove("EPS")
|
||||
|
||||
|
||||
def monkeypatch_requests_timeout():
|
||||
"""
|
||||
The requests package does not by default set a timeout for outgoing HTTP requests. This is dangerous especially since
|
||||
celery tasks have no timeout on the task as a whole (as web requests do), so HTTP requests to a non-responding
|
||||
external service could lead to a clogging of the entire celery queue.
|
||||
"""
|
||||
old_httpadapter_send = HTTPAdapter.send
|
||||
|
||||
def httpadapter_send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None, **kwargs):
|
||||
if timeout is None:
|
||||
timeout = 30
|
||||
return types.MethodType(old_httpadapter_send, self)(
|
||||
request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
HTTPAdapter.send = httpadapter_send
|
||||
|
||||
|
||||
def monkeypatch_all_at_ready():
|
||||
monkeypatch_vobject_performance()
|
||||
monkeypatch_pillow_safer()
|
||||
monkeypatch_requests_timeout()
|
||||
|
||||
@@ -164,9 +164,9 @@ def resize_image(image, size):
|
||||
return image
|
||||
|
||||
|
||||
def create_thumbnail(sourcename, size, formats=None):
|
||||
def create_thumbnail(sourcename, size):
|
||||
source = default_storage.open(sourcename)
|
||||
image = Image.open(BytesIO(source.read()), formats=formats or settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
image = Image.open(BytesIO(source.read()), formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
try:
|
||||
image.load()
|
||||
except:
|
||||
@@ -208,9 +208,9 @@ def create_thumbnail(sourcename, size, formats=None):
|
||||
return t
|
||||
|
||||
|
||||
def get_thumbnail(source, size, formats=None):
|
||||
def get_thumbnail(source, size):
|
||||
# Assumes files are immutable
|
||||
try:
|
||||
return Thumbnail.objects.get(source=source, size=size)
|
||||
except Thumbnail.DoesNotExist:
|
||||
return create_thumbnail(source, size, formats=formats)
|
||||
return create_thumbnail(source, size)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user