mirror of
https://github.com/pretix/pretix.git
synced 2026-08-17 11:56:27 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb8e3feb3d | ||
|
|
eb5ff1f34a |
+3
-3
@@ -10,9 +10,9 @@ tests:
|
|||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- make all compress
|
||||||
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --ignore=tests/e2e --maxfail=100
|
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||||
except:
|
except:
|
||||||
- '/^v.*$/'
|
- pypi
|
||||||
pypi:
|
pypi:
|
||||||
stage: release
|
stage: release
|
||||||
image:
|
image:
|
||||||
@@ -35,7 +35,7 @@ pypi:
|
|||||||
- twine check dist/*
|
- twine check dist/*
|
||||||
- twine upload dist/*
|
- twine upload dist/*
|
||||||
only:
|
only:
|
||||||
- '/^v.*$/'
|
- pypi
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- src/dist/
|
- src/dist/
|
||||||
|
|||||||
+2
-1
@@ -57,7 +57,8 @@ COPY vite.config.ts /pretix/vite.config.ts
|
|||||||
|
|
||||||
RUN pip3 install -U \
|
RUN pip3 install -U \
|
||||||
pip \
|
pip \
|
||||||
setuptools && \
|
setuptools \
|
||||||
|
wheel && \
|
||||||
cd /pretix && \
|
cd /pretix && \
|
||||||
PRETIX_DOCKER_BUILD=TRUE pip3 install \
|
PRETIX_DOCKER_BUILD=TRUE pip3 install \
|
||||||
-e ".[memcached]" \
|
-e ".[memcached]" \
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ Cart position endpoints
|
|||||||
* ``attendee_email`` (optional)
|
* ``attendee_email`` (optional)
|
||||||
* ``subevent`` (optional)
|
* ``subevent`` (optional)
|
||||||
* ``expires`` (optional)
|
* ``expires`` (optional)
|
||||||
* ``includes_tax`` (optional, **DEPRECATED**, do not use, will be removed)
|
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
|
||||||
* ``sales_channel`` (optional)
|
* ``sales_channel`` (optional)
|
||||||
* ``voucher`` (optional, expect a voucher code)
|
* ``voucher`` (optional, expect a voucher code)
|
||||||
* ``addons`` (optional, expect a list of nested objects of cart positions)
|
* ``addons`` (optional, expect a list of nested objects of cart positions)
|
||||||
|
|||||||
@@ -46,14 +46,12 @@ Checking a ticket in
|
|||||||
this request twice with the same nonce, the second request will also succeed but will always
|
this request twice with the same nonce, the second request will also succeed but will always
|
||||||
create only one check-in object even when the previous request was successful as well. This
|
create only one check-in object even when the previous request was successful as well. This
|
||||||
allows for a certain level of idempotency and enables you to re-try after a connection failure.
|
allows for a certain level of idempotency and enables you to re-try after a connection failure.
|
||||||
:<json string exchange_medium_type: To perform an exchange to a reusable medium, pass the type of the new reusable medium
|
|
||||||
:<json string exchange_medium_identifier: To perform an exchange to a reusable media, pass the identifier of the new medium
|
|
||||||
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
|
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
|
||||||
order) when building texts (currently only the ``reason_explanation`` response field).
|
order) when building texts (currently only the ``reason_explanation`` response field).
|
||||||
Defaults to ``false`` in which case the server will determine the language (currently
|
Defaults to ``false`` in which case the server will determine the language (currently
|
||||||
the event default language, might change in the future with support for the
|
the event default language, might change in the future with support for the
|
||||||
``Accept-Language`` header).
|
``Accept-Language`` header).
|
||||||
:>json string status: ``"ok"``, ``"incomplete"``, ``"exchange"``, or ``"error"``
|
:>json string status: ``"ok"``, ``"incomplete"``, or ``"error"``
|
||||||
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
|
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
|
||||||
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
|
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
|
||||||
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the
|
:>json object position: Copy of the matching order position (if any was found). The contents are the same as the
|
||||||
@@ -69,8 +67,6 @@ Checking a ticket in
|
|||||||
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
|
||||||
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
|
including the attributes ``id``, ``name``, ``event``, ``subevent``, and ``include_pending``.
|
||||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||||
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
|
|
||||||
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
|
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -228,9 +224,6 @@ Checking a ticket in
|
|||||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||||
* ``revoked`` - Ticket code has been revoked.
|
* ``revoked`` - Ticket code has been revoked.
|
||||||
* ``unapproved`` - Order has not yet been approved.
|
* ``unapproved`` - Order has not yet been approved.
|
||||||
* ``already_exchanged`` - Ticket already has been exchanged for a reusable medium that must now be used for check-in.
|
|
||||||
* ``medium_invalid`` - Reusable medium identifier given was not found or is not valid.
|
|
||||||
* ``medium_exists`` - Reusable medium identifier already exists, but expected to be new.
|
|
||||||
* ``error`` - Internal error.
|
* ``error`` - Internal error.
|
||||||
|
|
||||||
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
|
In case of reason ``rules`` and ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||||
|
|||||||
@@ -602,8 +602,7 @@ Order position endpoints
|
|||||||
|
|
||||||
We no longer recommend using this API if you're building a ticket scanning application, as it has a few design
|
We no longer recommend using this API if you're building a ticket scanning application, as it has a few design
|
||||||
flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not
|
flaws that can lead to `security issues`_ or compatibility issues due to barcode content characters that are not
|
||||||
URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead. Advanced features like medium
|
URL-safe. We recommend to use our new :ref:`check-in API <rest-checkin>` instead.
|
||||||
exchange are only supported on the new API.
|
|
||||||
|
|
||||||
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
||||||
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
||||||
@@ -742,9 +741,6 @@ Order position endpoints
|
|||||||
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
* ``ambiguous`` - Multiple tickets match scan, rejected.
|
||||||
* ``revoked`` - Ticket code has been revoked.
|
* ``revoked`` - Ticket code has been revoked.
|
||||||
* ``unapproved`` - Order has not yet been approved.
|
* ``unapproved`` - Order has not yet been approved.
|
||||||
* ``already_exchanged`` - Ticket already has been exchanged for a reusable medium that must now be used for check-in.
|
|
||||||
* ``medium_invalid`` - Reusable medium identifier given was not found and could not be automatically created.
|
|
||||||
* ``medium_exists`` - Reusable medium identifier already exists, but expected to be new.
|
|
||||||
|
|
||||||
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
|
In case of reason ``rules`` or ``invalid_time``, there might be an additional response field ``reason_explanation``
|
||||||
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
|
with a human-readable description of the violated rules. However, that field can also be missing or be ``null``.
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ allow_waitinglist boolean If ``false``,
|
|||||||
product when it is sold out.
|
product when it is sold out.
|
||||||
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
|
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
|
||||||
media_policy string Policy on how to handle reusable media (experimental feature).
|
media_policy string Policy on how to handle reusable media (experimental feature).
|
||||||
Possible values are ``null``, ``"new"``, ``"reuse"``, ``"reuse_or_new"``, ``"append"``, and ``"append_or_new"``.
|
Possible values are ``null``, ``"new"``, ``"reuse"``, and ``"reuse_or_new"``.
|
||||||
media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices.
|
media_type string Type of reusable media to work on (experimental feature). See :ref:`rest-reusablemedia` for possible choices.
|
||||||
show_quota_left boolean Publicly show how many tickets are still available.
|
show_quota_left boolean Publicly show how many tickets are still available.
|
||||||
If this is ``null``, the event default is used.
|
If this is ``null``, the event default is used.
|
||||||
|
|||||||
@@ -1069,7 +1069,7 @@ Creating orders
|
|||||||
* ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
* ``valid_from`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
||||||
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
||||||
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
||||||
* ``use_reusable_medium`` (optional, causes the new ticket to be connected to the given reusable medium, identified by its ID)
|
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
|
||||||
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
|
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
|
||||||
* ``answers``
|
* ``answers``
|
||||||
|
|
||||||
|
|||||||
@@ -21,16 +21,12 @@ id integer Internal ID of
|
|||||||
type string Type of medium, e.g. ``"barcode"``, ``"nfc_uid"`` or ``"nfc_mf0aes"``.
|
type string Type of medium, e.g. ``"barcode"``, ``"nfc_uid"`` or ``"nfc_mf0aes"``.
|
||||||
organizer string Organizer slug of the organizer who "owns" this medium.
|
organizer string Organizer slug of the organizer who "owns" this medium.
|
||||||
identifier string Unique identifier of the medium. The format depends on the ``type``.
|
identifier string Unique identifier of the medium. The format depends on the ``type``.
|
||||||
claim_token string Secret token to claim ownership of the medium (or ``null``)
|
|
||||||
label string Label to identify the medium, usually something human readable (or ``null``)
|
|
||||||
active boolean Whether this medium may be used.
|
active boolean Whether this medium may be used.
|
||||||
created datetime Date of creation
|
created datetime Date of creation
|
||||||
updated datetime Date of last modification
|
updated datetime Date of last modification
|
||||||
expires datetime Expiry date (or ``null``)
|
expires datetime Expiry date (or ``null``)
|
||||||
customer string Identifier of a customer account this medium belongs to.
|
customer string Identifier of a customer account this medium belongs to.
|
||||||
linked_orderpositions list of integers Internal IDs of tickets this medium is linked to.
|
linked_orderposition integer Internal ID of a ticket this medium is linked to.
|
||||||
linked_orderposition integer **DEPRECATED.** ID of the ticket the medium is linked to, if it is linked to
|
|
||||||
only one ticket. ``null``, if the medium is linked to none or multiple tickets.
|
|
||||||
linked_giftcard integer Internal ID of a gift card this medium is linked to.
|
linked_giftcard integer Internal ID of a gift card this medium is linked to.
|
||||||
info object Additional data, content depends on the ``type``. Consider
|
info object Additional data, content depends on the ``type``. Consider
|
||||||
this internal to the system and don't use it for your own data.
|
this internal to the system and don't use it for your own data.
|
||||||
@@ -43,14 +39,6 @@ Existing media types are:
|
|||||||
- ``nfc_uid``
|
- ``nfc_uid``
|
||||||
- ``nfc_mf0aes``
|
- ``nfc_mf0aes``
|
||||||
|
|
||||||
|
|
||||||
.. versionchanged:: 2026.5
|
|
||||||
|
|
||||||
The ``claim_token``, ``label``, ``linked_orderpositions`` attributes have been added, the ``linked_orderposition`` attribute has been
|
|
||||||
deprecated. Note: To maintain backwards compatibility ``linked_orderposition`` contains the internal ID of the linked order position
|
|
||||||
if the medium has exactly one order position in ``linked_orderpositions``.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -89,7 +77,6 @@ Endpoints
|
|||||||
"active": True,
|
"active": True,
|
||||||
"expires": None,
|
"expires": None,
|
||||||
"customer": None,
|
"customer": None,
|
||||||
"linked_orderpositions": [],
|
|
||||||
"linked_orderposition": None,
|
"linked_orderposition": None,
|
||||||
"linked_giftcard": None,
|
"linked_giftcard": None,
|
||||||
"notes": None,
|
"notes": None,
|
||||||
@@ -105,13 +92,10 @@ Endpoints
|
|||||||
:query string customer: Only show media linked to the given customer.
|
:query string customer: Only show media linked to the given customer.
|
||||||
:query string created_since: Only show media created since a given date.
|
:query string created_since: Only show media created since a given date.
|
||||||
:query string updated_since: Only show media updated since a given date.
|
:query string updated_since: Only show media updated since a given date.
|
||||||
:query integer linked_orderpositions: Only show media linked to the given tickets. Note: you can pass multiple ticket IDs by passing
|
|
||||||
``linked_orderpositions`` multiple times. Any medium matching any linked orderposition will be returned.
|
|
||||||
:query integer linked_orderposition: Only show media linked to the given ticket.
|
:query integer linked_orderposition: Only show media linked to the given ticket.
|
||||||
:query integer linked_giftcard: Only show media linked to the given gift card.
|
:query integer linked_giftcard: Only show media linked to the given gift card.
|
||||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderpositions"``,
|
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
|
||||||
``"linked_orderposition"`` (**DEPRECATED**), or ``"customer"``, the respective field will be shown
|
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
|
||||||
as a nested value instead of just an ID.
|
|
||||||
The nested objects are identical to the respective resources, except that order positions
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
matching easier. The parameter can be given multiple times.
|
matching easier. The parameter can be given multiple times.
|
||||||
@@ -150,7 +134,6 @@ Endpoints
|
|||||||
"active": True,
|
"active": True,
|
||||||
"expires": None,
|
"expires": None,
|
||||||
"customer": None,
|
"customer": None,
|
||||||
"linked_orderpositions": [],
|
|
||||||
"linked_orderposition": None,
|
"linked_orderposition": None,
|
||||||
"linked_giftcard": None,
|
"linked_giftcard": None,
|
||||||
"notes": None,
|
"notes": None,
|
||||||
@@ -208,7 +191,6 @@ Endpoints
|
|||||||
"active": True,
|
"active": True,
|
||||||
"expires": None,
|
"expires": None,
|
||||||
"customer": None,
|
"customer": None,
|
||||||
"linked_orderpositions": [],
|
|
||||||
"linked_orderposition": None,
|
"linked_orderposition": None,
|
||||||
"linked_giftcard": None,
|
"linked_giftcard": None,
|
||||||
"notes": None,
|
"notes": None,
|
||||||
@@ -216,9 +198,9 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to look up a medium for
|
:param organizer: The ``slug`` field of the organizer to look up a medium for
|
||||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
|
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
|
||||||
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
||||||
the respective resources, except that the ``linked_orderpositions`` each will have an attribute of the
|
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
|
||||||
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
||||||
can be given multiple times.
|
can be given multiple times.
|
||||||
:statuscode 201: no error
|
:statuscode 201: no error
|
||||||
@@ -245,7 +227,6 @@ Endpoints
|
|||||||
"active": True,
|
"active": True,
|
||||||
"expires": None,
|
"expires": None,
|
||||||
"customer": None,
|
"customer": None,
|
||||||
"linked_orderpositions": [],
|
|
||||||
"linked_orderposition": None,
|
"linked_orderposition": None,
|
||||||
"linked_giftcard": None,
|
"linked_giftcard": None,
|
||||||
"notes": None,
|
"notes": None,
|
||||||
@@ -270,7 +251,6 @@ Endpoints
|
|||||||
"active": True,
|
"active": True,
|
||||||
"expires": None,
|
"expires": None,
|
||||||
"customer": None,
|
"customer": None,
|
||||||
"linked_orderpositions": [],
|
|
||||||
"linked_orderposition": None,
|
"linked_orderposition": None,
|
||||||
"linked_giftcard": None,
|
"linked_giftcard": None,
|
||||||
"notes": None,
|
"notes": None,
|
||||||
@@ -278,7 +258,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a medium for
|
:param organizer: The ``slug`` field of the organizer to create a medium for
|
||||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
|
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
|
||||||
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
||||||
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
|
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
|
||||||
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
||||||
@@ -307,7 +287,7 @@ Endpoints
|
|||||||
Content-Length: 94
|
Content-Length: 94
|
||||||
|
|
||||||
{
|
{
|
||||||
"linked_orderpositions": [13, 29]
|
"linked_orderposition": 13
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -328,8 +308,7 @@ Endpoints
|
|||||||
"active": True,
|
"active": True,
|
||||||
"expires": None,
|
"expires": None,
|
||||||
"customer": None,
|
"customer": None,
|
||||||
"linked_orderpositions": [13, 29],
|
"linked_orderposition": 13,
|
||||||
"linked_orderposition": None,
|
|
||||||
"linked_giftcard": None,
|
"linked_giftcard": None,
|
||||||
"notes": None,
|
"notes": None,
|
||||||
"info": {}
|
"info": {}
|
||||||
@@ -337,7 +316,7 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
:param id: The ``id`` field of the medium to modify
|
:param id: The ``id`` field of the medium to modify
|
||||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
|
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
|
||||||
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
||||||
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
|
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
|
||||||
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ Backend
|
|||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
||||||
order_info, order_approve_info, event_settings_widget, oauth_application_registered,
|
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
|
||||||
order_position_buttons, subevent_forms, item_formsets, order_search_filter_q, order_search_forms, subevent_detail_html
|
item_formsets, order_search_filter_q, order_search_forms
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
:no-index:
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ is a python method that emulates a behavior similar to ``reverse``:
|
|||||||
|
|
||||||
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
|
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
|
||||||
|
|
||||||
.. autofunction:: pretix.multidomain.urlreverse.eventreverse_absolute
|
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri
|
||||||
|
|
||||||
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
|
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
|
||||||
as its first argument and can be used like this::
|
as its first argument and can be used like this::
|
||||||
|
|||||||
Generated
+91
-109
@@ -370,14 +370,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.2"
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -427,9 +427,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.133.0",
|
"version": "0.129.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
|
||||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -758,9 +758,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -775,9 +775,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -792,9 +792,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
|
||||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -809,9 +809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
|
||||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -826,9 +826,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
|
||||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -843,16 +843,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -863,16 +860,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
|
||||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -883,16 +877,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -903,16 +894,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -923,16 +911,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -943,16 +928,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
|
||||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -963,9 +945,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -980,9 +962,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
|
||||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -999,9 +981,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
|
||||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1016,9 +998,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
|
||||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3176,9 +3158,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3352,9 +3334,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3371,7 +3353,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -3628,14 +3610,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
|
||||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.133.0",
|
"@oxc-project/types": "=0.129.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -3644,27 +3626,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.3",
|
"@rolldown/binding-android-arm64": "1.0.0",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
"@rolldown/binding-darwin-arm64": "1.0.0",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
"@rolldown/binding-darwin-x64": "1.0.0",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
"@rolldown/binding-freebsd-x64": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
"@rolldown/binding-linux-arm64-musl": "1.0.0",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
"@rolldown/binding-linux-x64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
"@rolldown/binding-linux-x64-musl": "1.0.0",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
"@rolldown/binding-openharmony-arm64": "1.0.0",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
"@rolldown/binding-wasm32-wasi": "1.0.0",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
"@rolldown/binding-win32-x64-msvc": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
|
||||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -4343,9 +4325,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4483,17 +4465,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.16",
|
"version": "8.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
|
||||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.14",
|
||||||
"rolldown": "1.0.3",
|
"rolldown": "1.0.0",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
+12
-10
@@ -29,12 +29,12 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.15.*",
|
"BeautifulSoup4==4.14.*",
|
||||||
"bleach==6.4.*",
|
"bleach==6.3.*",
|
||||||
"celery==5.6.*",
|
"celery==5.6.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=49.0.0",
|
"cryptography>=48.0.0",
|
||||||
"css-inline==0.21.*",
|
"css-inline==0.20.*",
|
||||||
"defusedcsv>=3.0.0",
|
"defusedcsv>=3.0.0",
|
||||||
"dnspython==2.*",
|
"dnspython==2.*",
|
||||||
"Django[argon2]==5.2.*",
|
"Django[argon2]==5.2.*",
|
||||||
@@ -93,7 +93,7 @@ dependencies = [
|
|||||||
"redis==7.4.*",
|
"redis==7.4.*",
|
||||||
"reportlab==4.5.*",
|
"reportlab==4.5.*",
|
||||||
"requests==2.32.*",
|
"requests==2.32.*",
|
||||||
"sentry-sdk==2.63.*",
|
"sentry-sdk==2.60.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
@@ -101,23 +101,23 @@ dependencies = [
|
|||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
"ua-parser==1.0.*",
|
"ua-parser==1.0.*",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.8.*",
|
"webauthn==2.7.*",
|
||||||
"zeep==4.3.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.14.*",
|
"aiohttp==3.13.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.36.*",
|
"fakeredis==2.35.*",
|
||||||
"flake8==7.3.*",
|
"flake8==7.3.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==8.0.*",
|
"isort==8.0.*",
|
||||||
"pep8-naming==0.15.*",
|
"pep8-naming==0.15.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio>=1.4.0",
|
"pytest-asyncio>=1.3.0",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
@@ -125,7 +125,7 @@ dev = [
|
|||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.8.*",
|
"pytest-xdist==3.8.*",
|
||||||
"pytest-playwright",
|
"pytest-playwright",
|
||||||
"pytest==9.1.*",
|
"pytest==9.0.*",
|
||||||
"playwright",
|
"playwright",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
@@ -139,6 +139,8 @@ build-backend = "backend"
|
|||||||
backend-path = ["_build"]
|
backend-path = ["_build"]
|
||||||
requires = [
|
requires = [
|
||||||
"setuptools",
|
"setuptools",
|
||||||
|
"setuptools-rust",
|
||||||
|
"wheel",
|
||||||
"importlib_metadata",
|
"importlib_metadata",
|
||||||
"tomli",
|
"tomli",
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ localecompile:
|
|||||||
|
|
||||||
localegen:
|
localegen:
|
||||||
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
|
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||||
./manage.py makemessages --keep-pot -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||||
|
|
||||||
staticfiles: npminstall npmbuild jsi18n
|
staticfiles: npminstall npmbuild jsi18n
|
||||||
./manage.py collectstatic --noinput
|
./manage.py collectstatic --noinput
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2026.6.0.dev0"
|
__version__ = "2026.5.0.dev0"
|
||||||
|
|||||||
@@ -110,8 +110,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
|||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
('GET', 'api-v1:reusablemedium-list'),
|
('GET', 'api-v1:reusablemedium-list'),
|
||||||
('POST', 'api-v1:reusablemedium-lookup'),
|
|
||||||
('PATCH', 'api-v1:reusablemedium-detail')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -88,19 +88,11 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
|||||||
nonce = serializers.CharField(required=False, allow_null=True)
|
nonce = serializers.CharField(required=False, allow_null=True)
|
||||||
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
||||||
answers = serializers.JSONField(required=False, allow_null=True)
|
answers = serializers.JSONField(required=False, allow_null=True)
|
||||||
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
|
|
||||||
exchange_medium_identifier = serializers.CharField(required=False)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
exchange_fields = ["exchange_medium_type", "exchange_medium_identifier"]
|
|
||||||
if any(attrs.get(k) is None for k in exchange_fields) and not all(attrs.get(k) is None for k in exchange_fields):
|
|
||||||
raise ValidationError("If you set any of exchange_medium_type or exchange_medium_identifier, you need to set both of them.")
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
||||||
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
event = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ from pretix.base.settings import (
|
|||||||
LazyI18nStringList, validate_event_settings,
|
LazyI18nStringList, validate_event_settings,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import api_event_settings_fields
|
from pretix.base.signals import api_event_settings_fields
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_event_url(self, event):
|
def get_event_url(self, event):
|
||||||
return eventreverse_absolute(event, 'presale:event.index')
|
return build_absolute_uri(event, 'presale:event.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
@@ -871,7 +871,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'og_image',
|
'og_image',
|
||||||
'name_scheme',
|
'name_scheme',
|
||||||
'reusable_media_active',
|
'reusable_media_active',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'reusable_media_type_barcode',
|
'reusable_media_type_barcode',
|
||||||
'reusable_media_type_barcode_identifier_length',
|
'reusable_media_type_barcode_identifier_length',
|
||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
@@ -886,7 +885,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
# These are read-only since they are currently only settable on organizers, not events
|
# These are read-only since they are currently only settable on organizers, not events
|
||||||
'reusable_media_active',
|
'reusable_media_active',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'reusable_media_type_barcode',
|
'reusable_media_type_barcode',
|
||||||
'reusable_media_type_barcode_identifier_length',
|
'reusable_media_type_barcode_identifier_length',
|
||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
@@ -972,7 +970,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
'reusable_media_type_nfc_mf0aes',
|
'reusable_media_type_nfc_mf0aes',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'system_question_order',
|
'system_question_order',
|
||||||
'tax_rule_payment',
|
'tax_rule_payment',
|
||||||
'tax_rule_cancellation',
|
'tax_rule_cancellation',
|
||||||
|
|||||||
@@ -66,14 +66,13 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
expand_nested = self.context['request'].query_params.getlist('expand')
|
|
||||||
|
|
||||||
if 'linked_giftcard' in expand_nested:
|
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
||||||
if not self.context["can_read_giftcards"]:
|
if not self.context["can_read_giftcards"]:
|
||||||
raise PermissionDenied("No permission to access gift card details.")
|
raise PermissionDenied("No permission to access gift card details.")
|
||||||
|
|
||||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
||||||
if 'linked_giftcard.owner_ticket' in expand_nested:
|
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
||||||
else:
|
else:
|
||||||
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
|
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
|
||||||
@@ -82,27 +81,17 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
queryset=self.context['organizer'].issued_gift_cards.all()
|
queryset=self.context['organizer'].issued_gift_cards.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
# keep linked_orderposition (singular) for backwards compatibility, will be overwritten in self.validate
|
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
# Permission Check performed in to_representation
|
||||||
required=False,
|
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
||||||
allow_null=True,
|
|
||||||
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'linked_orderposition' in expand_nested or 'linked_orderpositions' in expand_nested:
|
|
||||||
self.fields['linked_orderpositions'] = NestedOrderPositionSerializer(
|
|
||||||
many=True,
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.fields['linked_orderpositions'] = serializers.PrimaryKeyRelatedField(
|
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
||||||
many=True,
|
|
||||||
required=False,
|
required=False,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
|
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'customer' in expand_nested:
|
if 'customer' in self.context['request'].query_params.getlist('expand'):
|
||||||
if not self.context["can_read_customers"]:
|
if not self.context["can_read_customers"]:
|
||||||
raise PermissionDenied("No permission to access customer details.")
|
raise PermissionDenied("No permission to access customer details.")
|
||||||
|
|
||||||
@@ -117,21 +106,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
if 'linked_orderposition' in data:
|
|
||||||
linked_orderposition = data['linked_orderposition']
|
|
||||||
# backwards-compatibility
|
|
||||||
if 'linked_orderpositions' in data:
|
|
||||||
raise ValidationError({
|
|
||||||
'linked_orderposition': 'You cannot use linked_orderposition and linked_orderpositions at the same time.'
|
|
||||||
})
|
|
||||||
if self.instance and self.instance.linked_orderpositions.count() > 1:
|
|
||||||
raise ValidationError({
|
|
||||||
'linked_orderposition': 'There are more than one linked_orderposition. You need to use linked_orderpositions.'
|
|
||||||
})
|
|
||||||
|
|
||||||
data['linked_orderpositions'] = [linked_orderposition] if linked_orderposition else []
|
|
||||||
del data['linked_orderposition']
|
|
||||||
|
|
||||||
if 'type' in data and 'identifier' in data:
|
if 'type' in data and 'identifier' in data:
|
||||||
qs = self.context['organizer'].reusable_media.filter(
|
qs = self.context['organizer'].reusable_media.filter(
|
||||||
identifier=data['identifier'], type=data['type']
|
identifier=data['identifier'], type=data['type']
|
||||||
@@ -147,28 +121,14 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
r = super().to_representation(instance)
|
r = super().to_representation(instance)
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
|
|
||||||
ops = r.get('linked_orderpositions', [])
|
|
||||||
# late permission evaluations for checks that depend on the actual linked events
|
# late permission evaluations for checks that depend on the actual linked events
|
||||||
expand_nested = self.context['request'].query_params.getlist('expand')
|
expand_nested = self.context['request'].query_params.getlist('expand')
|
||||||
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
||||||
if ops and 'linked_orderposition' in expand_nested or 'linked_orderpositions' in expand_nested:
|
if 'linked_orderposition' in expand_nested:
|
||||||
ops_noperm = []
|
if instance.linked_orderposition is not None:
|
||||||
for lop in instance.linked_orderpositions.all():
|
event = instance.linked_orderposition.order.event
|
||||||
event = lop.order.event
|
|
||||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
||||||
ops_noperm.append(lop.id)
|
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
|
||||||
if ops_noperm:
|
|
||||||
ops = [
|
|
||||||
{'id': op['id']} if op['id'] in ops_noperm
|
|
||||||
else op
|
|
||||||
for op in ops
|
|
||||||
]
|
|
||||||
r['linked_orderpositions'] = ops
|
|
||||||
|
|
||||||
# add linked_orderposition (singular) for backwards compatibility
|
|
||||||
if len(ops) < 2:
|
|
||||||
r['linked_orderposition'] = ops[0] if ops else None
|
|
||||||
|
|
||||||
if 'linked_giftcard.owner_ticket' in expand_nested:
|
if 'linked_giftcard.owner_ticket' in expand_nested:
|
||||||
gc = instance.linked_giftcard
|
gc = instance.linked_giftcard
|
||||||
@@ -188,12 +148,10 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
'updated',
|
'updated',
|
||||||
'type',
|
'type',
|
||||||
'identifier',
|
'identifier',
|
||||||
'claim_token',
|
|
||||||
'label',
|
|
||||||
'active',
|
'active',
|
||||||
'expires',
|
'expires',
|
||||||
'customer',
|
'customer',
|
||||||
'linked_orderpositions',
|
'linked_orderposition',
|
||||||
'linked_giftcard',
|
'linked_giftcard',
|
||||||
'info',
|
'info',
|
||||||
'notes',
|
'notes',
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ from pretix.base.settings import (
|
|||||||
)
|
)
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -757,7 +757,7 @@ class PaymentURLField(serializers.URLField):
|
|||||||
def to_representation(self, instance: OrderPayment):
|
def to_representation(self, instance: OrderPayment):
|
||||||
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||||
return None
|
return None
|
||||||
return eventreverse_absolute(instance.order.event, 'presale:event.order.pay', kwargs={
|
return build_absolute_uri(instance.order.event, 'presale:event.order.pay', kwargs={
|
||||||
'order': instance.order.code,
|
'order': instance.order.code,
|
||||||
'secret': instance.order.secret,
|
'secret': instance.order.secret,
|
||||||
'payment': instance.pk,
|
'payment': instance.pk,
|
||||||
@@ -806,7 +806,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class OrderURLField(serializers.URLField):
|
class OrderURLField(serializers.URLField):
|
||||||
def to_representation(self, instance: Order):
|
def to_representation(self, instance: Order):
|
||||||
return eventreverse_absolute(instance.event, 'presale:event.order', kwargs={
|
return build_absolute_uri(instance.event, 'presale:event.order', kwargs={
|
||||||
'order': instance.code,
|
'order': instance.code,
|
||||||
'secret': instance.secret,
|
'secret': instance.secret,
|
||||||
})
|
})
|
||||||
@@ -1149,7 +1149,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -1589,7 +1588,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos_data['attendee_name_parts'] = {
|
pos_data['attendee_name_parts'] = {
|
||||||
'_legacy': attendee_name
|
'_legacy': attendee_name
|
||||||
}
|
}
|
||||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium')})
|
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
|
||||||
if simulate:
|
if simulate:
|
||||||
pos.order = order._wrapped
|
pos.order = order._wrapped
|
||||||
else:
|
else:
|
||||||
@@ -1704,25 +1703,15 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
answ.options.add(*options)
|
answ.options.add(*options)
|
||||||
|
|
||||||
if use_reusable_medium:
|
if use_reusable_medium:
|
||||||
if pos.item.media_policy not in (Item.MEDIA_POLICY_APPEND, Item.MEDIA_POLICY_APPEND_OR_NEW):
|
use_reusable_medium.linked_orderposition = pos
|
||||||
for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True):
|
use_reusable_medium.save(update_fields=['linked_orderposition'])
|
||||||
use_reusable_medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.removed',
|
|
||||||
data={
|
|
||||||
'linked_orderposition': op_pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
use_reusable_medium.linked_orderpositions.set([pos])
|
|
||||||
else:
|
|
||||||
use_reusable_medium.linked_orderpositions.add(pos)
|
|
||||||
use_reusable_medium.log_action(
|
use_reusable_medium.log_action(
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
'pretix.reusable_medium.linked_orderposition.changed',
|
||||||
data={
|
data={
|
||||||
'by_order': order.code,
|
'by_order': order.code,
|
||||||
'linked_orderposition': pos.pk,
|
'linked_orderposition': pos.pk,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
use_reusable_medium.touch()
|
|
||||||
|
|
||||||
if not simulate:
|
if not simulate:
|
||||||
for cp in delete_cps:
|
for cp in delete_cps:
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ from pretix.helpers.permission_migration import (
|
|||||||
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
|
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
|
||||||
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
||||||
)
|
)
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class OrganizerSerializer(I18nAwareModelSerializer):
|
|||||||
slug = serializers.CharField(read_only=True)
|
slug = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def get_organizer_url(self, organizer):
|
def get_organizer_url(self, organizer):
|
||||||
return eventreverse_absolute(organizer, 'presale:organizer.index')
|
return build_absolute_uri(organizer, 'presale:organizer.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organizer
|
model = Organizer
|
||||||
@@ -499,7 +499,7 @@ class TeamInviteSerializer(serializers.ModelSerializer):
|
|||||||
'user': self,
|
'user': self,
|
||||||
'organizer': self.context['organizer'].name,
|
'organizer': self.context['organizer'].name,
|
||||||
'team': instance.team.name,
|
'team': instance.team.name,
|
||||||
'url': mainreverse_absolute('control:auth.invite', kwargs={
|
'url': build_global_uri('control:auth.invite', kwargs={
|
||||||
'token': instance.token
|
'token': instance.token
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -605,7 +605,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'cookie_consent_dialog_button_yes',
|
'cookie_consent_dialog_button_yes',
|
||||||
'cookie_consent_dialog_button_no',
|
'cookie_consent_dialog_button_no',
|
||||||
'reusable_media_active',
|
'reusable_media_active',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'reusable_media_type_barcode',
|
'reusable_media_type_barcode',
|
||||||
'reusable_media_type_barcode_identifier_length',
|
'reusable_media_type_barcode_identifier_length',
|
||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
|
|||||||
+25
-138
@@ -69,10 +69,8 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.orders import PrintLog
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.permissions import AnyPermissionOf
|
from pretix.base.permissions import AnyPermissionOf
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredMediaExchangeError, RequiredQuestionsError, SQLLogic,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
perform_checkin,
|
|
||||||
)
|
)
|
||||||
from pretix.base.services.media import perform_media_exchange
|
|
||||||
from pretix.base.signals import checkin_annulled
|
from pretix.base.signals import checkin_annulled
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
@@ -456,8 +454,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
|
|
||||||
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
|
||||||
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
||||||
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False,
|
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False):
|
||||||
exchange_medium_type=None, exchange_medium_identifier=None):
|
|
||||||
if not checkinlists:
|
if not checkinlists:
|
||||||
raise ValidationError('No check-in list passed.')
|
raise ValidationError('No check-in list passed.')
|
||||||
|
|
||||||
@@ -466,7 +463,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
|
|
||||||
device = auth if isinstance(auth, Device) else None
|
device = auth if isinstance(auth, Device) else None
|
||||||
gate = gate or (auth.gate if isinstance(auth, Device) else None)
|
gate = gate or (auth.gate if isinstance(auth, Device) else None)
|
||||||
medium = None
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'request': request,
|
'request': request,
|
||||||
@@ -495,7 +491,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
)
|
)
|
||||||
raw_barcode_for_checkin = None
|
raw_barcode_for_checkin = None
|
||||||
from_revoked_secret = False
|
from_revoked_secret = False
|
||||||
reusable_medium_used = None
|
|
||||||
if simulate:
|
if simulate:
|
||||||
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
|
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
|
||||||
|
|
||||||
@@ -526,12 +521,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
# with respecting the force option), or it's a reusable medium (-> proceed with that)
|
# with respecting the force option), or it's a reusable medium (-> proceed with that)
|
||||||
if not op_candidates:
|
if not op_candidates:
|
||||||
try:
|
try:
|
||||||
medium = ReusableMedium.objects.active().filter(
|
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
|
||||||
Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
|
|
||||||
).get(
|
|
||||||
organizer_id=checkinlists[0].event.organizer_id,
|
organizer_id=checkinlists[0].event.organizer_id,
|
||||||
type=source_type,
|
type=source_type,
|
||||||
identifier=raw_barcode,
|
identifier=raw_barcode,
|
||||||
|
linked_orderposition__isnull=False,
|
||||||
)
|
)
|
||||||
raw_barcode_for_checkin = raw_barcode
|
raw_barcode_for_checkin = raw_barcode
|
||||||
except ReusableMedium.DoesNotExist:
|
except ReusableMedium.DoesNotExist:
|
||||||
@@ -634,9 +628,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
else:
|
else:
|
||||||
linked_ops = medium.linked_orderpositions.all().select_related("order").prefetch_related("addons")
|
if media.linked_orderposition.order.event_id not in list_by_event:
|
||||||
linked_event_ids = {op.order.event_id for op in linked_ops}
|
|
||||||
if not any(event_id in list_by_event for event_id in linked_event_ids):
|
|
||||||
# Medium exists but connected ticket is for the wrong event
|
# Medium exists but connected ticket is for the wrong event
|
||||||
if not simulate:
|
if not simulate:
|
||||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||||
@@ -662,91 +654,28 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'checkin_texts': [],
|
'checkin_texts': [],
|
||||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||||
}, status=404)
|
}, status=404)
|
||||||
op_candidates = []
|
op_candidates = [media.linked_orderposition]
|
||||||
for op in linked_ops:
|
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
|
||||||
if op.order.event_id in list_by_event:
|
op_candidates += list(media.linked_orderposition.addons.all())
|
||||||
reusable_medium_used = medium
|
|
||||||
op_candidates.append(op)
|
|
||||||
if list_by_event[op.order.event_id].addon_match:
|
|
||||||
op_candidates += list(op.addons.all())
|
|
||||||
|
|
||||||
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
|
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
|
||||||
# key on the same list, we're probably dealing with multiple linked_orderpositions or the ``addon_match`` case
|
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
|
||||||
# here and need to figure out which op has the right product. This basically is a valid-for-checkin-test on every op.
|
# which add-on has the right product.
|
||||||
if len(op_candidates) > 1:
|
if len(op_candidates) > 1:
|
||||||
|
op_candidates_matching_product = [
|
||||||
|
op for op in op_candidates
|
||||||
|
if (
|
||||||
|
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
|
||||||
|
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
if not reusable_medium_used:
|
if len(op_candidates_matching_product) == 0:
|
||||||
# 3a. First, we clean up that we made an imprecise query above. If a scan is made for multiple check-in lists,
|
# None of the found add-ons has the correct product, too bad! We could just error out here, but
|
||||||
# we have queried ``addon_to__secret=raw_barcode``, even if some of the lists in question do not allow addon
|
|
||||||
# matching. So we accept all candidates that match one of these cases:
|
|
||||||
# - Exactly the ticket secret we scanned (because that's always a possible result)
|
|
||||||
# - Exactly the ticket pk we scanned (on legacy endpoints)
|
|
||||||
# - An add-on on a list that allows add-on matching
|
|
||||||
# This is not necessary when a reusable media was used, since in that case we already obeyed list.addon_match
|
|
||||||
# correctly above.
|
|
||||||
op_candidates_filtered = [
|
|
||||||
op for op in op_candidates
|
|
||||||
if (
|
|
||||||
op.secret == raw_barcode or
|
|
||||||
list_by_event[op.order.event_id].addon_match or
|
|
||||||
(str(op.pk) == raw_barcode and legacy_url_support and not untrusted_input)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
op_candidates_filtered = op_candidates
|
|
||||||
|
|
||||||
if len(op_candidates_filtered) > 1:
|
|
||||||
# 3b. If we still have multiple candidates, we filter by product based on the check-in list configuration.
|
|
||||||
# This is relevant for the addon_match scenario where the scanned ticket has multiple add-ons, but only
|
|
||||||
# one is contained in the check-in list used to scan. It makes sense to filter this first, since it is a
|
|
||||||
# "static" check, i.e. scanning the same QR code on the same check-in list will always do the same, no matter
|
|
||||||
# when I scan it, and it is "intentional" filtering in the sense that the admin configured this behaviour
|
|
||||||
# into the check-in list.
|
|
||||||
op_candidates_filtered = [
|
|
||||||
op for op in op_candidates_filtered
|
|
||||||
if list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()}
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(op_candidates_filtered) > 1:
|
|
||||||
# 3c. If we still have multiple candidates, we filter by validity date. This was introduced for the case where
|
|
||||||
# a reusable media refers to two tickets, one currently valid and one expired or in the future. Howeer,
|
|
||||||
# it could in theory also happen with two add-ons being on the same check-in list but without overlapping
|
|
||||||
# validity. It makes sense to filter this "after" the previous checks since it is not "intentional" filtering
|
|
||||||
# configured by the admin but "accidental" filtering that depends on the time of execution.
|
|
||||||
op_candidates_filtered = [
|
|
||||||
op for op in op_candidates_filtered
|
|
||||||
if (
|
|
||||||
(not op.valid_from or op.valid_from <= datetime) and
|
|
||||||
(not op.valid_until or op.valid_until > datetime)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(op_candidates_filtered) == 0:
|
|
||||||
# None of the ops is valid today or has the correct product, too bad! We could just error out here, but
|
|
||||||
# instead we just continue with *any* product and have it rejected by the check in perform_checkin.
|
# instead we just continue with *any* product and have it rejected by the check in perform_checkin.
|
||||||
# To improve the error message, we select the op that will "work next" or - if none matches - "worked last".
|
# This has the advantage of a better error message.
|
||||||
op_candidate = None
|
op_candidates = [op_candidates[0]]
|
||||||
for op in op_candidates:
|
elif len(op_candidates_matching_product) > 1:
|
||||||
if (
|
|
||||||
op.valid_from and op.valid_from > datetime and
|
|
||||||
(not op_candidate or op.valid_from < op_candidate.valid_from)
|
|
||||||
):
|
|
||||||
op_candidate = op
|
|
||||||
|
|
||||||
if not op_candidate:
|
|
||||||
# no candidate in the future, get closest in the past
|
|
||||||
for op in op_candidates:
|
|
||||||
if (
|
|
||||||
op.valid_until and op.valid_until < datetime and
|
|
||||||
(not op_candidate or op.valid_until > op_candidate.valid_until)
|
|
||||||
):
|
|
||||||
op_candidate = op
|
|
||||||
|
|
||||||
if not op_candidate:
|
|
||||||
op_candidate = op_candidates[0]
|
|
||||||
|
|
||||||
op_candidates = [op_candidate]
|
|
||||||
elif len(op_candidates_filtered) > 1:
|
|
||||||
# It's still ambiguous, we'll error out.
|
# It's still ambiguous, we'll error out.
|
||||||
# We choose the first match (regardless of product) for the logging since it's most likely to be the
|
# We choose the first match (regardless of product) for the logging since it's most likely to be the
|
||||||
# base product according to our order_by above.
|
# base product according to our order_by above.
|
||||||
@@ -780,7 +709,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
else:
|
else:
|
||||||
op_candidates = op_candidates_filtered
|
op_candidates = op_candidates_matching_product
|
||||||
|
|
||||||
op = op_candidates[0]
|
op = op_candidates[0]
|
||||||
common_checkin_args['list'] = list_by_event[op.order.event_id]
|
common_checkin_args['list'] = list_by_event[op.order.event_id]
|
||||||
@@ -792,10 +721,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
if str(q.pk) in answers_data:
|
if str(q.pk) in answers_data:
|
||||||
try:
|
try:
|
||||||
if q.type == Question.TYPE_FILE:
|
if q.type == Question.TYPE_FILE:
|
||||||
if answers_data[str(q.pk)]:
|
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
|
||||||
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
|
|
||||||
else:
|
|
||||||
given_answers[q] = None
|
|
||||||
else:
|
else:
|
||||||
given_answers[q] = q.clean_answer(answers_data[str(q.pk)])
|
given_answers[q] = q.clean_answer(answers_data[str(q.pk)])
|
||||||
except (ValidationError, BaseValidationError):
|
except (ValidationError, BaseValidationError):
|
||||||
@@ -808,14 +734,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
locale = op.order.event.settings.locale
|
locale = op.order.event.settings.locale
|
||||||
with language(locale):
|
with language(locale):
|
||||||
try:
|
try:
|
||||||
if exchange_medium_identifier and medium:
|
perform_checkin(
|
||||||
# Cannot scan a medium and then request to exchange it
|
|
||||||
raise CheckInError(
|
|
||||||
gettext('You cannot exchange a medium for a medium.'),
|
|
||||||
'error'
|
|
||||||
)
|
|
||||||
|
|
||||||
checkin_args = dict(
|
|
||||||
op=op,
|
op=op,
|
||||||
clist=list_by_event[op.order.event_id],
|
clist=list_by_event[op.order.event_id],
|
||||||
given_answers=given_answers,
|
given_answers=given_answers,
|
||||||
@@ -833,25 +752,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
from_revoked_secret=from_revoked_secret,
|
from_revoked_secret=from_revoked_secret,
|
||||||
simulate=simulate,
|
simulate=simulate,
|
||||||
gate=gate,
|
gate=gate,
|
||||||
reusable_medium=medium,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
|
|
||||||
with transaction.atomic():
|
|
||||||
# Do exchange and check-in atomically, i.e. both succeed or both fail
|
|
||||||
medium = perform_media_exchange(
|
|
||||||
organizer=request.organizer,
|
|
||||||
media_type=exchange_medium_type,
|
|
||||||
identifier=exchange_medium_identifier,
|
|
||||||
link_orderposition=op,
|
|
||||||
user=user,
|
|
||||||
auth=auth,
|
|
||||||
)
|
|
||||||
source_type = medium.media_type.identifier
|
|
||||||
checkin_args['reusable_medium'] = medium
|
|
||||||
perform_checkin(**checkin_args)
|
|
||||||
else:
|
|
||||||
perform_checkin(**checkin_args)
|
|
||||||
except RequiredQuestionsError as e:
|
except RequiredQuestionsError as e:
|
||||||
return Response({
|
return Response({
|
||||||
'status': 'incomplete',
|
'status': 'incomplete',
|
||||||
@@ -863,18 +764,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
],
|
],
|
||||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
except RequiredMediaExchangeError as e:
|
|
||||||
return Response({
|
|
||||||
'status': 'exchange',
|
|
||||||
'require_attention': op.require_checkin_attention,
|
|
||||||
'checkin_texts': op.checkin_texts,
|
|
||||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
|
||||||
'media_policy': e.media_policy,
|
|
||||||
'media_type': e.media_type,
|
|
||||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
|
||||||
'reason': e.code,
|
|
||||||
'reason_explanation': e.msg,
|
|
||||||
}, status=400)
|
|
||||||
except CheckInError as e:
|
except CheckInError as e:
|
||||||
if not simulate:
|
if not simulate:
|
||||||
op.order.log_action('pretix.event.checkin.denied', data={
|
op.order.log_action('pretix.event.checkin.denied', data={
|
||||||
@@ -1062,8 +951,6 @@ class CheckinRPCRedeemView(views.APIView):
|
|||||||
canceled_supported=True,
|
canceled_supported=True,
|
||||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
legacy_url_support=False,
|
legacy_url_support=False,
|
||||||
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
|
|
||||||
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,10 @@ with scopes_disabled():
|
|||||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
customer = django_filters.CharFilter(field_name='customer__identifier')
|
||||||
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
|
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
|
||||||
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
||||||
# backwards-compatible
|
|
||||||
linked_orderposition = django_filters.NumberFilter(field_name='linked_orderpositions__id')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ReusableMedium
|
model = ReusableMedium
|
||||||
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderpositions', 'linked_giftcard']
|
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
|
||||||
|
|
||||||
|
|
||||||
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||||
@@ -77,7 +75,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
||||||
return self.request.organizer.reusable_media.prefetch_related(
|
return self.request.organizer.reusable_media.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'linked_orderpositions',
|
'linked_orderposition',
|
||||||
queryset=OrderPosition.objects.select_related(
|
queryset=OrderPosition.objects.select_related(
|
||||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
@@ -119,38 +117,14 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
rm = ReusableMedium.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
ReusableMedium.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
||||||
prev_linked_ops_pks = list(rm.linked_orderpositions.values_list("pk", flat=True))
|
|
||||||
inst = serializer.save(identifier=serializer.instance.identifier, type=serializer.instance.type)
|
inst = serializer.save(identifier=serializer.instance.identifier, type=serializer.instance.type)
|
||||||
linked_ops_pks = inst.linked_orderpositions.values_list("pk", flat=True)
|
inst.log_action(
|
||||||
for op_pk in prev_linked_ops_pks:
|
'pretix.reusable_medium.changed',
|
||||||
if op_pk not in linked_ops_pks:
|
user=self.request.user,
|
||||||
inst.log_action(
|
auth=self.request.auth,
|
||||||
'pretix.reusable_medium.linked_orderposition.removed',
|
data=self.request.data,
|
||||||
user=self.request.user,
|
)
|
||||||
auth=self.request.auth,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': op_pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for op_pk in linked_ops_pks:
|
|
||||||
if op_pk not in prev_linked_ops_pks:
|
|
||||||
inst.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': op_pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
data = {k: v for k, v in self.request.data.items() if k not in ('linked_orderposition', 'linked_orderpositions')}
|
|
||||||
if data:
|
|
||||||
inst.log_action(
|
|
||||||
'pretix.reusable_medium.changed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=data,
|
|
||||||
)
|
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
@@ -183,6 +157,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
type=s.validated_data["type"],
|
type=s.validated_data["type"],
|
||||||
identifier=s.validated_data["identifier"],
|
identifier=s.validated_data["identifier"],
|
||||||
)
|
)
|
||||||
|
m.linked_orderposition = None # not relevant for cross-organizer
|
||||||
m.customer = None # not relevant for cross-organizer
|
m.customer = None # not relevant for cross-organizer
|
||||||
s = self.get_serializer(m)
|
s = self.get_serializer(m)
|
||||||
return Response({"result": s.data})
|
return Response({"result": s.data})
|
||||||
@@ -196,7 +171,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
return Response({"result": None})
|
return Response({"result": None})
|
||||||
|
|
||||||
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some performance
|
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
|
||||||
def list(self, request, **kwargs):
|
def list(self, request, **kwargs):
|
||||||
date = serializers.DateTimeField().to_representation(now())
|
date = serializers.DateTimeField().to_representation(now())
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ with scopes_disabled():
|
|||||||
)
|
)
|
||||||
).values('id')
|
).values('id')
|
||||||
|
|
||||||
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderpositions__order_id', flat=True)
|
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
|
||||||
|
|
||||||
mainq = (
|
mainq = (
|
||||||
code
|
code
|
||||||
@@ -1034,7 +1034,7 @@ with scopes_disabled():
|
|||||||
search = django_filters.CharFilter(method='search_qs')
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
|
|
||||||
def search_qs(self, queryset, name, value):
|
def search_qs(self, queryset, name, value):
|
||||||
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderpositions', flat=True)
|
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(secret__istartswith=value)
|
Q(secret__istartswith=value)
|
||||||
| Q(attendee_name_cached__icontains=value)
|
| Q(attendee_name_cached__icontains=value)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ def _get_or_create_server_keypair(organizer):
|
|||||||
|
|
||||||
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
|
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
|
||||||
payload = {
|
payload = {
|
||||||
'iss': eventreverse_absolute(client.organizer, 'presale:organizer.index').rstrip('/'),
|
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'),
|
||||||
'aud': client.client_id,
|
'aud': client.client_id,
|
||||||
'exp': int(expires.timestamp()),
|
'exp': int(expires.timestamp()),
|
||||||
'iat': int(time.time()),
|
'iat': int(time.time()),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
|
|
||||||
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
|
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
def get_answer(op, question_identifier=None):
|
def get_answer(op, question_identifier=None):
|
||||||
@@ -545,7 +545,7 @@ def get_data_fields(event, for_model=None):
|
|||||||
_("Order link"),
|
_("Order link"),
|
||||||
Question.TYPE_STRING,
|
Question.TYPE_STRING,
|
||||||
None,
|
None,
|
||||||
lambda order: eventreverse_absolute(
|
lambda order: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order', kwargs={
|
'presale:event.order', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -560,7 +560,7 @@ def get_data_fields(event, for_model=None):
|
|||||||
_("Ticket link"),
|
_("Ticket link"),
|
||||||
Question.TYPE_STRING,
|
Question.TYPE_STRING,
|
||||||
None,
|
None,
|
||||||
lambda op: eventreverse_absolute(
|
lambda op: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': op.order.code,
|
'order': op.order.code,
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ logger = logging.getLogger('pretix.base.email')
|
|||||||
|
|
||||||
T = TypeVar("T", bound=EmailBackend)
|
T = TypeVar("T", bound=EmailBackend)
|
||||||
|
|
||||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -255,15 +253,12 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
|
|||||||
|
|
||||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||||
ip_addr = ipaddress.ip_address(sa[0])
|
ip_addr = ipaddress.ip_address(sa[0])
|
||||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
|
||||||
if ip_addr.is_multicast:
|
if ip_addr.is_multicast:
|
||||||
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
||||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||||
raise socket.error(f"Request to local address {sa[0]} blocked")
|
raise socket.error(f"Request to local address {sa[0]} blocked")
|
||||||
if ip_addr.is_private:
|
if ip_addr.is_private:
|
||||||
raise socket.error(f"Request to private address {sa[0]} blocked")
|
raise socket.error(f"Request to private address {sa[0]} blocked")
|
||||||
if check_ip4 in _cgnat_net:
|
|
||||||
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
|
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ from ...control.forms.filter import get_all_payment_providers
|
|||||||
from ...helpers import GroupConcat
|
from ...helpers import GroupConcat
|
||||||
from ...helpers.iter import chunked_iterable
|
from ...helpers.iter import chunked_iterable
|
||||||
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
|
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
|
||||||
from ...multidomain.urlreverse import eventreverse_absolute
|
from ...multidomain.urlreverse import build_absolute_uri
|
||||||
from ..exporter import (
|
from ..exporter import (
|
||||||
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||||
)
|
)
|
||||||
@@ -429,7 +429,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
row.append(
|
row.append(
|
||||||
eventreverse_absolute(order.event, 'presale:event.order', kwargs={
|
build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
})
|
})
|
||||||
@@ -855,7 +855,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
row.append(
|
row.append(
|
||||||
eventreverse_absolute(order.event, 'presale:event.order.position', kwargs={
|
build_absolute_uri(order.event, 'presale:event.order.position', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': op.web_secret,
|
'secret': op.web_secret,
|
||||||
'position': op.positionid
|
'position': op.positionid
|
||||||
|
|||||||
@@ -20,13 +20,12 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.db.models import Prefetch
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
|
||||||
|
|
||||||
from ..exporter import ListExporter, OrganizerLevelExportMixin
|
from ..exporter import ListExporter, OrganizerLevelExportMixin
|
||||||
from ..models import OrderPosition, ReusableMedium
|
from ..models import ReusableMedium
|
||||||
from ..signals import register_multievent_data_exporters
|
from ..signals import register_multievent_data_exporters
|
||||||
|
|
||||||
|
|
||||||
@@ -45,9 +44,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
|||||||
media = ReusableMedium.objects.filter(
|
media = ReusableMedium.objects.filter(
|
||||||
organizer=self.organizer,
|
organizer=self.organizer,
|
||||||
).select_related(
|
).select_related(
|
||||||
'customer', 'linked_giftcard',
|
'customer', 'linked_orderposition', 'linked_giftcard',
|
||||||
).prefetch_related(
|
|
||||||
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order"))
|
|
||||||
).order_by('created')
|
).order_by('created')
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
@@ -64,23 +61,18 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
|||||||
yield headers
|
yield headers
|
||||||
yield self.ProgressSetTotal(total=media.count())
|
yield self.ProgressSetTotal(total=media.count())
|
||||||
|
|
||||||
can_read_giftcards = self.permission_holder.has_organizer_permission(self.organizer, 'organizer.giftcards:read')
|
|
||||||
|
|
||||||
for medium in media.iterator(chunk_size=1000):
|
for medium in media.iterator(chunk_size=1000):
|
||||||
giftcard_secret = medium.linked_giftcard.secret if medium.linked_giftcard_id else ''
|
row = [
|
||||||
if giftcard_secret and not can_read_giftcards:
|
|
||||||
giftcard_secret = giftcard_secret[:3] + "…"
|
|
||||||
|
|
||||||
yield [
|
|
||||||
medium.type,
|
medium.type,
|
||||||
medium.identifier,
|
medium.identifier,
|
||||||
_('Yes') if medium.active else _('No'),
|
_('Yes') if medium.active else _('No'),
|
||||||
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
|
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
|
||||||
medium.customer.identifier if medium.customer_id else '',
|
medium.customer.identifier if medium.customer_id else '',
|
||||||
', '.join([f"{op.order.code}-{op.positionid}" for op in medium.linked_orderpositions.all()]),
|
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
|
||||||
giftcard_secret,
|
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
|
||||||
medium.notes,
|
medium.notes,
|
||||||
]
|
]
|
||||||
|
yield row
|
||||||
|
|
||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
return f'{self.organizer.slug}_media'
|
return f'{self.organizer.slug}_media'
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ from django.db.models import QuerySet
|
|||||||
from django.forms import Select, widgets
|
from django.forms import Select, widgets
|
||||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import lazy
|
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
@@ -325,21 +324,16 @@ class WrappedPhonePrefixSelect(Select):
|
|||||||
initial = None
|
initial = None
|
||||||
|
|
||||||
def __init__(self, initial=None):
|
def __init__(self, initial=None):
|
||||||
def _get_choices():
|
choices = [("", "---------")]
|
||||||
choices = [("", "---------")]
|
|
||||||
|
|
||||||
if initial:
|
|
||||||
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
|
||||||
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
|
|
||||||
continue
|
|
||||||
if initial in values:
|
|
||||||
self.initial = "+%d" % prefix
|
|
||||||
break
|
|
||||||
choices += get_phone_prefixes_sorted_and_localized()
|
|
||||||
return choices
|
|
||||||
|
|
||||||
choices = lazy(_get_choices, list)()
|
|
||||||
|
|
||||||
|
if initial:
|
||||||
|
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||||
|
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
|
||||||
|
continue
|
||||||
|
if initial in values:
|
||||||
|
self.initial = "+%d" % prefix
|
||||||
|
break
|
||||||
|
choices += get_phone_prefixes_sorted_and_localized()
|
||||||
super().__init__(choices=choices, attrs={
|
super().__init__(choices=choices, attrs={
|
||||||
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
|
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
|
||||||
'autocomplete': 'tel-country-code',
|
'autocomplete': 'tel-country-code',
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import unicodedata
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -56,8 +58,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
|
|||||||
from pretix.base.signals import register_invoice_renderers
|
from pretix.base.signals import register_invoice_renderers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.reportlab import (
|
from pretix.helpers.reportlab import (
|
||||||
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
|
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
|
||||||
normalize_text, register_ttf_font_if_new, reshaper,
|
reshaper,
|
||||||
)
|
)
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
@@ -257,8 +259,18 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||||
|
|
||||||
def _normalize(self, text):
|
def _normalize(self, text):
|
||||||
# alias kept for plugin compatibility
|
# reportlab does not support unicode combination characters
|
||||||
return normalize_text(text)
|
# It's important we do this before we use ArabicReshaper
|
||||||
|
text = unicodedata.normalize("NFKC", text)
|
||||||
|
|
||||||
|
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||||
|
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||||
|
try:
|
||||||
|
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
|
||||||
|
except:
|
||||||
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
def _upper(self, val):
|
def _upper(self, val):
|
||||||
# We uppercase labels, but not in every language
|
# We uppercase labels, but not in every language
|
||||||
@@ -339,15 +351,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||||
|
|
||||||
def _clean_text(self, text, tags=None):
|
def _clean_text(self, text, tags=None):
|
||||||
# For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
|
return self._normalize(bleach.clean(
|
||||||
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
|
text,
|
||||||
# tags.
|
tags=set(tags) if tags else set()
|
||||||
return self._normalize(
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
bleach.clean(
|
|
||||||
text,
|
|
||||||
tags=set(tags) if tags else set()
|
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PaidMarker(Flowable):
|
class PaidMarker(Flowable):
|
||||||
@@ -398,7 +405,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
invoice_to_top = 52 * mm
|
invoice_to_top = 52 * mm
|
||||||
|
|
||||||
def _draw_invoice_to(self, canvas):
|
def _draw_invoice_to(self, canvas):
|
||||||
p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
|
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
|
||||||
|
style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||||
@@ -409,8 +417,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
invoice_from_top = 17 * mm
|
invoice_from_top = 17 * mm
|
||||||
|
|
||||||
def _draw_invoice_from(self, canvas):
|
def _draw_invoice_from(self, canvas):
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
self.invoice.full_invoice_from,
|
self._clean_text(self.invoice.full_invoice_from),
|
||||||
style=self.stylesheet['InvoiceFrom']
|
style=self.stylesheet['InvoiceFrom']
|
||||||
)
|
)
|
||||||
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
||||||
@@ -540,12 +548,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_event(self, canvas):
|
def _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(txt)
|
txt = str(txt)
|
||||||
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
|
txt = bleach.clean(txt, tags=set()).strip()
|
||||||
|
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
|
|
||||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||||
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
|
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
@@ -563,7 +572,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
else:
|
else:
|
||||||
p_str = shorten(self.invoice.event.name)
|
p_str = shorten(self.invoice.event.name)
|
||||||
|
|
||||||
p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
|
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, self.event_width, self.event_height)
|
p.wrapOn(canvas, self.event_width, self.event_height)
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
||||||
@@ -636,37 +645,39 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
|
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
|
||||||
if type_info_text:
|
if type_info_text:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
type_info_text,
|
type_info_text,
|
||||||
self.stylesheet['WarningBlock']
|
self.stylesheet['WarningBlock']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.custom_field:
|
if self.invoice.custom_field:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
'{}: {}'.format(
|
'{}: {}'.format(
|
||||||
str(self.invoice.event.settings.invoice_address_custom_field),
|
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
|
||||||
self.invoice.custom_field,
|
self._clean_text(self.invoice.custom_field),
|
||||||
),
|
),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.internal_reference:
|
if self.invoice.internal_reference:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
|
||||||
reference=self.invoice.internal_reference,
|
reference=self._clean_text(self.invoice.internal_reference),
|
||||||
),
|
)),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_vat_id:
|
if self.invoice.invoice_to_vat_id:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
|
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
|
||||||
|
self._clean_text(self.invoice.invoice_to_vat_id),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_beneficiary:
|
if self.invoice.invoice_to_beneficiary:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
|
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
|
||||||
|
self._clean_text(self.invoice.invoice_to_beneficiary),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -696,11 +707,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
story = [
|
story = [
|
||||||
NextPageTemplate('FirstPage'),
|
NextPageTemplate('FirstPage'),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
(
|
self._normalize(
|
||||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||||
else pgettext('invoice', 'Invoice')
|
else pgettext('invoice', 'Invoice')
|
||||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
|
||||||
self.stylesheet['Heading1']
|
self.stylesheet['Heading1']
|
||||||
),
|
),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
@@ -722,17 +733,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
]
|
]
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
else:
|
else:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
|
|
||||||
def _group_key(line):
|
def _group_key(line):
|
||||||
@@ -769,8 +780,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
max_height = self.stylesheet['Normal'].leading * 5
|
max_height = self.stylesheet['Normal'].leading * 5
|
||||||
p_style = self.stylesheet['Normal']
|
p_style = self.stylesheet['Normal']
|
||||||
for __ in range(1000):
|
for __ in range(1000):
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
curr_description,
|
self._clean_text(curr_description, tags=['br']),
|
||||||
p_style
|
p_style
|
||||||
)
|
)
|
||||||
h = p.wrap(max_width, doc.height)[1]
|
h = p.wrap(max_width, doc.height)[1]
|
||||||
@@ -851,7 +862,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
# Group together at the end of the invoice
|
# Group together at the end of the invoice
|
||||||
request_show_service_date = period_line
|
request_show_service_date = period_line
|
||||||
elif period_line:
|
elif period_line:
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
period_line,
|
period_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
@@ -863,7 +874,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
net_price=money_filter(net_value, self.invoice.event.currency),
|
net_price=money_filter(net_value, self.invoice.event.currency),
|
||||||
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
||||||
)
|
)
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
single_price_line,
|
single_price_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
@@ -872,11 +883,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
description_p_list.pop(0),
|
description_p_list.pop(0),
|
||||||
str(len(lines)),
|
str(len(lines)),
|
||||||
localize(tax_rate) + " %",
|
localize(tax_rate) + " %",
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
@@ -893,14 +904,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
single_price_line = pgettext('invoice', 'Single price: {price}').format(
|
single_price_line = pgettext('invoice', 'Single price: {price}').format(
|
||||||
price=money_filter(gross_value, self.invoice.event.currency),
|
price=money_filter(gross_value, self.invoice.event.currency),
|
||||||
)
|
)
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
single_price_line,
|
single_price_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
tdata.append((
|
tdata.append((
|
||||||
description_p_list.pop(0),
|
description_p_list.pop(0),
|
||||||
str(len(lines)),
|
str(len(lines)),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
@@ -933,12 +944,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
|
||||||
money_filter(total, self.invoice.event.currency)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
|
||||||
money_filter(total, self.invoice.event.currency)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -947,12 +958,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
pending_sum = self.invoice.order.pending_sum
|
pending_sum = self.invoice.order.pending_sum
|
||||||
if pending_sum != total:
|
if pending_sum != total:
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(pending_sum, self.invoice.event.currency)]
|
[money_filter(pending_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
@@ -969,12 +980,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
s=Sum('amount')
|
s=Sum('amount')
|
||||||
)['s'] or Decimal('0.00')
|
)['s'] or Decimal('0.00')
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
@@ -997,14 +1008,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
story.append(Spacer(1, 10 * mm))
|
story.append(Spacer(1, 10 * mm))
|
||||||
|
|
||||||
if request_show_service_date:
|
if request_show_service_date:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
|
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.payment_provider_text:
|
if self.invoice.payment_provider_text:
|
||||||
story.append(FontFallbackParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
|
self._normalize(self.invoice.payment_provider_text),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1028,10 +1039,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||||
]
|
]
|
||||||
thead = [
|
thead = [
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||||
''
|
''
|
||||||
]
|
]
|
||||||
tdata = [thead]
|
tdata = [thead]
|
||||||
@@ -1042,7 +1053,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
continue
|
continue
|
||||||
tax = taxvalue_map[idx]
|
tax = taxvalue_map[idx]
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||||
money_filter(gross - tax, self.invoice.event.currency),
|
money_filter(gross - tax, self.invoice.event.currency),
|
||||||
money_filter(gross, self.invoice.event.currency),
|
money_filter(gross, self.invoice.event.currency),
|
||||||
money_filter(tax, self.invoice.event.currency),
|
money_filter(tax, self.invoice.event.currency),
|
||||||
@@ -1061,7 +1072,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
table.setStyle(TableStyle(tstyledata))
|
table.setStyle(TableStyle(tstyledata))
|
||||||
story.append(Spacer(5 * mm, 5 * mm))
|
story.append(Spacer(5 * mm, 5 * mm))
|
||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||||
table
|
table
|
||||||
]))
|
]))
|
||||||
|
|
||||||
@@ -1078,7 +1089,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
net = gross - tax
|
net = gross - tax
|
||||||
|
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||||
fmt(net), fmt(gross), fmt(tax), ''
|
fmt(net), fmt(gross), fmt(tax), ''
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1087,13 +1098,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
Spacer(1, height=2 * mm),
|
Spacer(1, height=2 * mm),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
pgettext(
|
self._normalize(pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, this corresponds to:'
|
'{date}, this corresponds to:'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
|
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
),
|
),
|
||||||
Spacer(1, height=3 * mm),
|
Spacer(1, height=3 * mm),
|
||||||
@@ -1102,14 +1113,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||||
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
||||||
story.append(Spacer(1, 5 * mm))
|
story.append(Spacer(1, 5 * mm))
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(self._normalize(
|
||||||
pgettext(
|
pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, the invoice total corresponds to {total}.'
|
'{date}, the invoice total corresponds to {total}.'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||||
total=fmt(foreign_total)),
|
total=fmt(foreign_total))),
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1151,8 +1162,11 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
def _draw_invoice_from(self, canvas):
|
def _draw_invoice_from(self, canvas):
|
||||||
if not self.invoice.address_invoice_from:
|
if not self.invoice.address_invoice_from:
|
||||||
return
|
return
|
||||||
c = self.invoice.address_invoice_from.strip().split('\n')
|
c = [
|
||||||
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
|
self._clean_text(l)
|
||||||
|
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||||
|
]
|
||||||
|
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
|
||||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||||
super()._draw_invoice_from(canvas)
|
super()._draw_invoice_from(canvas)
|
||||||
@@ -1211,8 +1225,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
||||||
]
|
]
|
||||||
|
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
date_format(self.invoice.date, "DATE_FORMAT"),
|
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
|
||||||
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
||||||
)
|
)
|
||||||
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
||||||
@@ -1269,7 +1283,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
|
|||||||
i = []
|
i = []
|
||||||
|
|
||||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||||
i.append(PlainTextParagraph(
|
i.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Event date: {date_range}').format(
|
pgettext('invoice', 'Event date: {date_range}').format(
|
||||||
date_range=self.invoice.event.get_date_range_display(),
|
date_range=self.invoice.event.get_date_range_display(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Do nothing. Useful for startup performance testing."
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
pass
|
|
||||||
@@ -44,8 +44,7 @@ class Command(Parent):
|
|||||||
# Start the vite server in the background
|
# Start the vite server in the background
|
||||||
vite_server = subprocess.Popen(
|
vite_server = subprocess.Popen(
|
||||||
["npm", "run", "dev:control"],
|
["npm", "run", "dev:control"],
|
||||||
cwd=Path(__file__).parent.parent.parent.parent.parent,
|
cwd=Path(__file__).parent.parent.parent.parent.parent
|
||||||
stdin=subprocess.DEVNULL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def cleanup():
|
def cleanup():
|
||||||
|
|||||||
+14
-20
@@ -26,7 +26,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
class BaseMediaType:
|
class BaseMediaType:
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
medium_created_from_unknown_supported = False
|
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
supports_giftcard = False
|
supports_giftcard = False
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ class BaseMediaType:
|
|||||||
def is_active(self, organizer):
|
def is_active(self, organizer):
|
||||||
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
|
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
|
||||||
|
|
||||||
def handle_unknown(self, organizer, identifier, user, auth, force_create=False):
|
def handle_unknown(self, organizer, identifier, user, auth):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def handle_new(self, organizer, medium, user, auth):
|
def handle_new(self, organizer, medium, user, auth):
|
||||||
@@ -89,32 +88,23 @@ class NfcUidMediaType(BaseMediaType):
|
|||||||
verbose_name = _('NFC UID-based')
|
verbose_name = _('NFC UID-based')
|
||||||
icon = 'pretixbase/img/media/nfc_uid.svg'
|
icon = 'pretixbase/img/media/nfc_uid.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
medium_created_from_unknown_supported = True
|
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = True
|
supports_orderposition = False
|
||||||
|
|
||||||
def handle_unknown(self, organizer, identifier, user, auth, force_create=False):
|
def handle_unknown(self, organizer, identifier, user, auth):
|
||||||
from pretix.base.models import GiftCard, ReusableMedium
|
from pretix.base.models import GiftCard, ReusableMedium
|
||||||
|
|
||||||
create_giftcard = organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool)
|
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool):
|
||||||
if create_giftcard or force_create:
|
|
||||||
if identifier.startswith("08"):
|
if identifier.startswith("08"):
|
||||||
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
|
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
|
||||||
# UIDs on every read, so they won't be useful.
|
# UIDs on every read, so they won't be useful.
|
||||||
return
|
return
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if create_giftcard:
|
gc = GiftCard.objects.create(
|
||||||
gc = GiftCard.objects.create(
|
issuer=organizer,
|
||||||
issuer=organizer,
|
expires=organizer.default_gift_card_expiry,
|
||||||
expires=organizer.default_gift_card_expiry,
|
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
|
||||||
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
|
)
|
||||||
)
|
|
||||||
gc.log_action(
|
|
||||||
'pretix.giftcards.created',
|
|
||||||
user=user, auth=auth,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
gc = None
|
|
||||||
m = ReusableMedium.objects.create(
|
m = ReusableMedium.objects.create(
|
||||||
type=self.identifier,
|
type=self.identifier,
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
@@ -126,6 +116,10 @@ class NfcUidMediaType(BaseMediaType):
|
|||||||
'pretix.reusable_medium.created.auto',
|
'pretix.reusable_medium.created.auto',
|
||||||
user=user, auth=auth,
|
user=user, auth=auth,
|
||||||
)
|
)
|
||||||
|
gc.log_action(
|
||||||
|
'pretix.giftcards.created',
|
||||||
|
user=user, auth=auth,
|
||||||
|
)
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
@@ -135,7 +129,7 @@ class NfcMf0aesMediaType(BaseMediaType):
|
|||||||
icon = 'pretixbase/img/media/nfc_secure.svg'
|
icon = 'pretixbase/img/media/nfc_secure.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = True
|
supports_orderposition = False
|
||||||
|
|
||||||
def handle_new(self, organizer, medium, user, auth):
|
def handle_new(self, organizer, medium, user, auth):
|
||||||
from pretix.base.models import GiftCard
|
from pretix.base.models import GiftCard
|
||||||
|
|||||||
@@ -282,12 +282,10 @@ def metric_values():
|
|||||||
|
|
||||||
# Throwaway metrics
|
# Throwaway metrics
|
||||||
exact_tables = [
|
exact_tables = [
|
||||||
Order, Invoice, Event, Organizer
|
Order, OrderPosition, Invoice, Event, Organizer
|
||||||
]
|
]
|
||||||
for m in apps.get_models(): # Count all models
|
for m in apps.get_models(): # Count all models
|
||||||
if issubclass(m, OrderPosition):
|
if any(issubclass(m, p) for p in exact_tables):
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.all.count()
|
|
||||||
elif any(issubclass(m, p) for p in exact_tables):
|
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
|
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
|
||||||
else:
|
else:
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ class LocaleMiddleware(MiddlewareMixin):
|
|||||||
|
|
||||||
def process_request(self, request: HttpRequest):
|
def process_request(self, request: HttpRequest):
|
||||||
language = get_language_from_request(request)
|
language = get_language_from_request(request)
|
||||||
region = None
|
|
||||||
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it
|
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it
|
||||||
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
|
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
|
||||||
# set and can be taken into account for the decision.
|
# set and can be taken into account for the decision.
|
||||||
@@ -95,16 +94,15 @@ class LocaleMiddleware(MiddlewareMixin):
|
|||||||
if '-' not in language and settings_holder.settings.region:
|
if '-' not in language and settings_holder.settings.region:
|
||||||
language += '-' + settings_holder.settings.region
|
language += '-' + settings_holder.settings.region
|
||||||
if settings_holder.settings.region:
|
if settings_holder.settings.region:
|
||||||
region = settings_holder.settings.region
|
set_region(settings_holder.settings.region)
|
||||||
else:
|
else:
|
||||||
gs = global_settings_object(request)
|
gs = global_settings_object(request)
|
||||||
if '-' not in language and gs.settings.region:
|
if '-' not in language and gs.settings.region:
|
||||||
language += '-' + gs.settings.region
|
language += '-' + gs.settings.region
|
||||||
if gs.settings.region:
|
if gs.settings.region:
|
||||||
region = gs.settings.region
|
set_region(gs.settings.region)
|
||||||
|
|
||||||
translation.activate(language)
|
translation.activate(language)
|
||||||
set_region(region)
|
|
||||||
request.LANGUAGE_CODE = get_language_without_region()
|
request.LANGUAGE_CODE = get_language_without_region()
|
||||||
|
|
||||||
tzname = None
|
tzname = None
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 4.2.26 on 2025-11-24 11:32
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0299_itemprogramtime_location"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="reusablemedium",
|
|
||||||
name="claim_token",
|
|
||||||
field=models.CharField(max_length=200, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="reusablemedium",
|
|
||||||
name="label",
|
|
||||||
field=models.CharField(max_length=200, null=True),
|
|
||||||
),
|
|
||||||
# use temporary related_name "linked_mediums" for ManyToManyField, so we can migrate existing data
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="reusablemedium",
|
|
||||||
name="linked_orderpositions",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
related_name="linked_mediums", to="pretixbase.orderposition"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunSQL(
|
|
||||||
sql="INSERT INTO pretixbase_reusablemedium_linked_orderpositions (reusablemedium_id, orderposition_id) SELECT id, linked_orderposition_id FROM pretixbase_reusablemedium WHERE linked_orderposition_id IS NOT NULL;",
|
|
||||||
reverse_sql="DELETE FROM pretixbase_reusablemedium_linked_orderpositions;",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Generated by Django 4.2.26 on 2025-11-24 11:32
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def reverse(apps, schema_editor):
|
|
||||||
ReusableMedium = apps.get_model('pretixbase', 'ReusableMedium')
|
|
||||||
|
|
||||||
qs = ReusableMedium.linked_orderpositions.through.objects
|
|
||||||
objs = []
|
|
||||||
# get last added orderposition from linked_orderpositions
|
|
||||||
for rm_id, op_id in qs.filter(id__in=qs.values("reusablemedium_id").annotate(max_id=models.Max('id')).values('max_id')).values_list("reusablemedium_id", "orderposition_id"):
|
|
||||||
obj = ReusableMedium(
|
|
||||||
id=rm_id,
|
|
||||||
linked_orderposition_id=op_id,
|
|
||||||
)
|
|
||||||
objs.append(obj)
|
|
||||||
|
|
||||||
ReusableMedium.objects.bulk_update(objs, ['linked_orderposition_id'])
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0300_add_reusablemedium_label"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
# according to the docs, UPDATE FROM should run similarly on sqlite and postgres, but I could not get it to work
|
|
||||||
# so roll back the data migration with code before deleting data from through-table in 0297
|
|
||||||
migrations.RunPython(migrations.RunPython.noop, reverse),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="reusablemedium",
|
|
||||||
name="linked_orderposition",
|
|
||||||
),
|
|
||||||
# change related_name for new ManyToManyField to previously used linked_media
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="reusablemedium",
|
|
||||||
name="linked_orderpositions",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
related_name="linked_media", to="pretixbase.orderposition"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -57,7 +57,7 @@ from django_otp.models import Device
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
from ...helpers.countries import FastCountryField
|
from ...helpers.countries import FastCountryField
|
||||||
from ...helpers.u2f import pub_key_from_der, websafe_decode
|
from ...helpers.u2f import pub_key_from_der, websafe_decode
|
||||||
@@ -378,7 +378,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
{
|
{
|
||||||
'user': self,
|
'user': self,
|
||||||
'messages': msg,
|
'messages': msg,
|
||||||
'url': mainreverse_absolute('control:user.settings'),
|
'url': build_absolute_uri('control:user.settings'),
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
},
|
},
|
||||||
event=None,
|
event=None,
|
||||||
@@ -466,7 +466,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
{
|
{
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'user': self,
|
'user': self,
|
||||||
'url': (mainreverse_absolute('control:auth.forgot.recover')
|
'url': (build_absolute_uri('control:auth.forgot.recover')
|
||||||
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
||||||
},
|
},
|
||||||
None, locale=self.locale, user=self
|
None, locale=self.locale, user=self
|
||||||
|
|||||||
@@ -346,14 +346,11 @@ class Checkin(models.Model):
|
|||||||
REASON_INCOMPLETE = 'incomplete'
|
REASON_INCOMPLETE = 'incomplete'
|
||||||
REASON_ALREADY_REDEEMED = 'already_redeemed'
|
REASON_ALREADY_REDEEMED = 'already_redeemed'
|
||||||
REASON_AMBIGUOUS = 'ambiguous'
|
REASON_AMBIGUOUS = 'ambiguous'
|
||||||
REASON_MEDIUM_INVALID = 'medium_invalid'
|
|
||||||
REASON_MEDIUM_EXISTS = 'medium_exists'
|
|
||||||
REASON_ERROR = 'error'
|
REASON_ERROR = 'error'
|
||||||
REASON_BLOCKED = 'blocked'
|
REASON_BLOCKED = 'blocked'
|
||||||
REASON_UNAPPROVED = 'unapproved'
|
REASON_UNAPPROVED = 'unapproved'
|
||||||
REASON_INVALID_TIME = 'invalid_time'
|
REASON_INVALID_TIME = 'invalid_time'
|
||||||
REASON_ANNULLED = 'annulled'
|
REASON_ANNULLED = 'annulled'
|
||||||
REASON_ALREADY_EXCHANGED = 'already_exchanged'
|
|
||||||
REASONS = (
|
REASONS = (
|
||||||
(REASON_CANCELED, _('Order canceled')),
|
(REASON_CANCELED, _('Order canceled')),
|
||||||
(REASON_INVALID, _('Unknown ticket')),
|
(REASON_INVALID, _('Unknown ticket')),
|
||||||
@@ -369,9 +366,6 @@ class Checkin(models.Model):
|
|||||||
(REASON_UNAPPROVED, _('Order not approved')),
|
(REASON_UNAPPROVED, _('Order not approved')),
|
||||||
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
|
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
|
||||||
(REASON_ANNULLED, _('Check-in annulled')),
|
(REASON_ANNULLED, _('Check-in annulled')),
|
||||||
(REASON_ALREADY_EXCHANGED, _('Ticket already exchanged')),
|
|
||||||
(REASON_MEDIUM_INVALID, _('Reusable medium invalid')),
|
|
||||||
(REASON_MEDIUM_EXISTS, _('Reusable medium already exists')),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
successful = models.BooleanField(
|
successful = models.BooleanField(
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class Customer(LoggedModel):
|
|||||||
|
|
||||||
def send_security_notice(self, message, email=None):
|
def send_security_notice(self, message, email=None):
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with language(self.locale):
|
with language(self.locale):
|
||||||
@@ -178,7 +178,7 @@ class Customer(LoggedModel):
|
|||||||
{
|
{
|
||||||
**self.get_email_context(),
|
**self.get_email_context(),
|
||||||
'message': str(message),
|
'message': str(message),
|
||||||
'url': eventreverse_absolute(self.organizer, 'presale:organizer.customer.index')
|
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
|
||||||
},
|
},
|
||||||
customer=self,
|
customer=self,
|
||||||
organizer=self.organizer,
|
organizer=self.organizer,
|
||||||
@@ -299,12 +299,12 @@ class Customer(LoggedModel):
|
|||||||
|
|
||||||
def send_activation_mail(self):
|
def send_activation_mail(self):
|
||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.forms.customer import TokenGenerator
|
from pretix.presale.forms.customer import TokenGenerator
|
||||||
|
|
||||||
ctx = self.get_email_context()
|
ctx = self.get_email_context()
|
||||||
token = TokenGenerator().make_token(self)
|
token = TokenGenerator().make_token(self)
|
||||||
ctx['url'] = eventreverse_absolute(
|
ctx['url'] = build_absolute_uri(
|
||||||
self.organizer,
|
self.organizer,
|
||||||
'presale:organizer.customer.activate'
|
'presale:organizer.customer.activate'
|
||||||
) + '?id=' + self.identifier + '&token=' + token
|
) + '?id=' + self.identifier + '&token=' + token
|
||||||
@@ -395,7 +395,7 @@ class AttendeeProfile(models.Model):
|
|||||||
self.company,
|
self.company,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
||||||
self.country.name if self.country else None,
|
self.country.name,
|
||||||
]
|
]
|
||||||
for a in self.answers:
|
for a in self.answers:
|
||||||
value = a.get('value')
|
value = a.get('value')
|
||||||
|
|||||||
@@ -724,7 +724,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def social_image(self):
|
def social_image(self):
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
img = None
|
img = None
|
||||||
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
|
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
|
||||||
@@ -742,7 +742,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
logger.exception(f'Failed to create thumbnail of {logo_file}')
|
logger.exception(f'Failed to create thumbnail of {logo_file}')
|
||||||
img = default_storage.url(logo_file)
|
img = default_storage.url(logo_file)
|
||||||
if img:
|
if img:
|
||||||
return urljoin(eventreverse_absolute(self, 'presale:event.index'), img)
|
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||||
|
|
||||||
def _seats(self, ignore_voucher=None):
|
def _seats(self, ignore_voucher=None):
|
||||||
from .seating import Seat
|
from .seating import Seat
|
||||||
@@ -883,8 +883,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
|
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
|
||||||
)
|
)
|
||||||
|
|
||||||
is_cross_organizer = other.organizer_id != self.organizer_id
|
|
||||||
|
|
||||||
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
|
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
|
||||||
# Plugins can create data in installed() hook based on existing data of the event.
|
# Plugins can create data in installed() hook based on existing data of the event.
|
||||||
# Calling set_active_plugins() results in defaults being created while actually data
|
# Calling set_active_plugins() results in defaults being created while actually data
|
||||||
@@ -913,15 +911,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
for emv in EventMetaValue.objects.filter(event=other):
|
for emv in EventMetaValue.objects.filter(event=other):
|
||||||
emv.pk = None
|
emv.pk = None
|
||||||
emv.event = self
|
emv.event = self
|
||||||
if is_cross_organizer:
|
|
||||||
try:
|
|
||||||
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
|
|
||||||
except EventMetaProperty.DoesNotExist:
|
|
||||||
meta_prop = emv.property
|
|
||||||
meta_prop.pk = None
|
|
||||||
meta_prop.organizer = self.organizer
|
|
||||||
meta_prop.save(force_insert=True)
|
|
||||||
emv.property = meta_prop
|
|
||||||
emv.save(force_insert=True)
|
emv.save(force_insert=True)
|
||||||
|
|
||||||
for fl in EventFooterLink.objects.filter(event=other):
|
for fl in EventFooterLink.objects.filter(event=other):
|
||||||
@@ -975,13 +964,13 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if i.tax_rule_id:
|
if i.tax_rule_id:
|
||||||
i.tax_rule = tax_map[i.tax_rule_id]
|
i.tax_rule = tax_map[i.tax_rule_id]
|
||||||
|
|
||||||
if i.grant_membership_type and is_cross_organizer:
|
if i.grant_membership_type and other.organizer_id != self.organizer_id:
|
||||||
i.grant_membership_type = None
|
i.grant_membership_type = None
|
||||||
|
|
||||||
i.save() # no force_insert since i.picture.save could have already inserted
|
i.save() # no force_insert since i.picture.save could have already inserted
|
||||||
i.log_action('pretix.object.cloned')
|
i.log_action('pretix.object.cloned')
|
||||||
|
|
||||||
if require_membership_types and not is_cross_organizer:
|
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||||
i.require_membership_types.set(require_membership_types)
|
i.require_membership_types.set(require_membership_types)
|
||||||
|
|
||||||
if not i.all_sales_channels:
|
if not i.all_sales_channels:
|
||||||
@@ -996,7 +985,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
v._prefetched_objects_cache = {}
|
v._prefetched_objects_cache = {}
|
||||||
v.save(force_insert=True)
|
v.save(force_insert=True)
|
||||||
|
|
||||||
if require_membership_types and not is_cross_organizer:
|
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||||
v.require_membership_types.set(require_membership_types)
|
v.require_membership_types.set(require_membership_types)
|
||||||
if not v.all_sales_channels:
|
if not v.all_sales_channels:
|
||||||
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||||
@@ -1880,8 +1869,6 @@ class EventMetaValue(LoggedModel):
|
|||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.event and self.event.organizer != self.property.organizer:
|
|
||||||
raise ValidationError(_("Property and event must belong to the same organizer."))
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|||||||
@@ -452,16 +452,11 @@ class Item(LoggedModel):
|
|||||||
MEDIA_POLICY_REUSE = 'reuse'
|
MEDIA_POLICY_REUSE = 'reuse'
|
||||||
MEDIA_POLICY_NEW = 'new'
|
MEDIA_POLICY_NEW = 'new'
|
||||||
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
||||||
MEDIA_POLICY_APPEND = 'append'
|
|
||||||
MEDIA_POLICY_APPEND_OR_NEW = 'append_or_new'
|
|
||||||
MEDIA_POLICIES = (
|
MEDIA_POLICIES = (
|
||||||
(None, _("Don't use reusable media, use regular one-off tickets")),
|
(None, _("Don't use re-usable media, use regular one-off tickets")),
|
||||||
|
(MEDIA_POLICY_REUSE, _('Require an existing medium to be re-used')),
|
||||||
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
|
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
|
||||||
(MEDIA_POLICY_REUSE, _('Require an existing medium to be reused, replacing any previous tickets')),
|
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used')),
|
||||||
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used, replacing any previous tickets')),
|
|
||||||
(MEDIA_POLICY_APPEND, _('Require an existing medium to be reused, adding to any previous tickets')),
|
|
||||||
(MEDIA_POLICY_APPEND_OR_NEW,
|
|
||||||
_('Require either an existing or a new medium to be used, adding to any previous tickets')),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ItemQuerySetManager()
|
objects = ItemQuerySetManager()
|
||||||
@@ -774,7 +769,7 @@ class Item(LoggedModel):
|
|||||||
null=True, blank=True, max_length=16,
|
null=True, blank=True, max_length=16,
|
||||||
verbose_name=_('Reusable media policy'),
|
verbose_name=_('Reusable media policy'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'If this product should be stored on a reusable physical medium, you can attach a physical media policy. '
|
'If this product should be stored on a re-usable physical medium, you can attach a physical media policy. '
|
||||||
'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
|
'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
|
||||||
'renewable season tickets or re-chargeable gift card wristbands. '
|
'renewable season tickets or re-chargeable gift card wristbands. '
|
||||||
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
|
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
|
||||||
@@ -783,7 +778,7 @@ class Item(LoggedModel):
|
|||||||
media_type = models.CharField(
|
media_type = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
choices=[(None, _("Don't use reusable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
|
choices=[(None, _("Don't use re-usable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
|
||||||
verbose_name=_('Reusable media type'),
|
verbose_name=_('Reusable media type'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Select the type of physical medium that should be used for this product. Note that not all media types '
|
'Select the type of physical medium that should be used for this product. Note that not all media types '
|
||||||
@@ -1000,11 +995,6 @@ class Item(LoggedModel):
|
|||||||
raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
|
raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
|
||||||
if not mt.supports_giftcard and issue_giftcard:
|
if not mt.supports_giftcard and issue_giftcard:
|
||||||
raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
|
raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
|
||||||
if media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
|
||||||
if not mt.medium_created_by_server and not mt.medium_created_from_unknown_supported:
|
|
||||||
raise ValidationError(_('The selected media type requires all media to be registered in the system '
|
|
||||||
'prior to their usage. Therefore, the selected media policy does not make '
|
|
||||||
'sense for this media type.'))
|
|
||||||
if issue_giftcard:
|
if issue_giftcard:
|
||||||
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
|
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
|
||||||
'gift cards for some reusable media types can be created or re-charged directly '
|
'gift cards for some reusable media types can be created or re-charged directly '
|
||||||
@@ -2230,7 +2220,7 @@ class Quota(LoggedModel):
|
|||||||
class ItemMetaProperty(LoggedModel):
|
class ItemMetaProperty(LoggedModel):
|
||||||
"""
|
"""
|
||||||
An event can have ItemMetaProperty objects attached to define meta information fields
|
An event can have ItemMetaProperty objects attached to define meta information fields
|
||||||
for its items. This information can be reused for example in ticket layouts.
|
for its items. This information can be re-used for example in ticket layouts.
|
||||||
|
|
||||||
:param event: The event this property is defined for.
|
:param event: The event this property is defined for.
|
||||||
:type event: Event
|
:type event: Event
|
||||||
|
|||||||
@@ -72,16 +72,6 @@ class ReusableMedium(LoggedModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
|
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
|
||||||
)
|
)
|
||||||
claim_token = models.CharField(
|
|
||||||
max_length=200,
|
|
||||||
verbose_name=pgettext_lazy('reusable_medium', 'Claim token'),
|
|
||||||
null=True, blank=True
|
|
||||||
)
|
|
||||||
label = models.CharField(
|
|
||||||
max_length=200,
|
|
||||||
verbose_name=pgettext_lazy('reusable_medium', 'Label'),
|
|
||||||
null=True, blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
active = models.BooleanField(
|
active = models.BooleanField(
|
||||||
verbose_name=_('Active'),
|
verbose_name=_('Active'),
|
||||||
@@ -99,14 +89,12 @@ class ReusableMedium(LoggedModel):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
verbose_name=_('Customer account'),
|
verbose_name=_('Customer account'),
|
||||||
)
|
)
|
||||||
linked_orderpositions = models.ManyToManyField(
|
linked_orderposition = models.ForeignKey(
|
||||||
OrderPosition,
|
OrderPosition,
|
||||||
|
null=True, blank=True,
|
||||||
related_name='linked_media',
|
related_name='linked_media',
|
||||||
verbose_name=_('Linked tickets'),
|
on_delete=models.SET_NULL,
|
||||||
help_text=_(
|
verbose_name=_('Linked ticket'),
|
||||||
'If you link to more than one ticket, make sure there is no overlap in validity. '
|
|
||||||
'If multiple tickets are valid at once, this will lead to failed check-ins.'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
linked_giftcard = models.ForeignKey(
|
linked_giftcard = models.ForeignKey(
|
||||||
GiftCard,
|
GiftCard,
|
||||||
@@ -129,10 +117,7 @@ class ReusableMedium(LoggedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
return self.expires and self.expires < now()
|
return self.expires and self.expires > now()
|
||||||
|
|
||||||
def touch(self):
|
|
||||||
self.save(update_fields=['updated'])
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("identifier", "type", "organizer"),)
|
unique_together = (("identifier", "type", "organizer"),)
|
||||||
|
|||||||
@@ -354,60 +354,38 @@ class Order(LockModel, LoggedModel):
|
|||||||
def _transaction_key_reset(self):
|
def _transaction_key_reset(self):
|
||||||
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
|
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
|
|
||||||
# Expects to be called in a transaction
|
|
||||||
from . import (
|
|
||||||
GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not transaction.get_connection().in_atomic_block:
|
|
||||||
raise Exception('gracefully_delete_bulk should only be called in atomic transaction!')
|
|
||||||
|
|
||||||
logs_create = []
|
|
||||||
for o in orders:
|
|
||||||
if not o.testmode:
|
|
||||||
raise TypeError("Only test mode orders can be deleted.")
|
|
||||||
order_gracefully_delete.send(event, order=o)
|
|
||||||
logs_create.append(o.log_action(
|
|
||||||
'pretix.event.order.deleted', user=user, auth=auth,
|
|
||||||
data={
|
|
||||||
'code': o.code,
|
|
||||||
},
|
|
||||||
save=False,
|
|
||||||
))
|
|
||||||
LogEntry.bulk_create_and_postprocess(logs_create)
|
|
||||||
|
|
||||||
voucher_ids = OrderPosition.objects.filter(
|
|
||||||
order__in=orders,
|
|
||||||
voucher__isnull=False
|
|
||||||
).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
|
|
||||||
voucher_usages = Counter(voucher_ids)
|
|
||||||
for v_id, usage_count in voucher_usages.items():
|
|
||||||
Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
|
|
||||||
|
|
||||||
GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
|
|
||||||
GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
|
|
||||||
GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
|
|
||||||
GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
|
|
||||||
Membership.objects.filter(granted_in__order__in=orders, testmode=True).update(granted_in=None)
|
|
||||||
OrderPosition.all.filter(order__in=orders, addon_to__isnull=False).delete()
|
|
||||||
OrderPosition.all.filter(order__in=orders).delete()
|
|
||||||
OrderFee.all.filter(order__in=orders).delete()
|
|
||||||
Transaction.objects.filter(order__in=orders).delete()
|
|
||||||
OrderRefund.objects.filter(order__in=orders).delete()
|
|
||||||
OrderPayment.objects.filter(order__in=orders).delete()
|
|
||||||
if isinstance(orders, models.QuerySet):
|
|
||||||
orders.delete()
|
|
||||||
else:
|
|
||||||
Order.objects.filter(pk__in=[o.pk for o in orders]).delete()
|
|
||||||
event.cache.delete('complain_testmode_orders')
|
|
||||||
|
|
||||||
def gracefully_delete(self, user=None, auth=None):
|
def gracefully_delete(self, user=None, auth=None):
|
||||||
|
from . import GiftCard, GiftCardTransaction, Membership, Voucher
|
||||||
|
|
||||||
if not self.testmode:
|
if not self.testmode:
|
||||||
raise TypeError("Only test mode orders can be deleted.")
|
raise TypeError("Only test mode orders can be deleted.")
|
||||||
|
self.log_action(
|
||||||
|
'pretix.event.order.deleted', user=user, auth=auth,
|
||||||
|
data={
|
||||||
|
'code': self.code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Order.gracefully_delete_bulk(self.event, Order.objects.filter(pk=self.pk), user, auth)
|
order_gracefully_delete.send(self.event, order=self)
|
||||||
|
|
||||||
|
if self.status != Order.STATUS_CANCELED:
|
||||||
|
for position in self.positions.all():
|
||||||
|
if position.voucher:
|
||||||
|
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||||
|
|
||||||
|
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None)
|
||||||
|
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
|
||||||
|
GiftCardTransaction.objects.filter(order=self).update(order=None)
|
||||||
|
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
|
||||||
|
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None)
|
||||||
|
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
|
||||||
|
OrderPosition.all.filter(order=self).delete()
|
||||||
|
OrderFee.all.filter(order=self).delete()
|
||||||
|
Transaction.objects.filter(order=self).delete()
|
||||||
|
self.refunds.all().delete()
|
||||||
|
self.payments.all().delete()
|
||||||
|
self.event.cache.delete('complain_testmode_orders')
|
||||||
|
self.delete()
|
||||||
|
|
||||||
def email_confirm_secret(self):
|
def email_confirm_secret(self):
|
||||||
return self.tagged_secret("email_confirm", 9)
|
return self.tagged_secret("email_confirm", 9)
|
||||||
@@ -1697,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
|||||||
self.company,
|
self.company,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
||||||
self.country.name if self.country else ''
|
self.country.name
|
||||||
]
|
]
|
||||||
lines = [r.strip() for r in lines if r]
|
lines = [r.strip() for r in lines if r]
|
||||||
return '\n'.join(lines).strip()
|
return '\n'.join(lines).strip()
|
||||||
@@ -3438,7 +3416,7 @@ class InvoiceAddress(models.Model):
|
|||||||
self.name,
|
self.name,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
||||||
self.country.name if self.country else '',
|
self.country.name,
|
||||||
self.vat_id,
|
self.vat_id,
|
||||||
self.custom_field,
|
self.custom_field,
|
||||||
self.internal_reference,
|
self.internal_reference,
|
||||||
|
|||||||
@@ -118,10 +118,7 @@ class SeatingPlan(LoggedModel):
|
|||||||
for zi, z in enumerate(self.layout_data['zones']):
|
for zi, z in enumerate(self.layout_data['zones']):
|
||||||
zpos = (z['position']['x'], z['position']['y'])
|
zpos = (z['position']['x'], z['position']['y'])
|
||||||
for ri, r in enumerate(z['rows']):
|
for ri, r in enumerate(z['rows']):
|
||||||
rpos = (
|
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y'])
|
||||||
zpos[0] + r.get('position', {}).get('x', 0),
|
|
||||||
zpos[1] + r.get('position', {}).get('y', 0),
|
|
||||||
)
|
|
||||||
row_label = None
|
row_label = None
|
||||||
if r.get('row_label'):
|
if r.get('row_label'):
|
||||||
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
|
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
|
||||||
@@ -150,8 +147,8 @@ class SeatingPlan(LoggedModel):
|
|||||||
zone=z['name'],
|
zone=z['name'],
|
||||||
category=s['category'],
|
category=s['category'],
|
||||||
sorting_rank=rank,
|
sorting_rank=rank,
|
||||||
x=rpos[0] + s.get('position', {}).get('x', 0),
|
x=rpos[0] + s['position']['x'],
|
||||||
y=rpos[1] + s.get('position', {}).get('y', 0),
|
y=rpos[1] + s['position']['y'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
from pretix.base.models import Event, LogEntry
|
from pretix.base.models import Event, LogEntry
|
||||||
from pretix.base.signals import register_notification_types
|
from pretix.base.signals import register_notification_types
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_ALL_TYPES = None
|
_ALL_TYPES = None
|
||||||
@@ -170,7 +170,7 @@ class ParametrizedOrderNotificationType(NotificationType):
|
|||||||
def build_notification(self, logentry: LogEntry):
|
def build_notification(self, logentry: LogEntry):
|
||||||
order = logentry.content_object
|
order = logentry.content_object
|
||||||
|
|
||||||
order_url = mainreverse_absolute(
|
order_url = build_absolute_uri(
|
||||||
'control:event.order',
|
'control:event.order',
|
||||||
kwargs={
|
kwargs={
|
||||||
'organizer': logentry.event.organizer.slug,
|
'organizer': logentry.event.organizer.slug,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ from pretix.helpers import OF_SELF
|
|||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import format_map
|
||||||
from pretix.helpers.money import DecimalTextInput
|
from pretix.helpers.money import DecimalTextInput
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.views import get_cart
|
from pretix.presale.views import get_cart
|
||||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ class BasePaymentProvider:
|
|||||||
|
|
||||||
if not self.settings.get('_hidden_seed'):
|
if not self.settings.get('_hidden_seed'):
|
||||||
self.settings.set('_hidden_seed', get_random_string(64))
|
self.settings.set('_hidden_seed', get_random_string(64))
|
||||||
hidden_url = eventreverse_absolute(self.event, 'presale:event.payment.unlock', kwargs={
|
hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={
|
||||||
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
|
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -372,11 +372,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": _("Atlantis"),
|
"editor_sample": _("Atlantis"),
|
||||||
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
|
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
|
||||||
}),
|
}),
|
||||||
("invoice_custom_field", {
|
|
||||||
"label": _("Invoice custom recipient field"),
|
|
||||||
"editor_sample": _("Custom recipient field"),
|
|
||||||
"evaluate": lambda op, order, ev: order.invoice_address.custom_field if getattr(order, 'invoice_address', None) else ''
|
|
||||||
}),
|
|
||||||
("addons", {
|
("addons", {
|
||||||
"label": _("List of Add-Ons"),
|
"label": _("List of Add-Ons"),
|
||||||
"editor_sample": _("Add-on 1\n2x Add-on 2"),
|
"editor_sample": _("Add-on 1\n2x Add-on 2"),
|
||||||
@@ -1067,7 +1062,7 @@ class Renderer:
|
|||||||
except:
|
except:
|
||||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
|
||||||
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
|
p = Paragraph(text, style=style)
|
||||||
return p, ad, lineheight
|
return p, ad, lineheight
|
||||||
|
|
||||||
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||||
|
|||||||
@@ -287,11 +287,11 @@ def _check_position_constraints(
|
|||||||
raise CartPositionError(error_messages['unavailable'])
|
raise CartPositionError(error_messages['unavailable'])
|
||||||
|
|
||||||
# Invalid media policy for online sale
|
# Invalid media policy for online sale
|
||||||
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||||
mt = MEDIA_TYPES[item.media_type]
|
mt = MEDIA_TYPES[item.media_type]
|
||||||
if not mt.medium_created_by_server:
|
if not mt.medium_created_by_server:
|
||||||
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||||
elif item.media_policy in (Item.MEDIA_POLICY_REUSE, Item.MEDIA_POLICY_APPEND):
|
elif item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||||
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||||
|
|
||||||
# Item removed from sales channel
|
# Item removed from sales channel
|
||||||
|
|||||||
@@ -867,15 +867,6 @@ class RequiredQuestionsError(Exception):
|
|||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
class RequiredMediaExchangeError(Exception):
|
|
||||||
def __init__(self, msg, code, media_policy, media_type):
|
|
||||||
self.msg = msg
|
|
||||||
self.code = code
|
|
||||||
self.media_policy = media_policy
|
|
||||||
self.media_type = media_type
|
|
||||||
super().__init__(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def _save_answers(op, answers, given_answers):
|
def _save_answers(op, answers, given_answers):
|
||||||
def _create_answer(question, answer):
|
def _create_answer(question, answer):
|
||||||
try:
|
try:
|
||||||
@@ -948,7 +939,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
|
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
|
||||||
gate=None, reusable_medium=None):
|
gate=None):
|
||||||
"""
|
"""
|
||||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||||
not valid at this time.
|
not valid at this time.
|
||||||
@@ -964,7 +955,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
:param datetime: The datetime of the checkin, defaults to now.
|
:param datetime: The datetime of the checkin, defaults to now.
|
||||||
:param simulate: If true, the check-in is not saved.
|
:param simulate: If true, the check-in is not saved.
|
||||||
:param gate: The gate the check-in was performed at.
|
:param gate: The gate the check-in was performed at.
|
||||||
:param reusable_medium: The medium that is available for an exchange
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# !!!!!!!!!
|
# !!!!!!!!!
|
||||||
@@ -1045,7 +1035,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||||
opqs = OrderPosition.all.select_related("order", "item")
|
opqs = OrderPosition.all
|
||||||
if type != Checkin.TYPE_EXIT:
|
if type != Checkin.TYPE_EXIT:
|
||||||
opqs = opqs.select_for_update(of=OF_SELF)
|
opqs = opqs.select_for_update(of=OF_SELF)
|
||||||
op = opqs.get(pk=op.pk)
|
op = opqs.get(pk=op.pk)
|
||||||
@@ -1111,24 +1101,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
require_answers
|
require_answers
|
||||||
)
|
)
|
||||||
|
|
||||||
required_media_policy = op.item.media_policy
|
|
||||||
required_media_type = op.item.media_type
|
|
||||||
require_a_medium = required_media_policy and required_media_type
|
|
||||||
linked_media = op.linked_media
|
|
||||||
if require_a_medium and not reusable_medium and not force:
|
|
||||||
if not linked_media.exists():
|
|
||||||
raise RequiredMediaExchangeError(
|
|
||||||
_('Ticket needs to be exchanged to a suitable medium.'),
|
|
||||||
'exchange',
|
|
||||||
required_media_policy,
|
|
||||||
required_media_type
|
|
||||||
)
|
|
||||||
elif op.organizer.settings.reusable_media_usage_enforced:
|
|
||||||
raise CheckInError(
|
|
||||||
_('This ticket has already been exchanged for a reusable medium that now needs to be used instead.'),
|
|
||||||
'already_exchanged',
|
|
||||||
)
|
|
||||||
|
|
||||||
device = None
|
device = None
|
||||||
if isinstance(auth, Device):
|
if isinstance(auth, Device):
|
||||||
device = auth
|
device = auth
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ from pretix.base.signals import (
|
|||||||
)
|
)
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers import OF_SELF, repeatable_reads_transaction
|
from pretix.helpers import OF_SELF, repeatable_reads_transaction
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -455,7 +455,7 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
|
|||||||
schedule,
|
schedule,
|
||||||
organizer,
|
organizer,
|
||||||
exporter,
|
exporter,
|
||||||
mainreverse_absolute(
|
build_absolute_uri(
|
||||||
'control:organizer.export',
|
'control:organizer.export',
|
||||||
kwargs={
|
kwargs={
|
||||||
'organizer': organizer.slug,
|
'organizer': organizer.slug,
|
||||||
@@ -481,7 +481,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
|
|||||||
schedule,
|
schedule,
|
||||||
event,
|
event,
|
||||||
exporter,
|
exporter,
|
||||||
mainreverse_absolute(
|
build_absolute_uri(
|
||||||
'control:event.orders.export',
|
'control:event.orders.export',
|
||||||
kwargs={
|
kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ from pretix.helpers.format import (
|
|||||||
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||||
)
|
)
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
from pretix.helpers.hierarkey import clean_filename
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.ical import get_private_icals
|
from pretix.presale.ical import get_private_icals
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.mail')
|
logger = logging.getLogger('pretix.base.mail')
|
||||||
@@ -997,7 +997,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
|||||||
body_plain += _(
|
body_plain += _(
|
||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).replace("\n", "\r\n").format(
|
||||||
orderurl=eventreverse_absolute(
|
orderurl=build_absolute_uri(
|
||||||
order.event, 'presale:event.order.position', kwargs={
|
order.event, 'presale:event.order.position', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
@@ -1013,7 +1013,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
|||||||
body_plain += _(
|
body_plain += _(
|
||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).replace("\n", "\r\n").format(
|
||||||
event=event.name, orderurl=eventreverse_absolute(
|
event=event.name, orderurl=build_absolute_uri(
|
||||||
order.event, 'presale:event.order.open', kwargs={
|
order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
|
|||||||
@@ -23,13 +23,10 @@ import secrets
|
|||||||
|
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.models import GiftCardAcceptance
|
||||||
from pretix.base.models import Checkin, GiftCardAcceptance, Item
|
from pretix.base.models.media import MediumKeySet
|
||||||
from pretix.base.models.media import MediumKeySet, ReusableMedium
|
|
||||||
from pretix.base.services.checkin import CheckInError
|
|
||||||
|
|
||||||
|
|
||||||
def create_nfc_mf0aes_keyset(organizer):
|
def create_nfc_mf0aes_keyset(organizer):
|
||||||
@@ -73,174 +70,3 @@ def get_keysets_for_organizer(organizer):
|
|||||||
if new_set:
|
if new_set:
|
||||||
sets.append(new_set)
|
sets.append(new_set)
|
||||||
return sets
|
return sets
|
||||||
|
|
||||||
|
|
||||||
def perform_media_exchange(organizer, media_type, identifier, link_orderposition, user, auth):
|
|
||||||
"""
|
|
||||||
Create or retrieve a medium, then link the order position to it. Expected to be called in a transaction.
|
|
||||||
|
|
||||||
:param organizer: Organizer to operate in
|
|
||||||
:param media_type: Type of medium to operate with
|
|
||||||
:param identifier: Identifier of the medium
|
|
||||||
:param link_orderposition: Position to link to the medium
|
|
||||||
:return: ReusableMedium
|
|
||||||
"""
|
|
||||||
medium = None
|
|
||||||
media_policy = link_orderposition.item.media_policy
|
|
||||||
|
|
||||||
if media_type not in MEDIA_TYPES: # should be caught by serializer already
|
|
||||||
raise CheckInError(
|
|
||||||
_('Invalid medium type.'),
|
|
||||||
Checkin.REASON_ERROR,
|
|
||||||
reason=_('Invalid medium type.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not MEDIA_TYPES[media_type].is_active(organizer):
|
|
||||||
raise CheckInError(
|
|
||||||
_('Medium type is not enabled for organizer.'),
|
|
||||||
Checkin.REASON_ERROR,
|
|
||||||
reason=_('Medium type is not enabled for organizer.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if link_orderposition.item.media_type != media_type:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Incorrect medium type for product.'),
|
|
||||||
Checkin.REASON_PRODUCT,
|
|
||||||
reason=_('Incorrect medium type for product.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if link_orderposition.linked_media.exists():
|
|
||||||
raise CheckInError(
|
|
||||||
_('Ticket is already exchanged for reusable medium.'),
|
|
||||||
Checkin.REASON_ALREADY_EXCHANGED,
|
|
||||||
reason=_('Ticket is already exchanged for reusable medium.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if media_policy in (Item.MEDIA_POLICY_APPEND, Item.MEDIA_POLICY_APPEND_OR_NEW, Item.MEDIA_POLICY_NEW):
|
|
||||||
link_action = "append"
|
|
||||||
else:
|
|
||||||
link_action = "replace"
|
|
||||||
|
|
||||||
if media_policy in (Item.MEDIA_POLICY_REUSE, Item.MEDIA_POLICY_APPEND):
|
|
||||||
try:
|
|
||||||
medium = ReusableMedium.objects.get(
|
|
||||||
type=media_type,
|
|
||||||
identifier=identifier,
|
|
||||||
organizer=organizer,
|
|
||||||
)
|
|
||||||
except ReusableMedium.DoesNotExist:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
reason=_('Reusable medium not found.'),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if medium.is_expired or not medium.active:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium is inactive or expired.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
reason=_('Reusable medium is inactive or expired.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif media_policy in (Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW):
|
|
||||||
try:
|
|
||||||
medium = ReusableMedium.objects.get(
|
|
||||||
type=media_type,
|
|
||||||
identifier=identifier,
|
|
||||||
organizer=organizer,
|
|
||||||
)
|
|
||||||
except ReusableMedium.DoesNotExist:
|
|
||||||
if not MEDIA_TYPES[media_type].medium_created_from_unknown_supported:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found and could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
|
|
||||||
medium = MEDIA_TYPES[media_type].handle_unknown(organizer, identifier, user, auth, force_create=True)
|
|
||||||
if not medium:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found and could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
|
|
||||||
if medium.is_expired or not medium.active:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium is inactive or expired.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
reason=_('Reusable medium is inactive or expired.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif media_policy == Item.MEDIA_POLICY_NEW:
|
|
||||||
if not MEDIA_TYPES[media_type].medium_created_from_unknown_supported:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium not found and could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
medium = MEDIA_TYPES[media_type].handle_unknown(organizer, identifier, user, auth, force_create=True)
|
|
||||||
except IntegrityError:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium already exists.'),
|
|
||||||
Checkin.REASON_MEDIUM_EXISTS,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not medium:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Reusable medium could not be created.'),
|
|
||||||
Checkin.REASON_MEDIUM_INVALID,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise CheckInError(
|
|
||||||
_('Product does not support medium exchange.'),
|
|
||||||
Checkin.REASON_PRODUCT,
|
|
||||||
reason=_('Product does not support medium exchange.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if link_action == 'append':
|
|
||||||
medium.linked_orderpositions.add(link_orderposition)
|
|
||||||
medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
|
||||||
user=user,
|
|
||||||
auth=auth,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': link_orderposition,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif link_action == 'replace':
|
|
||||||
already_found = False
|
|
||||||
for op_pk in medium.linked_orderpositions.values_list('pk', flat=True):
|
|
||||||
if op_pk == link_orderposition.pk:
|
|
||||||
already_found = True
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.removed',
|
|
||||||
data={
|
|
||||||
'linked_orderposition': op_pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if not already_found:
|
|
||||||
medium.linked_orderpositions.set([link_orderposition])
|
|
||||||
medium.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
|
||||||
user=user,
|
|
||||||
auth=auth,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': link_orderposition,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
link_orderposition.order.log_action(
|
|
||||||
'pretix.reusable_medium.exchanged',
|
|
||||||
data={
|
|
||||||
'position': link_orderposition.pk,
|
|
||||||
'positionid': link_orderposition.positionid,
|
|
||||||
'medium': medium.pk,
|
|
||||||
'medium_identifier': medium.identifier,
|
|
||||||
'medium_type': medium.media_type.identifier,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
medium.touch()
|
|
||||||
|
|
||||||
return medium
|
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ from typing import List
|
|||||||
from django.conf import settings as django_settings
|
from django.conf import settings as django_settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _, ngettext
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
|
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
|
||||||
@@ -261,7 +260,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||||
# shorter. We'll see what works better in reality…
|
# shorter. We'll see what works better in reality…
|
||||||
vouchers = []
|
vouchers = []
|
||||||
codes = set()
|
|
||||||
lock_seats = []
|
lock_seats = []
|
||||||
for i, record in enumerate(data):
|
for i, record in enumerate(data):
|
||||||
try:
|
try:
|
||||||
@@ -270,14 +268,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
|
|
||||||
if not record.get("code"):
|
if not record.get("code"):
|
||||||
raise ValidationError(_('A voucher cannot be created without a code.'))
|
raise ValidationError(_('A voucher cannot be created without a code.'))
|
||||||
code = record.get("code")
|
|
||||||
if code.upper() in codes:
|
|
||||||
raise ValidationError(
|
|
||||||
_('Voucher codes must be unique. Code "{code}" already exists in this import.').format(
|
|
||||||
code=code,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
codes.add(code.upper())
|
|
||||||
Voucher.clean_item_properties(
|
Voucher.clean_item_properties(
|
||||||
record,
|
record,
|
||||||
event,
|
event,
|
||||||
@@ -296,22 +286,8 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
lock_seats.append(voucher.seat)
|
lock_seats.append(voucher.seat)
|
||||||
except (ValidationError, ImportError) as e:
|
except (ValidationError, ImportError) as e:
|
||||||
raise DataImportError(
|
raise DataImportError(
|
||||||
_('Invalid data in row {row}: {message}').format(row=i + 1, message=str(e))
|
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||||
)
|
)
|
||||||
existing_codes = Voucher.objects.filter(
|
|
||||||
event=event,
|
|
||||||
code__in=codes,
|
|
||||||
).values_list("code", flat=True)
|
|
||||||
if len(existing_codes):
|
|
||||||
raise DataImportError(
|
|
||||||
ngettext(
|
|
||||||
'Voucher codes must be unique. Import contains existing voucher code {code}.',
|
|
||||||
'Voucher codes must be unique. Import contains existing voucher codes {code}.',
|
|
||||||
len(existing_codes)
|
|
||||||
).format(
|
|
||||||
code=", ".join(existing_codes)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# We don't support quotas here, so we only need to lock if seats are in use
|
# We don't support quotas here, so we only need to lock if seats are in use
|
||||||
@@ -324,13 +300,7 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
|
|
||||||
save_logentries = []
|
save_logentries = []
|
||||||
for v in vouchers:
|
for v in vouchers:
|
||||||
try:
|
v.save()
|
||||||
v.save()
|
|
||||||
except IntegrityError:
|
|
||||||
# should not happen as we check existing codes before, but we did not lock so we might have a race-condition
|
|
||||||
raise DataImportError(
|
|
||||||
_('Vouchers could not be imported, probably due to a voucher code already being in use.')
|
|
||||||
)
|
|
||||||
save_logentries.append(v.log_action(
|
save_logentries.append(v.log_action(
|
||||||
'pretix.voucher.added',
|
'pretix.voucher.added',
|
||||||
user=user,
|
user=user,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
|||||||
from pretix.base.signals import notification
|
from pretix.base.signals import notification
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.celery import get_task_priority
|
from pretix.helpers.celery import get_task_priority
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900)
|
@app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900)
|
||||||
@@ -136,10 +136,10 @@ def send_notification_mail(notification: Notification, user: User):
|
|||||||
'site_url': settings.SITE_URL,
|
'site_url': settings.SITE_URL,
|
||||||
'color': settings.PRETIX_PRIMARY_COLOR,
|
'color': settings.PRETIX_PRIMARY_COLOR,
|
||||||
'notification': notification,
|
'notification': notification,
|
||||||
'settings_url': mainreverse_absolute(
|
'settings_url': build_absolute_uri(
|
||||||
'control:user.settings.notifications',
|
'control:user.settings.notifications',
|
||||||
),
|
),
|
||||||
'disable_url': mainreverse_absolute(
|
'disable_url': build_absolute_uri(
|
||||||
'control:user.settings.notifications.off',
|
'control:user.settings.notifications.off',
|
||||||
kwargs={
|
kwargs={
|
||||||
'token': user.notifications_token,
|
'token': user.notifications_token,
|
||||||
|
|||||||
@@ -3506,7 +3506,7 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
|
|||||||
from pretix.base.models import ReusableMedium
|
from pretix.base.models import ReusableMedium
|
||||||
|
|
||||||
for p in order.positions.all():
|
for p in order.positions.all():
|
||||||
if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW, Item.MEDIA_POLICY_APPEND_OR_NEW):
|
if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||||
mt = MEDIA_TYPES[p.item.media_type]
|
mt = MEDIA_TYPES[p.item.media_type]
|
||||||
if mt.medium_created_by_server and not p.linked_media.exists():
|
if mt.medium_created_by_server and not p.linked_media.exists():
|
||||||
rm = ReusableMedium.objects.create(
|
rm = ReusableMedium.objects.create(
|
||||||
@@ -3515,8 +3515,8 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
|
|||||||
identifier=mt.generate_identifier(sender.organizer),
|
identifier=mt.generate_identifier(sender.organizer),
|
||||||
active=True,
|
active=True,
|
||||||
customer=order.customer,
|
customer=order.customer,
|
||||||
|
linked_orderposition=p,
|
||||||
)
|
)
|
||||||
rm.linked_orderpositions.add(p)
|
|
||||||
rm.log_action(
|
rm.log_action(
|
||||||
'pretix.reusable_medium.created',
|
'pretix.reusable_medium.created',
|
||||||
data={
|
data={
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ def get_best_name(position_or_address, parts=False):
|
|||||||
|
|
||||||
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
||||||
def base_placeholders(sender, **kwargs):
|
def base_placeholders(sender, **kwargs):
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
def _event_sample(event):
|
def _event_sample(event):
|
||||||
if event.has_subevents:
|
if event.has_subevents:
|
||||||
@@ -388,14 +388,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
lambda event: LazyDate(now() + timedelta(days=15))
|
lambda event: LazyDate(now() + timedelta(days=15))
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
'hash': order.email_confirm_secret()
|
'hash': order.email_confirm_secret()
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -406,7 +406,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleButtonPlaceholder(
|
SimpleButtonPlaceholder(
|
||||||
'url_button', ['order', 'event'],
|
'url_button', ['order', 'event'],
|
||||||
url_func=lambda order, event: eventreverse_absolute(
|
url_func=lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -415,7 +415,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
text_func=lambda order, event: _("View order details"),
|
text_func=lambda order, event: _("View order details"),
|
||||||
sample_url_func=lambda event: eventreverse_absolute(
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -426,13 +426,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
sample_text_func=lambda event: _("View order details"),
|
sample_text_func=lambda event: _("View order details"),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.modify', kwargs={
|
'presale:event.order.modify', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.modify', kwargs={
|
'presale:event.order.modify', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -441,13 +441,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_products_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.change', kwargs={
|
'presale:event.order.change', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.change', kwargs={
|
'presale:event.order.change', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -456,13 +456,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_cancel', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.cancel', kwargs={
|
'presale:event.order.cancel', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.cancel', kwargs={
|
'presale:event.order.cancel', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -471,7 +471,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['event', 'position'], lambda event, position: eventreverse_absolute(
|
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position',
|
'presale:event.order.position',
|
||||||
kwargs={
|
kwargs={
|
||||||
@@ -480,7 +480,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -491,7 +491,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleButtonPlaceholder(
|
SimpleButtonPlaceholder(
|
||||||
'url_button', ['event', 'position'],
|
'url_button', ['event', 'position'],
|
||||||
url_func=lambda event, position: eventreverse_absolute(
|
url_func=lambda event, position: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
@@ -500,7 +500,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
text_func=lambda event, position: _("View registration details"),
|
text_func=lambda event, position: _("View registration details"),
|
||||||
sample_url_func=lambda event: eventreverse_absolute(
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -511,14 +511,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
sample_text_func=lambda event: _("View registration details"),
|
sample_text_func=lambda event: _("View registration details"),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
|
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.modify', kwargs={
|
'presale:event.order.position.modify', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.modify', kwargs={
|
'presale:event.order.position.modify', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -528,14 +528,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_products_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
|
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.change', kwargs={
|
'presale:event.order.position.change', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.change', kwargs={
|
'presale:event.order.position.change', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -581,20 +581,20 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_remove', ['waiting_list_voucher', 'event'],
|
'url_remove', ['waiting_list_voucher', 'event'],
|
||||||
lambda waiting_list_voucher, event: eventreverse_absolute(
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
event, 'presale:event.waitinglist.remove'
|
event, 'presale:event.waitinglist.remove'
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.waitinglist.remove',
|
'presale:event.waitinglist.remove',
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['waiting_list_voucher', 'event'],
|
'url', ['waiting_list_voucher', 'event'],
|
||||||
lambda waiting_list_voucher, event: eventreverse_absolute(
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.redeem',
|
'presale:event.redeem',
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
@@ -611,7 +611,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||||
'* {} - {}'.format(
|
'* {} - {}'.format(
|
||||||
order.full_code,
|
order.full_code,
|
||||||
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -623,7 +623,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
), lambda event: '\n' + '\n\n'.join(
|
), lambda event: '\n' + '\n\n'.join(
|
||||||
'* {} - {}'.format(
|
'* {} - {}'.format(
|
||||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||||
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
'order': order['code'],
|
'order': order['code'],
|
||||||
@@ -662,13 +662,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
'voucher_url_list', ['event', 'voucher_list'],
|
'voucher_url_list', ['event', 'voucher_list'],
|
||||||
lambda event, voucher_list: ' \n'.join([
|
lambda event, voucher_list: ' \n'.join([
|
||||||
eventreverse_absolute(
|
build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + c
|
) + '?voucher=' + c
|
||||||
for c in voucher_list
|
for c in voucher_list
|
||||||
]),
|
]),
|
||||||
lambda event: ' \n'.join([
|
lambda event: ' \n'.join([
|
||||||
eventreverse_absolute(
|
build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + c
|
) + '?voucher=' + c
|
||||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||||
@@ -676,10 +676,10 @@ def base_placeholders(sender, **kwargs):
|
|||||||
inline=False,
|
inline=False,
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: eventreverse_absolute(event, 'presale:event.index', kwargs={
|
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
}), lambda event: eventreverse_absolute(event, 'presale:event.index', kwargs={
|
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from pretix.base.services.mail import mail
|
|||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
from pretix.base.signals import periodic_task
|
from pretix.base.signals import periodic_task
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
@@ -121,7 +121,7 @@ def send_update_notification_email():
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
'url': mainreverse_absolute('control:global.update')
|
'url': build_absolute_uri('control:global.update')
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -211,25 +211,12 @@ DEFAULTS = {
|
|||||||
'form_class': forms.BooleanField,
|
'form_class': forms.BooleanField,
|
||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Activate reusable media"),
|
label=_("Activate re-usable media"),
|
||||||
help_text=_("The reusable media feature allows you to connect tickets and gift cards with physical media "
|
help_text=_("The re-usable media feature allows you to connect tickets and gift cards with physical media "
|
||||||
"such as wristbands or chip cards that may be reused for different tickets or gift cards "
|
"such as wristbands or chip cards that may be re-used for different tickets or gift cards "
|
||||||
"later.")
|
"later.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'reusable_media_usage_enforced': {
|
|
||||||
'default': 'False',
|
|
||||||
'type': bool,
|
|
||||||
'form_class': forms.BooleanField,
|
|
||||||
'serializer_class': serializers.BooleanField,
|
|
||||||
'form_kwargs': dict(
|
|
||||||
label=_("Enforce the usage of issued reusable media for check-in"),
|
|
||||||
help_text=_("If enabled, a ticket barcode will not be accepted anymore, if a reusable medium has been "
|
|
||||||
"created and linked to a ticket. Keeping this option turned off will treat the reusable "
|
|
||||||
"medium and ticket as equals."),
|
|
||||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-reusable_media_active'}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'reusable_media_type_barcode': {
|
'reusable_media_type_barcode': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
|
|
||||||
{% block custom_header %}{% endblock %}
|
{% block custom_header %}{% endblock %}
|
||||||
{% if css_theme %}
|
{% if css_theme %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
||||||
@@ -22,4 +21,5 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
<script src="{% static "pretixbase/js/errors.js" %}"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load rich_text %}
|
{% load rich_text %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load wrap_in %}
|
|
||||||
{% block title %}{% trans "Redirect" %}{% endblock %}
|
{% block title %}{% trans "Redirect" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<i class="fa fa-link fa-fw big-icon"></i>
|
<i class="fa fa-link fa-fw big-icon"></i>
|
||||||
<div class="error-details">
|
<div class="error-details">
|
||||||
<h1>{% trans "Redirect" %}</h1>
|
<h1>{% trans "Redirect" %}</h1>
|
||||||
<h3>
|
<h3>
|
||||||
{% blocktrans trimmed with host=hostname|wrap_in:'strong' %}
|
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
|
||||||
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
|
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
|
|||||||
@@ -461,31 +461,3 @@ class SalesChannelCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
|||||||
**super().create_option(name, value, label, selected, index, subindex, attrs),
|
**super().create_option(name, value, label, selected, index, subindex, attrs),
|
||||||
"plugin_missing": plugin and plugin not in self.event.get_plugins(),
|
"plugin_missing": plugin and plugin not in self.event.get_plugins(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModelChoiceIteratorWithNone(forms.models.ModelChoiceIterator):
|
|
||||||
# see django.forms.models.ModelChoiceIterator for original implementation
|
|
||||||
def __iter__(self):
|
|
||||||
if self.field.empty_label is not None:
|
|
||||||
yield ("", self.field.empty_label)
|
|
||||||
if self.field.none_label is not None:
|
|
||||||
yield ("_none", self.field.none_label)
|
|
||||||
queryset = self.queryset
|
|
||||||
# Can't use iterator() when queryset uses prefetch_related()
|
|
||||||
if not queryset._prefetch_related_lookups:
|
|
||||||
queryset = queryset.iterator()
|
|
||||||
for obj in queryset:
|
|
||||||
yield self.choice(obj)
|
|
||||||
|
|
||||||
|
|
||||||
class ModelChoiceFieldWithNone(forms.ModelChoiceField):
|
|
||||||
iterator = ModelChoiceIteratorWithNone
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.none_label = kwargs.pop("none_label", None)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
if value == "_none":
|
|
||||||
return value
|
|
||||||
return super().to_python(value)
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ from pretix.control.forms.widgets import Select2
|
|||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
|
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
|
||||||
from pretix.multidomain.urlreverse import (
|
from pretix.multidomain.urlreverse import (
|
||||||
eventreverse_absolute, get_organizer_domain,
|
build_absolute_uri, get_organizer_domain,
|
||||||
)
|
)
|
||||||
from pretix.plugins.banktransfer.payment import BankTransfer
|
from pretix.plugins.banktransfer.payment import BankTransfer
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
@@ -219,7 +219,7 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||||
'Sample Conference Center\nHeidelberg, Germany'
|
'Sample Conference Center\nHeidelberg, Germany'
|
||||||
)
|
)
|
||||||
self.fields['slug'].widget.prefix = eventreverse_absolute(self.organizer, 'presale:organizer.index')
|
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index')
|
||||||
self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required
|
self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required
|
||||||
if self.has_subevents:
|
if self.has_subevents:
|
||||||
del self.fields['presale_start']
|
del self.fields['presale_start']
|
||||||
@@ -1673,7 +1673,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
|
|||||||
|
|
||||||
class TaxRuleLineForm(I18nForm):
|
class TaxRuleLineForm(I18nForm):
|
||||||
country = LazyTypedChoiceField(
|
country = LazyTypedChoiceField(
|
||||||
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
|
choices=CountriesAndEUAndStates(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
address_type = forms.ChoiceField(
|
address_type = forms.ChoiceField(
|
||||||
|
|||||||
@@ -1342,13 +1342,7 @@ class QuestionAnswerFilterForm(forms.Form):
|
|||||||
opqs = opqs.filter(canceled=False)
|
opqs = opqs.filter(canceled=False)
|
||||||
if fdata.get("item", "") != "":
|
if fdata.get("item", "") != "":
|
||||||
i = fdata.get("item", "")
|
i = fdata.get("item", "")
|
||||||
if '-' in i:
|
opqs = opqs.filter(item_id__in=(i,))
|
||||||
opqs = opqs.filter(
|
|
||||||
item_id=i.split('-')[0],
|
|
||||||
variation_id=i.split('-')[1],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
opqs = opqs.filter(item_id=i)
|
|
||||||
|
|
||||||
return opqs
|
return opqs
|
||||||
|
|
||||||
@@ -1877,7 +1871,7 @@ class ReusableMediaFilterForm(FilterForm):
|
|||||||
Q(identifier__icontains=query)
|
Q(identifier__icontains=query)
|
||||||
| Q(customer__identifier__icontains=query)
|
| Q(customer__identifier__icontains=query)
|
||||||
| Q(customer__external_identifier__istartswith=query)
|
| Q(customer__external_identifier__istartswith=query)
|
||||||
| Q(linked_orderpositions__order__code__icontains=query)
|
| Q(linked_orderposition__order__code__icontains=query)
|
||||||
| Q(linked_giftcard__secret__icontains=query)
|
| Q(linked_giftcard__secret__icontains=query)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -108,9 +108,8 @@ class GlobalSettingsForm(SettingsForm):
|
|||||||
('widget_vite_origins', forms.CharField(
|
('widget_vite_origins', forms.CharField(
|
||||||
widget=forms.Textarea(attrs={'rows': '3'}),
|
widget=forms.Textarea(attrs={'rows': '3'}),
|
||||||
required=False,
|
required=False,
|
||||||
# Not translated on purpose, this is a temporary feature and contains too many special case words
|
label=_("Vite widget origins"),
|
||||||
label="Vite widget origins",
|
help_text=_("One origin per line (e.g. https://example.com). Requests from these origins will be served the new vite-based widget."),
|
||||||
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the new vite-based widget.",
|
|
||||||
))
|
))
|
||||||
])
|
])
|
||||||
responses = register_global_settings.send(self)
|
responses = register_global_settings.send(self)
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ from pretix.control.forms import ExtFileField, SplitDateTimeField
|
|||||||
from pretix.control.forms.event import (
|
from pretix.control.forms.event import (
|
||||||
SafeEventMultipleChoiceField, multimail_validate,
|
SafeEventMultipleChoiceField, multimail_validate,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.widgets import Select2, Select2Multiple
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.multidomain.models import KnownDomain
|
from pretix.multidomain.models import KnownDomain
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
class OrganizerForm(I18nModelForm):
|
class OrganizerForm(I18nModelForm):
|
||||||
@@ -249,15 +249,6 @@ class SafeOrderPositionChoiceField(forms.ModelChoiceField):
|
|||||||
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
||||||
|
|
||||||
|
|
||||||
class SafeOrderPositionMultipleChoiceField(forms.ModelMultipleChoiceField):
|
|
||||||
def __init__(self, queryset, **kwargs):
|
|
||||||
queryset = queryset.model.all.none()
|
|
||||||
super().__init__(queryset, **kwargs)
|
|
||||||
|
|
||||||
def label_from_instance(self, op):
|
|
||||||
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
|
||||||
|
|
||||||
|
|
||||||
class EventMetaPropertyForm(I18nModelForm):
|
class EventMetaPropertyForm(I18nModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = EventMetaProperty
|
model = EventMetaProperty
|
||||||
@@ -636,7 +627,6 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
'cookie_consent_dialog_button_yes',
|
'cookie_consent_dialog_button_yes',
|
||||||
'cookie_consent_dialog_button_no',
|
'cookie_consent_dialog_button_no',
|
||||||
'reusable_media_active',
|
'reusable_media_active',
|
||||||
'reusable_media_usage_enforced',
|
|
||||||
'reusable_media_type_barcode',
|
'reusable_media_type_barcode',
|
||||||
'reusable_media_type_barcode_identifier_length',
|
'reusable_media_type_barcode_identifier_length',
|
||||||
'reusable_media_type_nfc_uid',
|
'reusable_media_type_nfc_uid',
|
||||||
@@ -791,7 +781,7 @@ class MailSettingsForm(SettingsForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if 'url' in base_parameters:
|
if 'url' in base_parameters:
|
||||||
placeholders['url'] = eventreverse_absolute(
|
placeholders['url'] = build_absolute_uri(
|
||||||
self.organizer,
|
self.organizer,
|
||||||
'presale:organizer.customer.activate'
|
'presale:organizer.customer.activate'
|
||||||
) + '?token=' + get_random_string(30)
|
) + '?token=' + get_random_string(30)
|
||||||
@@ -973,12 +963,12 @@ class ReusableMediumUpdateForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ReusableMedium
|
model = ReusableMedium
|
||||||
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderpositions', 'notes']
|
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderposition', 'notes']
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'expires': SplitDateTimeField,
|
'expires': SplitDateTimeField,
|
||||||
'customer': SafeModelChoiceField,
|
'customer': SafeModelChoiceField,
|
||||||
'linked_giftcard': SafeModelChoiceField,
|
'linked_giftcard': SafeModelChoiceField,
|
||||||
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
|
'linked_orderposition': SafeOrderPositionChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'expires': SplitDateTimePickerWidget,
|
'expires': SplitDateTimePickerWidget,
|
||||||
@@ -988,8 +978,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
organizer = self.instance.organizer
|
organizer = self.instance.organizer
|
||||||
|
|
||||||
self.fields['linked_orderpositions'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
|
self.fields['linked_orderposition'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
|
||||||
self.fields['linked_orderpositions'].widget = Select2Multiple(
|
self.fields['linked_orderposition'].widget = Select2(
|
||||||
attrs={
|
attrs={
|
||||||
'data-model-select2': 'generic',
|
'data-model-select2': 'generic',
|
||||||
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
|
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
|
||||||
@@ -997,8 +987,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.fields['linked_orderpositions'].widget.choices = self.fields['linked_orderpositions'].choices
|
self.fields['linked_orderposition'].widget.choices = self.fields['linked_orderposition'].choices
|
||||||
self.fields['linked_orderpositions'].required = False
|
self.fields['linked_orderposition'].required = False
|
||||||
|
|
||||||
self.fields['linked_giftcard'].queryset = organizer.issued_gift_cards.all()
|
self.fields['linked_giftcard'].queryset = organizer.issued_gift_cards.all()
|
||||||
self.fields['linked_giftcard'].widget = Select2(
|
self.fields['linked_giftcard'].widget = Select2(
|
||||||
@@ -1052,12 +1042,12 @@ class ReusableMediumCreateForm(ReusableMediumUpdateForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ReusableMedium
|
model = ReusableMedium
|
||||||
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderpositions', 'linked_giftcard', 'customer', 'notes']
|
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderposition', 'linked_giftcard', 'customer', 'notes']
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'expires': SplitDateTimeField,
|
'expires': SplitDateTimeField,
|
||||||
'customer': SafeModelChoiceField,
|
'customer': SafeModelChoiceField,
|
||||||
'linked_giftcard': SafeModelChoiceField,
|
'linked_giftcard': SafeModelChoiceField,
|
||||||
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
|
'linked_orderposition': SafeOrderPositionChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'expires': SplitDateTimePickerWidget,
|
'expires': SplitDateTimePickerWidget,
|
||||||
|
|||||||
@@ -29,30 +29,17 @@ class Select2Mixin:
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def options(self, name, value, attrs=None):
|
def options(self, name, value, attrs=None):
|
||||||
if not value or not value[0]:
|
if value and value[0]:
|
||||||
return
|
for i, selected in enumerate(self.choices.queryset.filter(pk__in=value)):
|
||||||
has_none = "_none" in value
|
yield self.create_option(
|
||||||
if has_none:
|
None,
|
||||||
value = [v for v in value if v != "_none"]
|
self.choices.field.prepare_value(selected),
|
||||||
yield self.create_option(
|
self.choices.field.label_from_instance(selected),
|
||||||
None,
|
True,
|
||||||
"_none",
|
i,
|
||||||
self.choices.field.none_label,
|
subindex=None,
|
||||||
True,
|
attrs=attrs
|
||||||
0,
|
)
|
||||||
subindex=None,
|
|
||||||
attrs=attrs
|
|
||||||
)
|
|
||||||
for i, selected in enumerate(self.choices.queryset.filter(pk__in=value)):
|
|
||||||
yield self.create_option(
|
|
||||||
None,
|
|
||||||
self.choices.field.prepare_value(selected),
|
|
||||||
self.choices.field.label_from_instance(selected),
|
|
||||||
True,
|
|
||||||
i + (1 if has_none else 0),
|
|
||||||
subindex=None,
|
|
||||||
attrs=attrs
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def optgroups(self, name, value, attrs=None):
|
def optgroups(self, name, value, attrs=None):
|
||||||
|
|||||||
@@ -743,10 +743,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
|||||||
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
|
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
|
||||||
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
|
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
|
||||||
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
|
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
|
||||||
'pretix.reusable_medium.linked_orderposition.added': _('A new ticket has been added to the medium.'),
|
|
||||||
'pretix.reusable_medium.linked_orderposition.removed': _('A ticket has been removed from the medium.'),
|
|
||||||
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
|
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
|
||||||
'pretix.reusable_medium.exchanged': _('The ticket #{positionid} was exchanged for reusable medium {medium_identifier}.'),
|
|
||||||
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
|
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
|
||||||
'pretix.email.error': _('Sending of an email has failed.'),
|
'pretix.email.error': _('Sending of an email has failed.'),
|
||||||
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
|
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
|
||||||
|
|||||||
@@ -213,16 +213,6 @@ quota as argument in the ``quota`` keyword argument.
|
|||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
subevent_detail_html = EventPluginSignal()
|
|
||||||
"""
|
|
||||||
Arguments: 'subevent'
|
|
||||||
|
|
||||||
This signal allows you to append HTML to a SubEvent's detail view. You receive the
|
|
||||||
subevent as argument in the ``subevent`` keyword argument.
|
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
|
||||||
"""
|
|
||||||
|
|
||||||
organizer_edit_tabs = DeprecatedSignal()
|
organizer_edit_tabs = DeprecatedSignal()
|
||||||
"""
|
"""
|
||||||
Arguments: 'organizer', 'request'
|
Arguments: 'organizer', 'request'
|
||||||
@@ -271,16 +261,6 @@ As with all event plugin signals, the ``sender`` keyword argument will contain t
|
|||||||
Additionally, the argument ``order`` and ``request`` are available.
|
Additionally, the argument ``order`` and ``request`` are available.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
order_approve_info = EventPluginSignal()
|
|
||||||
"""
|
|
||||||
Arguments: ``order``, ``request``
|
|
||||||
|
|
||||||
This signal is sent out to display additional information on the order approve page
|
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
|
||||||
Additionally, the argument ``order`` and ``request`` are available.
|
|
||||||
"""
|
|
||||||
|
|
||||||
order_position_buttons = EventPluginSignal()
|
order_position_buttons = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
Arguments: ``order``, ``position``, ``request``
|
Arguments: ``order``, ``position``, ``request``
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ from pretix import __version__
|
|||||||
from pretix.base.models import Order, OrderPayment, Transaction
|
from pretix.base.models import Order, OrderPayment, Transaction
|
||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.reportlab import PlainTextParagraph
|
|
||||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||||
from pretix.settings import DATA_DIR
|
from pretix.settings import DATA_DIR
|
||||||
|
|
||||||
@@ -80,23 +79,23 @@ class SysReport(ReportlabExportMixin):
|
|||||||
style_small.fontSize = 6
|
style_small.fontSize = 6
|
||||||
|
|
||||||
story = [
|
story = [
|
||||||
PlainTextParagraph("System report", headlinestyle),
|
Paragraph("System report", headlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Usage", subheadlinestyle),
|
Paragraph("Usage", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
self._usage_table(),
|
self._usage_table(),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Installed versions", subheadlinestyle),
|
Paragraph("Installed versions", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
self._tech_table(),
|
self._tech_table(),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Plugins", subheadlinestyle),
|
Paragraph("Plugins", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph(self._get_plugin_versions(), style_small),
|
Paragraph(self._get_plugin_versions(), style_small),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Custom templates", subheadlinestyle),
|
Paragraph("Custom templates", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph(self._get_custom_templates(), style_small),
|
Paragraph(self._get_custom_templates(), style_small),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -122,13 +121,13 @@ class SysReport(ReportlabExportMixin):
|
|||||||
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
||||||
]
|
]
|
||||||
tdata = [
|
tdata = [
|
||||||
[PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
||||||
[PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
|
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
|
||||||
[PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
|
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
|
||||||
[PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
|
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
|
||||||
[
|
[
|
||||||
PlainTextParagraph("Database engine:", style),
|
Paragraph("Database engine:", style),
|
||||||
PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
|
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
||||||
@@ -207,7 +206,7 @@ class SysReport(ReportlabExportMixin):
|
|||||||
year_last = now().year
|
year_last = now().year
|
||||||
tdata = [
|
tdata = [
|
||||||
[
|
[
|
||||||
PlainTextParagraph(l, style_small_head)
|
Paragraph(l, style_small_head)
|
||||||
for l in (
|
for l in (
|
||||||
"Time frame",
|
"Time frame",
|
||||||
"Currency",
|
"Currency",
|
||||||
@@ -258,19 +257,19 @@ class SysReport(ReportlabExportMixin):
|
|||||||
|
|
||||||
tdata.append(
|
tdata.append(
|
||||||
(
|
(
|
||||||
PlainTextParagraph(
|
Paragraph(
|
||||||
date_format(first_day, "M Y")
|
date_format(first_day, "M Y")
|
||||||
+ " – "
|
+ " – "
|
||||||
+ date_format(after_day - timedelta(days=1), "M Y"),
|
+ date_format(after_day - timedelta(days=1), "M Y"),
|
||||||
style_small,
|
style_small,
|
||||||
),
|
),
|
||||||
PlainTextParagraph(c, style_small),
|
Paragraph(c, style_small),
|
||||||
PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
|
Paragraph(str(orders_count), style_small) if i == 0 else "",
|
||||||
PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
||||||
PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
|
Paragraph(str(testmode_count), style_small) if i == 0 else "",
|
||||||
PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
||||||
PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
|
Paragraph(str(revenue_data.get("c") or 0), style_small),
|
||||||
PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,6 @@
|
|||||||
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
|
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
|
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
|
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{{ html_head|safe }}
|
{{ html_head|safe }}
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,6 @@
|
|||||||
<span class="fa fa-check-circle"></span>
|
<span class="fa fa-check-circle"></span>
|
||||||
{% elif result.status == "incomplete" %}
|
{% elif result.status == "incomplete" %}
|
||||||
<span class="fa fa-question-circle"></span>
|
<span class="fa fa-question-circle"></span>
|
||||||
{% elif result.status == "exchange" %}
|
|
||||||
<span class="fa fa-recycle"></span>
|
|
||||||
{% elif result.status == "error" %}
|
{% elif result.status == "error" %}
|
||||||
{% if result.reason == "already_redeemed" %}
|
{% if result.reason == "already_redeemed" %}
|
||||||
<span class="fa fa-warning"></span>
|
<span class="fa fa-warning"></span>
|
||||||
@@ -81,14 +79,6 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% elif result.status == "exchange" %}
|
|
||||||
<h3 class="nomargin-top">{% trans "Media exchange required" %}</h3>
|
|
||||||
<p>
|
|
||||||
{% blocktrans trimmed with media_policy=media_policies|getitem:result.media_policy media_type=media_types|getitem:result.media_type %}
|
|
||||||
This ticket needs to be exchanged into a <strong>{{ media_type }}</strong> reusable medium.
|
|
||||||
<strong>{{ media_policy }}</strong>.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
{% elif result.status == "error" %}
|
{% elif result.status == "error" %}
|
||||||
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
|
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
|
||||||
{% if result.reason_explanation %}
|
{% if result.reason_explanation %}
|
||||||
|
|||||||
@@ -9,24 +9,23 @@
|
|||||||
<h3 class="panel-title">{% trans "Go offline" %}</h3>
|
<h3 class="panel-title">{% trans "Go offline" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="row panel-body">
|
<div class="row panel-body">
|
||||||
<div class="col-sm-12 col-lg-6">
|
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||||
<p>
|
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
You can take your event offline. Nobody except your team will be able to see or access it any more.
|
You can take your event offline. Nobody except your team will be able to see or access it any more.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div class="col-sm-12 col-md-3">
|
||||||
<form class="col-sm-12 col-lg-6 text-right"
|
<form action="{% url "control:event.live" event=request.event.slug organizer=request.organizer.slug %}"
|
||||||
action="{% url "control:event.live" event=request.event.slug organizer=request.organizer.slug %}"
|
method="post">
|
||||||
method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<input type="hidden" name="live" value="false">
|
||||||
<input type="hidden" name="live" value="false">
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||||
<span class="fa fa-power-off"></span>
|
<span class="fa fa-power-off"></span>
|
||||||
{% trans "Go offline" %}
|
{% trans "Go offline" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,24 +34,22 @@
|
|||||||
<h3 class="panel-title">{% trans "Cancel event" %}</h3>
|
<h3 class="panel-title">{% trans "Cancel event" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="row panel-body">
|
<div class="row panel-body">
|
||||||
<div class="col-sm-12 col-lg-6">
|
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||||
<p>
|
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
If you need to call off your event you want to cancel and refund all tickets, you can do so through
|
If you need to call off your event you want to cancel and refund all tickets, you can do so through
|
||||||
this option.
|
this option.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 col-lg-6 text-right">
|
<div class="col-sm-12 col-md-3 text-center">
|
||||||
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
|
{% if "event:cancel" in request.eventpermset %}
|
||||||
class="btn btn-danger btn-lg pull-right {% if "event:cancel" not in request.eventpermset %}disabled{% endif %}">
|
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
|
||||||
<span class="fa fa-ban"></span>
|
class="btn btn-danger btn-block btn-lg">
|
||||||
{% if "event:cancel" in request.eventpermset %}
|
<span class="fa fa-ban"></span>
|
||||||
{% trans "Cancel event" %}
|
{% trans "Cancel event" %}
|
||||||
{% else %}
|
</a>
|
||||||
{% trans "No permission" %}
|
{% else %}
|
||||||
{% endif %}
|
{% trans "No permission" %}
|
||||||
</a>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,16 +59,15 @@
|
|||||||
<h3 class="panel-title">{% trans "Delete personal data" %}</h3>
|
<h3 class="panel-title">{% trans "Delete personal data" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="row panel-body">
|
<div class="row panel-body">
|
||||||
<div class="col-sm-12 col-lg-6">
|
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||||
<p>
|
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
You can remove personal data such as names and email addresses from your event and only retain the
|
You can remove personal data such as names and email addresses from your event and only retain the
|
||||||
financial information such as the number and type of tickets sold.
|
financial information such as the number and type of tickets sold.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 col-lg-6 text-right">
|
<div class="col-sm-12 col-md-3">
|
||||||
<a href="{% url "control:event.shredder.start" event=request.event.slug organizer=request.organizer.slug %}" class="btn btn-danger btn-lg">
|
<a href="
|
||||||
|
{% url "control:event.shredder.start" event=request.event.slug organizer=request.organizer.slug %}" class="btn btn-danger btn-lg btn-block">
|
||||||
<span class="fa fa-eraser"></span>
|
<span class="fa fa-eraser"></span>
|
||||||
{% trans "Delete personal data" %}
|
{% trans "Delete personal data" %}
|
||||||
</a>
|
</a>
|
||||||
@@ -84,17 +80,15 @@
|
|||||||
<h3 class="panel-title">{% trans "Delete event" %}</h3>
|
<h3 class="panel-title">{% trans "Delete event" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="row panel-body">
|
<div class="row panel-body">
|
||||||
<div class="col-sm-12 col-lg-6">
|
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||||
<p>
|
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
You can delete your event completely only as long as it does not contain any undeletable data, such as
|
You can delete your event completely only as long as it does not contain any undeletable data, such as
|
||||||
orders not performed in test mode.
|
orders not performed in test mode.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 col-lg-6 text-right">
|
<div class="col-sm-12 col-md-3">
|
||||||
<a href="{% url "control:event.delete" organizer=request.organizer.slug event=request.event.slug %}"
|
<a href="{% url "control:event.delete" organizer=request.organizer.slug event=request.event.slug %}"
|
||||||
class="btn btn-danger btn-lg {% if not request.event.allow_delete %}disabled{% endif %}">
|
class="btn btn-danger btn-block btn-lg {% if not request.event.allow_delete %}disabled{% endif %}">
|
||||||
<span class="fa fa-trash"></span>
|
<span class="fa fa-trash"></span>
|
||||||
{% trans "Delete event" %}
|
{% trans "Delete event" %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "pretixcontrol/event/base.html" %}
|
{% extends "pretixcontrol/event/base.html" %}
|
||||||
{% load eventsignal %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans "Approve order" %}
|
{% trans "Approve order" %}
|
||||||
@@ -8,9 +7,6 @@
|
|||||||
<h1>
|
<h1>
|
||||||
{% trans "Approve order" %}
|
{% trans "Approve order" %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{% eventsignal request.event "pretix.control.signals.order_approve_info" order=order request=request %}
|
|
||||||
|
|
||||||
<p>{% blocktrans trimmed %}
|
<p>{% blocktrans trimmed %}
|
||||||
Do you really want to approve this order?
|
Do you really want to approve this order?
|
||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
|
|||||||
@@ -222,7 +222,6 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Reusable media" %}</legend>
|
<legend>{% trans "Reusable media" %}</legend>
|
||||||
{% bootstrap_field sform.reusable_media_active layout="control" %}
|
{% bootstrap_field sform.reusable_media_active layout="control" %}
|
||||||
{% bootstrap_field sform.reusable_media_usage_enforced layout="control" %}
|
|
||||||
<div data-display-dependency="#{{ sform.reusable_media_active.id_for_label }}">
|
<div data-display-dependency="#{{ sform.reusable_media_active.id_for_label }}">
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
|||||||
@@ -72,6 +72,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||||
{% bootstrap_field form.key layout='inline' form_group_class="" %}
|
{% bootstrap_field form.key layout='inline' form_group_class="" %}
|
||||||
|
{% if form.key.help_text %}
|
||||||
|
<span class="help-block">{{ form.key.help_text|safe }}</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 col-md-4 col-lg-5">
|
<div class="col-sm-6 col-md-4 col-lg-5">
|
||||||
{% bootstrap_field form.label layout='inline' form_group_class="" %}
|
{% bootstrap_field form.label layout='inline' form_group_class="" %}
|
||||||
|
|||||||
@@ -58,8 +58,8 @@
|
|||||||
<a href="?{% url_replace request 'ordering' 'identifier' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'ordering' 'identifier' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
<th>{% trans "Media type" context "reusable_media" %}
|
<th>{% trans "Media type" context "reusable_media" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-type' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'type' %}"><i class="fa fa-caret-up"></i></a></th>
|
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||||
<th>{% trans "Connections" context "reusable_media" %}</th>
|
<th>{% trans "Connections" context "reusable_media" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -90,13 +90,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for op in m.linked_orderpositions.all %}
|
{% if m.linked_orderposition %}
|
||||||
<span class="helper-display-block">
|
<span class="helper-display-block">
|
||||||
<span class="fa fa-ticket fa-fw"></span>
|
<span class="fa fa-ticket fa-fw"></span>
|
||||||
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
|
<a href="{% url "control:event.order" event=m.linked_orderposition.order.event.slug organizer=request.organizer.slug code=m.linked_orderposition.order.code %}">
|
||||||
{{ op.order.code }}</a>-{{ op.positionid }}
|
{{ m.linked_orderposition.order.code }}</a>-{{ m.linked_orderposition.positionid }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
{% if m.linked_giftcard %}
|
{% if m.linked_giftcard %}
|
||||||
<span class="helper-display-block">
|
<span class="helper-display-block">
|
||||||
<span class="fa fa-credit-card fa-fw"></span>
|
<span class="fa fa-credit-card fa-fw"></span>
|
||||||
|
|||||||
@@ -26,19 +26,7 @@
|
|||||||
<dt>{% trans "Media type" context "reusable_media" %}</dt>
|
<dt>{% trans "Media type" context "reusable_media" %}</dt>
|
||||||
<dd>{{ medium.get_type_display }}</dd>
|
<dd>{{ medium.get_type_display }}</dd>
|
||||||
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
|
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
|
||||||
<dd>
|
<dd><code>{{ medium.identifier }}</code></dd>
|
||||||
<code id="medium_identifier">{{ medium.identifier }}</code>
|
|
||||||
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#medium_identifier">
|
|
||||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
|
||||||
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
|
||||||
</button>
|
|
||||||
{% if medium.type == "barcode" %}
|
|
||||||
<button type="button" class="btn btn-default btn-xs js-only" data-toggle="qrcode" data-qrcode="{{ medium.identifier }}">
|
|
||||||
<i class="fa fa-qrcode" aria-hidden="true"></i>
|
|
||||||
<span class="sr-only">{% trans "Create QR code" %}</span>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
<dt>{% trans "Status" %}</dt>
|
<dt>{% trans "Status" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if not medium.active %}
|
{% if not medium.active %}
|
||||||
@@ -53,34 +41,34 @@
|
|||||||
<dd>
|
<dd>
|
||||||
{% if medium.customer %}
|
{% if medium.customer %}
|
||||||
<span class="helper-display-block">
|
<span class="helper-display-block">
|
||||||
<span class="fa fa-user fa-fw"></span>
|
<span class="fa fa-user fa-fw"></span>
|
||||||
{% if "organizer.customers:read" in request.orgapermset %}
|
{% if "organizer.customers:read" in request.orgapermset %}
|
||||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=medium.customer.identifier %}">
|
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=medium.customer.identifier %}">
|
||||||
{{ medium.customer }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
{{ medium.customer }}
|
{{ medium.customer }}
|
||||||
{% endif %}
|
</a>
|
||||||
</span>
|
{% else %}
|
||||||
|
{{ medium.customer }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for op in medium.linked_orderpositions.all %}
|
{% if medium.linked_orderposition %}
|
||||||
<span class="helper-display-block">
|
<span class="helper-display-block">
|
||||||
<span class="fa fa-ticket fa-fw"></span>
|
<span class="fa fa-ticket fa-fw"></span>
|
||||||
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
|
<a href="{% url "control:event.order" event=medium.linked_orderposition.order.event.slug organizer=request.organizer.slug code=medium.linked_orderposition.order.code %}">
|
||||||
{{ op.order.code }}</a>-{{ op.positionid }}
|
{{ medium.linked_orderposition.order.code }}</a>-{{ medium.linked_orderposition.positionid }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
{% if medium.linked_giftcard %}
|
{% if medium.linked_giftcard %}
|
||||||
<span class="helper-display-block">
|
<span class="helper-display-block">
|
||||||
<span class="fa fa-credit-card fa-fw"></span>
|
<span class="fa fa-credit-card fa-fw"></span>
|
||||||
{% if "organizer.giftcards:read" in request.orgapermset %}
|
{% if "organizer.giftcards:read" in request.orgapermset %}
|
||||||
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=medium.linked_giftcard.id %}">
|
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=medium.linked_giftcard.id %}">
|
||||||
{{ medium.linked_giftcard.secret }}
|
{{ medium.linked_giftcard.secret }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ medium.linked_giftcard.secret|slice:":3" }}…
|
{{ medium.linked_giftcard.secret|slice:":3" }}…
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
{% if medium.notes %}
|
{% if medium.notes %}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load getitem %}
|
{% load getitem %}
|
||||||
{% load icon %}
|
|
||||||
{% block inner %}
|
{% block inner %}
|
||||||
{% if team %}
|
{% if team %}
|
||||||
<h1>{% trans "Team:" %} {{ team.name }}</h1>
|
<h1>{% trans "Team:" %} {{ team.name }}</h1>
|
||||||
@@ -26,18 +25,6 @@
|
|||||||
<legend>{% trans "Organizer permissions" %}</legend>
|
<legend>{% trans "Organizer permissions" %}</legend>
|
||||||
{% bootstrap_field form.all_organizer_permissions layout="control" %}
|
{% bootstrap_field form.all_organizer_permissions layout="control" %}
|
||||||
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_organizer_permissions" data-inverse>
|
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_organizer_permissions" data-inverse>
|
||||||
<p class="text-muted">
|
|
||||||
{% icon "info-circle" %}
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
Even if a team has no access to a certain category of data, they might still be able to see
|
|
||||||
parts of this data when it is linked to data they can see.
|
|
||||||
{% endblocktrans %}
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
For example, someone with access to customer accounts will be able to see some information
|
|
||||||
about gift cards linked to a customer account, even if they generally can't see gift cards
|
|
||||||
directly.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
{% for f in form.organizer_field_names %}
|
{% for f in form.organizer_field_names %}
|
||||||
{% bootstrap_field form|getitem:f layout="control" %}
|
{% bootstrap_field form|getitem:f layout="control" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -50,17 +37,6 @@
|
|||||||
{% bootstrap_field form.limit_events layout="control" %}
|
{% bootstrap_field form.limit_events layout="control" %}
|
||||||
{% bootstrap_field form.all_event_permissions layout="control" %}
|
{% bootstrap_field form.all_event_permissions layout="control" %}
|
||||||
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_event_permissions" data-inverse>
|
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_event_permissions" data-inverse>
|
||||||
<p class="text-muted">
|
|
||||||
{% icon "info-circle" %}
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
Even if a team has no access to a certain category of data, they might still be able to see
|
|
||||||
parts of this data when it is linked to data they can see.
|
|
||||||
{% endblocktrans %}
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
For example, someone with access to orders will be able to see some information about
|
|
||||||
vouchers used to create an order, even if they generally can't see vouchers directly.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
{% for f in form.event_field_names %}
|
{% for f in form.event_field_names %}
|
||||||
{% bootstrap_field form|getitem:f layout="control" %}
|
{% bootstrap_field form|getitem:f layout="control" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{{ layout|json_script:"editor-data" }}
|
<script type="application/json" id="editor-data">
|
||||||
|
{{ layout|safe }}
|
||||||
|
</script>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="panel panel-default panel-pdf-editor">
|
<div class="panel panel-default panel-pdf-editor">
|
||||||
|
|||||||
@@ -4,306 +4,290 @@
|
|||||||
{% load formset_tags %}
|
{% load formset_tags %}
|
||||||
{% load eventsignal %}
|
{% load eventsignal %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load money %}
|
{% block title %}{% trans "Date" context "subevent" %}{% endblock %}
|
||||||
{% load icon %}
|
|
||||||
{% block title %}{% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }}
|
|
||||||
{% endblocktrans %}{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>
|
{% if not subevent.pk %}
|
||||||
{% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }}{% endblocktrans %}
|
<h1>{% trans "Create date" context "subevent" %}</h1>
|
||||||
{% if 'event.subevents:write' in request.eventpermset %}
|
{% else %}
|
||||||
<a href="{% url "control:event.subevent.edit" event=request.event.slug organizer=request.event.organizer.slug subevent=subevent.pk %}"
|
<h1>{% trans "Date" context "subevent" %}</h1>
|
||||||
class="btn btn-default">
|
{% endif %}
|
||||||
<span class="fa fa-edit"></span>
|
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||||
{% trans "Edit" %}
|
{% csrf_token %}
|
||||||
</a>
|
{% bootstrap_form_errors form %}
|
||||||
{% endif %}
|
{% for f in itemvar_forms %}
|
||||||
</h1>
|
{% bootstrap_form_errors f %}
|
||||||
<div class="row">
|
{% endfor %}
|
||||||
<div class="{% if "event.orders:read" in request.eventpermset %}col-md-5{% else %}col-md-10{% endif %} col-xs-12">
|
<div class="row">
|
||||||
<fieldset>
|
<div class="col-xs-12 {% if subevent.pk %}col-lg-10{% endif %}">
|
||||||
<legend>{% trans "General information" %}</legend>
|
|
||||||
<dl class="dl-horizontal">
|
|
||||||
<dt>{% trans "Name" %}</dt>
|
|
||||||
<dd>{{ subevent.name }}</dd>
|
|
||||||
<dt>{% trans "ID" %}</dt>
|
|
||||||
<dd>#{{ subevent.pk }}</dd>
|
|
||||||
<dt>{% trans "Status" %}</dt>
|
|
||||||
<dd>
|
|
||||||
{% if not subevent.active %}
|
|
||||||
<span class="label label-danger">{% trans "Disabled" %}</span>
|
|
||||||
{% elif subevent.presale_has_ended %}
|
|
||||||
<span class="label label-warning">{% trans "Presale over" %}</span>
|
|
||||||
{% elif not subevent.presale_is_running %}
|
|
||||||
<span class="label label-warning">{% trans "Presale not started" %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="label label-success">{% trans "On sale" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</dd>
|
|
||||||
<dt>{% trans "Event start time" %}</dt>
|
|
||||||
<dd>{{ subevent.date_from|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
|
||||||
<dt>{% trans "Event end time" %}</dt>
|
|
||||||
<dd>{{ subevent.date_to|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
|
||||||
{% if subevent.date_admission %}
|
|
||||||
<dt>{% trans "Admission time" %}</dt>
|
|
||||||
<dd>{{ subevent.date_admission|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
{% if subevent.presale_start %}
|
|
||||||
<dt>{% trans "Start of presale" %}</dt>
|
|
||||||
<dd>{{ subevent.presale_start|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
{% if subevent.presale_end %}
|
|
||||||
<dt>{% trans "End of presale" %}</dt>
|
|
||||||
<dd>{{ subevent.presale_end|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
{% if subevent.location %}
|
|
||||||
<dt>{% trans "Location" %}</dt>
|
|
||||||
<dd>{{ subevent.location|linebreaksbr }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
<dt>{% trans "Show in lists" %}</dt>
|
|
||||||
<dd>{{ subevent.is_public|yesno }}</dd>
|
|
||||||
{% for k, v in subevent.meta_data.items %}
|
|
||||||
<dt>{{ k }}</dt>
|
|
||||||
<dd>{{ v }}</dd>
|
|
||||||
{% endfor %}
|
|
||||||
{% if subevent.comment %}
|
|
||||||
<dt>{% trans "Internal comment" %}</dt>
|
|
||||||
<dd>{{ subevent.comment|linebreaksbr }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
</dl>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>{% trans "Quotas" %}</legend>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover table-quotas">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Quota name" %}</th>
|
|
||||||
<th>{% trans "Products" %}</th>
|
|
||||||
<th>{% trans "Total capacity" %}</th>
|
|
||||||
<th>{% trans "Capacity left" %}</th>
|
|
||||||
<th class="action-col-2"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for q in quotas %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<strong><a
|
|
||||||
href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
|
|
||||||
{% if q.ignore_for_event_availability %}
|
|
||||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip"
|
|
||||||
title="{% trans "Ignore this quota when determining event availability" %}"></span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<ul>
|
|
||||||
{% for item in q.cached_items %}
|
|
||||||
{% if not item.has_variations %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for v in q.variations.all %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
|
|
||||||
{{ v.item }} – {{ v }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
|
||||||
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
|
||||||
<td class="text-right flip">
|
|
||||||
{% if 'event.items:write' in request.eventpermset %}
|
|
||||||
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}"
|
|
||||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{% if checkinlists %}
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Check-in lists" %}</legend>
|
<legend>{% trans "General information" %}</legend>
|
||||||
<div class="table-responsive">
|
{% bootstrap_field form.name layout="control" %}
|
||||||
<table class="table table-hover table-quotas">
|
{% bootstrap_field form.active layout="control" %}
|
||||||
<thead>
|
{% bootstrap_field form.date_from layout="control" %}
|
||||||
<tr>
|
{% bootstrap_field form.date_to layout="control" %}
|
||||||
<th>{% trans "Name" %}</th>
|
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||||
{% if "event.orders:read" in request.eventpermset %}
|
{% bootstrap_field form.date_admission layout="control" %}
|
||||||
<th>{% trans "Checked in" %}</th>
|
{% bootstrap_field form.frontpage_text layout="control" %}
|
||||||
{% endif %}
|
{% bootstrap_field form.is_public layout="control" %}
|
||||||
<th>{% trans "Products" %}</th>
|
{% bootstrap_field form.comment layout="control" %}
|
||||||
<th class="action-col-2"></th>
|
{% if meta_forms %}
|
||||||
</tr>
|
<div class="form-group metadata-group">
|
||||||
</thead>
|
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
|
||||||
<tbody>
|
<div class="col-md-9">
|
||||||
{% for cl in checkinlists %}
|
{% for form in meta_forms %}
|
||||||
<tr>
|
<div class="row">
|
||||||
<td>
|
<div class="col-md-4">
|
||||||
<strong><a
|
<label for="{{ form.value.id_for_label }}">
|
||||||
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
{{ form.property.name }}
|
||||||
</td>
|
</label>
|
||||||
{% if "event.orders:read" in request.eventpermset %}
|
</div>
|
||||||
<td>
|
<div class="col-md-8">
|
||||||
<div class="quotabox availability">
|
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||||
<div class="progress">
|
</div>
|
||||||
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Timeline" %}</legend>
|
||||||
|
{% bootstrap_field form.presale_start layout="control" %}
|
||||||
|
{% bootstrap_field form.presale_end layout="control" %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Quotas" %}</legend>
|
||||||
|
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% bootstrap_formset_errors formset %}
|
||||||
|
<div data-formset-body>
|
||||||
|
{% for form in formset %}
|
||||||
|
<div class="panel panel-default" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ form.id }}
|
||||||
|
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="numbers">
|
<div class="col-md-2 text-right flip">
|
||||||
{{ cl.checkin_count|default_if_none:"0" }} /
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
{{ cl.position_count|default_if_none:"0" }}
|
<i class="fa fa-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</h4>
|
||||||
{% endif %}
|
</div>
|
||||||
<td>
|
<div class="panel-body form-horizontal">
|
||||||
{% if cl.all_products %}
|
{% bootstrap_form_errors form %}
|
||||||
<em>{% trans "All" %}</em>
|
{% bootstrap_field form.size layout="control" %}
|
||||||
{% else %}
|
{% bootstrap_field form.itemvars layout="control" %}
|
||||||
<ul>
|
{% bootstrap_field form.release_after_exit layout="control" %}
|
||||||
{% for item in cl.limit_products.all %}
|
{% bootstrap_field form.ignore_for_event_availability layout="control" %}
|
||||||
<li>
|
</div>
|
||||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
</div>
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-right flip">
|
|
||||||
{% if "event.orders:read" in request.eventpermset %}
|
|
||||||
<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>
|
|
||||||
{% endif %}
|
|
||||||
{% if "event.settings.general:write" 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>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
<script type="form-template" data-formset-empty-form>
|
||||||
|
{% escapescript %}
|
||||||
|
<div class="panel panel-default" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ formset.empty_form.id }}
|
||||||
|
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 text-right flip">
|
||||||
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body form-horizontal">
|
||||||
|
{% bootstrap_field formset.empty_form.size layout="control" %}
|
||||||
|
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
|
||||||
|
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
|
||||||
|
{% bootstrap_field formset.empty_form.ignore_for_event_availability layout="control" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endescapescript %}
|
||||||
|
</script>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-default" data-formset-add>
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
|
||||||
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
|
||||||
{% eventsignal request.event "pretix.control.signals.subevent_detail_html" subevent=subevent %}
|
|
||||||
</div>
|
|
||||||
{% if "event.orders:read" in request.eventpermset %}
|
|
||||||
<div class="col-md-5 col-xs-12">
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>
|
<legend>{% trans "Product settings" %}</legend>
|
||||||
{% trans "Orders" %}
|
<p class="text-muted">
|
||||||
<span class="badge">
|
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
||||||
{{ order_count }}
|
</p>
|
||||||
</span>
|
{% for f in itemvar_forms %}
|
||||||
</legend>
|
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
|
||||||
{% if order_count %}
|
{% bootstrap_form_errors f %}
|
||||||
<div class="table-responsive">
|
<div class="form-group subevent-itemvar-group">
|
||||||
<table class="table table-condensed table-hover table-orders">
|
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||||
<thead>
|
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||||
<tr>
|
</label>
|
||||||
<th>{% trans "Order code" %}</th>
|
<div class="col-md-4">
|
||||||
<th>{% trans "Details" %}</th>
|
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||||
</tr>
|
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
<div class="col-md-4">
|
||||||
{% for o in orders %}
|
<br>
|
||||||
<tr>
|
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||||
<td>
|
</div>
|
||||||
<strong>
|
</div>
|
||||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
<div class="form-group subevent-itemvar-group">
|
||||||
{{ o.code }}
|
<div class="col-md-4 col-md-offset-3">
|
||||||
</a>
|
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||||
</strong>
|
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||||
<br>
|
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
|
||||||
{% if o.testmode %}
|
</div>
|
||||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
<div class="col-md-4">
|
||||||
{% endif %}
|
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||||
{% if o.status == "p" and o.pcnt == 0 %}
|
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||||
{# Everything related to this subevent is canceled #}
|
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||||
<span class="label label-danger">
|
</div>
|
||||||
<span class="fa fa-times"></span>
|
</div>
|
||||||
{% trans "partially canceled" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if "." in o.sales_channel.icon %}
|
|
||||||
<img src="{% static o.sales_channel.icon %}" class="fa-like-image"
|
|
||||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}">
|
|
||||||
{% else %}
|
|
||||||
<span class="fa fa-fw fa-{{ o.sales_channel.icon }} text-muted"
|
|
||||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}"></span>
|
|
||||||
{% endif %}
|
|
||||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
|
||||||
{% if o.email %}
|
|
||||||
<br>{% icon "envelope-o fa-fw text-muted" %}
|
|
||||||
{{ o.email|default_if_none:"" }}
|
|
||||||
{% endif %}
|
|
||||||
{% if o.invoice_address.name %}
|
|
||||||
<br>{% icon "user fa-fw text-muted" %} {{ o.invoice_address.name }}
|
|
||||||
{% endif %}
|
|
||||||
<br>{% icon "ticket text-muted fa-fw" %} {{ o.pcnt }}
|
|
||||||
{% if o.comment %}
|
|
||||||
<br>
|
|
||||||
<span class="text-muted">
|
|
||||||
{{ o.comment|linebreaksbr }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if o.custom_followup_due %}
|
|
||||||
<br>
|
|
||||||
<span class="label label-danger">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
|
|
||||||
TODO {{ date }}{% endblocktrans %}</span>
|
|
||||||
{% elif o.custom_followup_at %}
|
|
||||||
<br>
|
|
||||||
<span class="label label-default">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
|
|
||||||
TODO {{ date }}{% endblocktrans %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{% if order_count > 10 %}
|
{% endfor %}
|
||||||
<p class="text-center">
|
</fieldset>
|
||||||
<a href="{% url "control:event.orders" organizer=request.organizer.slug event=request.event.slug %}?subevent={{ subevent.pk }}"
|
<fieldset>
|
||||||
class="btn btn-default">
|
<legend>{% trans "Check-in lists" %}</legend>
|
||||||
{% trans "View all" %}
|
<p class="help-block">
|
||||||
</a>
|
{% blocktrans trimmed %}
|
||||||
</p>
|
You can choose to either add one or more check-in lists for every date in your series individually,
|
||||||
{% endif %}
|
or use just one check-in list for all your dates and limit admission through check-in rules. Which
|
||||||
{% else %}
|
approach is better depends on multiple factors, such as the number of dates in your series. For a
|
||||||
<div class="empty-collection">
|
series with one or less event date per day, individual lists are usually more helpful. If you
|
||||||
<p>
|
use dates to represent many time slots on the same day, or even overlapping time slots, working with
|
||||||
{% blocktrans trimmed %}
|
just one large check-in list will be easier.
|
||||||
No orders found.
|
{% endblocktrans %}
|
||||||
{% endblocktrans %}
|
</p>
|
||||||
</p>
|
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
||||||
|
{{ cl_formset.management_form }}
|
||||||
|
{% bootstrap_formset_errors cl_formset %}
|
||||||
|
<div data-formset-body>
|
||||||
|
{% for form in cl_formset %}
|
||||||
|
<div class="panel panel-default" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ form.id }}
|
||||||
|
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 text-right flip">
|
||||||
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body form-horizontal">
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
{% bootstrap_field form.include_pending layout="control" %}
|
||||||
|
{% bootstrap_field form.all_products layout="control" %}
|
||||||
|
{% bootstrap_field form.limit_products layout="control" %}
|
||||||
|
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||||
|
{% if form.gates %}
|
||||||
|
{% bootstrap_field form.gates layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<script type="form-template" data-formset-empty-form>
|
||||||
|
{% escapescript %}
|
||||||
|
<div class="panel panel-default" data-formset-form>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ cl_formset.empty_form.id }}
|
||||||
|
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 text-right flip">
|
||||||
|
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body form-horizontal">
|
||||||
|
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
||||||
|
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||||
|
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||||
|
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||||
|
{% if cl_formset.empty_form.gates %}
|
||||||
|
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endescapescript %}
|
||||||
|
</script>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-default" data-formset-add>
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
{% for f in plugin_forms %}
|
||||||
|
{% if f.title %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{ f.title }}</legend>
|
||||||
|
{% if f.template %}
|
||||||
|
{% include f.template with form=f %}
|
||||||
|
{% else %}
|
||||||
|
{% bootstrap_form f layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Additional settings" %}</legend>
|
||||||
|
{% for f in plugin_forms %}
|
||||||
|
{% if not f.title %}
|
||||||
|
{% if f.template %}
|
||||||
|
{% include f.template with form=f %}
|
||||||
|
{% else %}
|
||||||
|
{% bootstrap_form f layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% if subevent.pk %}
|
||||||
<div class="col-md-2 col-xs-12">
|
<div class="col-xs-12 col-lg-2">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
{% trans "Date history" context "subevent" %}
|
{% trans "Date history" context "subevent" %}
|
||||||
</h3>
|
</h3>
|
||||||
|
</div>
|
||||||
|
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-group submit-group submit-group-sticky">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
{% extends "pretixcontrol/event/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% load formset_tags %}
|
|
||||||
{% load eventsignal %}
|
|
||||||
{% load static %}
|
|
||||||
{% block title %}{% trans "Date" context "subevent" %}{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
{% if not subevent.pk %}
|
|
||||||
<h1>{% trans "Create date" context "subevent" %}</h1>
|
|
||||||
{% else %}
|
|
||||||
<h1>{% trans "Date" context "subevent" %}</h1>
|
|
||||||
{% endif %}
|
|
||||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% bootstrap_form_errors form %}
|
|
||||||
{% for f in itemvar_forms %}
|
|
||||||
{% bootstrap_form_errors f %}
|
|
||||||
{% endfor %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 {% if subevent.pk %}col-lg-10{% endif %}">
|
|
||||||
<fieldset>
|
|
||||||
<legend>{% trans "General information" %}</legend>
|
|
||||||
{% bootstrap_field form.name layout="control" %}
|
|
||||||
{% bootstrap_field form.active layout="control" %}
|
|
||||||
{% bootstrap_field form.date_from layout="control" %}
|
|
||||||
{% bootstrap_field form.date_to layout="control" %}
|
|
||||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
|
||||||
{% bootstrap_field form.date_admission layout="control" %}
|
|
||||||
{% bootstrap_field form.frontpage_text layout="control" %}
|
|
||||||
{% bootstrap_field form.is_public layout="control" %}
|
|
||||||
{% bootstrap_field form.comment layout="control" %}
|
|
||||||
{% if meta_forms %}
|
|
||||||
<div class="form-group metadata-group">
|
|
||||||
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
|
|
||||||
<div class="col-md-9">
|
|
||||||
{% for form in meta_forms %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="{{ form.value.id_for_label }}">
|
|
||||||
{{ form.property.name }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>{% trans "Timeline" %}</legend>
|
|
||||||
{% bootstrap_field form.presale_start layout="control" %}
|
|
||||||
{% bootstrap_field form.presale_end layout="control" %}
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>{% trans "Quotas" %}</legend>
|
|
||||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
|
||||||
{{ formset.management_form }}
|
|
||||||
{% bootstrap_formset_errors formset %}
|
|
||||||
<div data-formset-body>
|
|
||||||
{% for form in formset %}
|
|
||||||
<div class="panel panel-default" data-formset-form>
|
|
||||||
<div class="sr-only">
|
|
||||||
{{ form.id }}
|
|
||||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-10">
|
|
||||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 text-right flip">
|
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body form-horizontal">
|
|
||||||
{% bootstrap_form_errors form %}
|
|
||||||
{% bootstrap_field form.size layout="control" %}
|
|
||||||
{% bootstrap_field form.itemvars layout="control" %}
|
|
||||||
{% bootstrap_field form.release_after_exit layout="control" %}
|
|
||||||
{% bootstrap_field form.ignore_for_event_availability layout="control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<script type="form-template" data-formset-empty-form>
|
|
||||||
{% escapescript %}
|
|
||||||
<div class="panel panel-default" data-formset-form>
|
|
||||||
<div class="sr-only">
|
|
||||||
{{ formset.empty_form.id }}
|
|
||||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-10">
|
|
||||||
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 text-right flip">
|
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body form-horizontal">
|
|
||||||
{% bootstrap_field formset.empty_form.size layout="control" %}
|
|
||||||
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
|
|
||||||
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
|
|
||||||
{% bootstrap_field formset.empty_form.ignore_for_event_availability layout="control" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endescapescript %}
|
|
||||||
</script>
|
|
||||||
<p>
|
|
||||||
<button type="button" class="btn btn-default" data-formset-add>
|
|
||||||
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
|
|
||||||
</p>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>{% trans "Product settings" %}</legend>
|
|
||||||
<p class="text-muted">
|
|
||||||
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
|
||||||
</p>
|
|
||||||
{% for f in itemvar_forms %}
|
|
||||||
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
|
|
||||||
{% bootstrap_form_errors f %}
|
|
||||||
<div class="form-group subevent-itemvar-group">
|
|
||||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
|
||||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
|
||||||
</label>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
|
||||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<br>
|
|
||||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group subevent-itemvar-group">
|
|
||||||
<div class="col-md-4 col-md-offset-3">
|
|
||||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
|
||||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
|
||||||
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
|
||||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
|
||||||
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>{% trans "Check-in lists" %}</legend>
|
|
||||||
<p class="help-block">
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
You can choose to either add one or more check-in lists for every date in your series individually,
|
|
||||||
or use just one check-in list for all your dates and limit admission through check-in rules. Which
|
|
||||||
approach is better depends on multiple factors, such as the number of dates in your series. For a
|
|
||||||
series with one or less event date per day, individual lists are usually more helpful. If you
|
|
||||||
use dates to represent many time slots on the same day, or even overlapping time slots, working with
|
|
||||||
just one large check-in list will be easier.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
|
||||||
{{ cl_formset.management_form }}
|
|
||||||
{% bootstrap_formset_errors cl_formset %}
|
|
||||||
<div data-formset-body>
|
|
||||||
{% for form in cl_formset %}
|
|
||||||
<div class="panel panel-default" data-formset-form>
|
|
||||||
<div class="sr-only">
|
|
||||||
{{ form.id }}
|
|
||||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-10">
|
|
||||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 text-right flip">
|
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body form-horizontal">
|
|
||||||
{% bootstrap_form_errors form %}
|
|
||||||
{% bootstrap_field form.include_pending layout="control" %}
|
|
||||||
{% bootstrap_field form.all_products layout="control" %}
|
|
||||||
{% bootstrap_field form.limit_products layout="control" %}
|
|
||||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
|
||||||
{% if form.gates %}
|
|
||||||
{% bootstrap_field form.gates layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<script type="form-template" data-formset-empty-form>
|
|
||||||
{% escapescript %}
|
|
||||||
<div class="panel panel-default" data-formset-form>
|
|
||||||
<div class="sr-only">
|
|
||||||
{{ cl_formset.empty_form.id }}
|
|
||||||
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
|
||||||
</div>
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-10">
|
|
||||||
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 text-right flip">
|
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body form-horizontal">
|
|
||||||
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
|
||||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
|
||||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
|
||||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
|
||||||
{% if cl_formset.empty_form.gates %}
|
|
||||||
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endescapescript %}
|
|
||||||
</script>
|
|
||||||
<p>
|
|
||||||
<button type="button" class="btn btn-default" data-formset-add>
|
|
||||||
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</fieldset>
|
|
||||||
{% for f in plugin_forms %}
|
|
||||||
{% if f.title %}
|
|
||||||
<fieldset>
|
|
||||||
<legend>{{ f.title }}</legend>
|
|
||||||
{% if f.template %}
|
|
||||||
{% include f.template with form=f %}
|
|
||||||
{% else %}
|
|
||||||
{% bootstrap_form f layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
</fieldset>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<fieldset>
|
|
||||||
<legend>{% trans "Additional settings" %}</legend>
|
|
||||||
{% for f in plugin_forms %}
|
|
||||||
{% if not f.title %}
|
|
||||||
{% if f.template %}
|
|
||||||
{% include f.template with form=f %}
|
|
||||||
{% else %}
|
|
||||||
{% bootstrap_form f layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
{% if subevent.pk %}
|
|
||||||
<div class="col-xs-12 col-lg-2">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3 class="panel-title">
|
|
||||||
{% trans "Date history" context "subevent" %}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="form-group submit-group submit-group-sticky">
|
|
||||||
<a href="{{ next_url }}" class="btn btn-default btn-cancel">
|
|
||||||
{% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
|
||||||
{% trans "Save" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}">
|
<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>
|
{{ s.name }}</a></strong><br>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
#{{ s.pk }}
|
#{{ s.pk }}
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if "event.subevents:write" in request.eventpermset %}
|
{% if "event.subevents:write" in request.eventpermset %}
|
||||||
<a href="{% url "control:event.subevent.edit" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
|
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
|
||||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
||||||
data-toggle="dropdown">
|
data-toggle="dropdown">
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<a href="{% url "control:event.subevent.delete" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
<a href="{% url "control:event.subevent.delete" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -308,8 +308,7 @@ urlpatterns = [
|
|||||||
re_path(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
|
re_path(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
|
||||||
re_path(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
|
re_path(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
|
||||||
re_path(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
|
re_path(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
|
||||||
re_path(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventDetail.as_view(), name='event.subevent'),
|
re_path(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),
|
||||||
re_path(r'^subevents/(?P<subevent>\d+)/edit$', subevents.SubEventUpdate.as_view(), name='event.subevent.edit'),
|
|
||||||
re_path(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
|
re_path(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
|
||||||
name='event.subevent.delete'),
|
name='event.subevent.delete'),
|
||||||
re_path(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'),
|
re_path(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ from i18nfield.strings import LazyI18nString
|
|||||||
|
|
||||||
from pretix.api.views.checkin import _redeem_process
|
from pretix.api.views.checkin import _redeem_process
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, Item, LogEntry, Order, OrderPosition
|
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
|
||||||
from pretix.base.models.checkin import CheckinList
|
from pretix.base.models.checkin import CheckinList
|
||||||
from pretix.base.models.orders import PrintLog
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.permissions import AnyPermissionOf
|
from pretix.base.permissions import AnyPermissionOf
|
||||||
@@ -401,14 +401,13 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
{
|
{
|
||||||
'id': i.pk,
|
'id': i.pk,
|
||||||
'name': str(i),
|
'name': str(i),
|
||||||
'active': i.active,
|
|
||||||
'variations': [
|
'variations': [
|
||||||
{
|
{
|
||||||
'id': v.pk,
|
'id': v.pk,
|
||||||
'name': str(v.value)
|
'name': str(v.value)
|
||||||
} for v in i.variations.all()
|
} for v in i.variations.all()
|
||||||
]
|
]
|
||||||
} for i in self.request.event.items.prefetch_related('variations')
|
} for i in self.request.event.items.filter(active=True).prefetch_related('variations')
|
||||||
],
|
],
|
||||||
**super().get_context_data(),
|
**super().get_context_data(),
|
||||||
}
|
}
|
||||||
@@ -533,8 +532,6 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
|||||||
checkinlist=self.list,
|
checkinlist=self.list,
|
||||||
result=self.result,
|
result=self.result,
|
||||||
reason_labels=dict(Checkin.REASONS),
|
reason_labels=dict(Checkin.REASONS),
|
||||||
media_policies=dict(Item.MEDIA_POLICIES),
|
|
||||||
media_types=dict(MEDIA_TYPES),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from pretix.control.permissions import (
|
|||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
|
|
||||||
from ...helpers.compat import CompatDeleteView
|
from ...helpers.compat import CompatDeleteView
|
||||||
from . import CreateView, UpdateView
|
from . import CreateView, PaginationMixin, UpdateView
|
||||||
|
|
||||||
|
|
||||||
class DiscountDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
class DiscountDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||||
@@ -183,7 +183,7 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class DiscountList(ListView):
|
class DiscountList(PaginationMixin, ListView):
|
||||||
model = Discount
|
model = Discount
|
||||||
context_object_name = 'discounts'
|
context_object_name = 'discounts'
|
||||||
template_name = 'pretixcontrol/items/discounts.html'
|
template_name = 'pretixcontrol/items/discounts.html'
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlparse, urlsplit
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
@@ -64,6 +64,7 @@ from django.shortcuts import get_object_or_404, redirect
|
|||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import conditional_escape, format_html
|
from django.utils.html import conditional_escape, format_html
|
||||||
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
|
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
|
||||||
@@ -96,9 +97,7 @@ from pretix.control.permissions import EventPermissionRequiredMixin
|
|||||||
from pretix.control.views.mailsetup import MailSettingsSetupView
|
from pretix.control.views.mailsetup import MailSettingsSetupView
|
||||||
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
||||||
from pretix.helpers.database import rolledback_transaction
|
from pretix.helpers.database import rolledback_transaction
|
||||||
from pretix.multidomain.urlreverse import (
|
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
|
||||||
eventreverse_absolute, get_event_domain,
|
|
||||||
)
|
|
||||||
from pretix.presale.views.widget import (
|
from pretix.presale.views.widget import (
|
||||||
version_default as widget_version_default,
|
version_default as widget_version_default,
|
||||||
)
|
)
|
||||||
@@ -1149,11 +1148,8 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
|||||||
if request.POST.get("delete") == "yes":
|
if request.POST.get("delete") == "yes":
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
Order.gracefully_delete_bulk(
|
for order in request.event.orders.filter(testmode=True):
|
||||||
request.event,
|
order.gracefully_delete(user=self.request.user)
|
||||||
request.event.orders.filter(testmode=True),
|
|
||||||
user=self.request.user
|
|
||||||
)
|
|
||||||
except ProtectedError:
|
except ProtectedError:
|
||||||
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
|
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
|
||||||
'created by plug-ins) do not allow it.'))
|
'created by plug-ins) do not allow it.'))
|
||||||
@@ -1738,10 +1734,10 @@ class EventQRCode(EventPermissionRequiredMixin, View):
|
|||||||
permission = None
|
permission = None
|
||||||
|
|
||||||
def get(self, request, *args, filetype, **kwargs):
|
def get(self, request, *args, filetype, **kwargs):
|
||||||
url = eventreverse_absolute(request.event, 'presale:event.index')
|
url = build_absolute_uri(request.event, 'presale:event.index')
|
||||||
|
|
||||||
if "url" in request.GET:
|
if "url" in request.GET:
|
||||||
if request.GET["url"].startswith(url):
|
if url_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]):
|
||||||
url = request.GET["url"]
|
url = request.GET["url"]
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied("Untrusted URL")
|
raise PermissionDenied("Untrusted URL")
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class CategoryList(ListView):
|
class CategoryList(PaginationMixin, ListView):
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
context_object_name = 'categories'
|
context_object_name = 'categories'
|
||||||
template_name = 'pretixcontrol/items/categories.html'
|
template_name = 'pretixcontrol/items/categories.html'
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ from pretix.helpers import OF_SELF
|
|||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
from pretix.helpers.hierarkey import clean_filename
|
||||||
from pretix.helpers.iter import chunked_iterable
|
|
||||||
from pretix.helpers.json import CustomJSONEncoder
|
from pretix.helpers.json import CustomJSONEncoder
|
||||||
from pretix.helpers.safedownload import check_token
|
from pretix.helpers.safedownload import check_token
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
@@ -241,7 +240,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
||||||
qs = self.allowed_for(self.get_queryset())
|
qs = self.allowed_for(self.allowed_for(self.get_queryset()))
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
orders_with_successful_action = 0
|
orders_with_successful_action = 0
|
||||||
for i, o in enumerate(qs):
|
for i, o in enumerate(qs):
|
||||||
@@ -395,21 +394,8 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
|
|||||||
testmode=True,
|
testmode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
def execute_single(self, instance, form: forms.Form):
|
||||||
qs = self.allowed_for(self.get_queryset())
|
instance.gracefully_delete(user=self.request.user)
|
||||||
total = qs.count()
|
|
||||||
all_ids = list(qs.values_list("id", flat=True))
|
|
||||||
|
|
||||||
orders_with_successful_action = 0
|
|
||||||
for chunk in chunked_iterable(all_ids, 1000):
|
|
||||||
Order.gracefully_delete_bulk(
|
|
||||||
self.request.event,
|
|
||||||
qs.filter(id__in=chunk),
|
|
||||||
user=self.request.user,
|
|
||||||
)
|
|
||||||
orders_with_successful_action += len(chunk)
|
|
||||||
self.async_set_progress(orders_with_successful_action / total * 100)
|
|
||||||
return orders_with_successful_action, total
|
|
||||||
|
|
||||||
|
|
||||||
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import get_current_timezone, now
|
from django.utils.timezone import get_current_timezone, now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _, ngettext
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
@@ -91,7 +91,9 @@ from pretix.base.models import (
|
|||||||
ReusableMedium, ScheduledOrganizerExport, Team, TeamInvite, User,
|
ReusableMedium, ScheduledOrganizerExport, Team, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
from pretix.base.models.event import (
|
||||||
|
Event, EventMetaProperty, EventMetaValue, SubEvent, SubEventMetaValue,
|
||||||
|
)
|
||||||
from pretix.base.models.giftcards import (
|
from pretix.base.models.giftcards import (
|
||||||
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
|
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
|
||||||
)
|
)
|
||||||
@@ -139,8 +141,8 @@ from pretix.helpers import OF_SELF, GroupConcat
|
|||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.forms.customer import TokenGenerator
|
from pretix.presale.forms.customer import TokenGenerator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1039,7 +1041,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
|||||||
'user': self,
|
'user': self,
|
||||||
'organizer': self.request.organizer.name,
|
'organizer': self.request.organizer.name,
|
||||||
'team': instance.team.name,
|
'team': instance.team.name,
|
||||||
'url': mainreverse_absolute('control:auth.invite', kwargs={
|
'url': build_global_uri('control:auth.invite', kwargs={
|
||||||
'token': instance.token
|
'token': instance.token
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -2466,11 +2468,51 @@ class EventMetaPropertyEditorMixin:
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def formset(self):
|
def formset(self):
|
||||||
return EventMetaPropertyAllowedValueFormSet(
|
formset = EventMetaPropertyAllowedValueFormSet(
|
||||||
data=self.request.POST if self.request.method == "POST" else None,
|
data=self.request.POST if self.request.method == "POST" else None,
|
||||||
organizer=self.request.organizer,
|
organizer=self.request.organizer,
|
||||||
initial=(self.object.choices or []) if self.object else [],
|
initial=(self.object.choices or []) if self.object else [],
|
||||||
)
|
)
|
||||||
|
if self.event_value_counts or self.subevent_value_counts:
|
||||||
|
for form in formset.initial_forms:
|
||||||
|
uses = []
|
||||||
|
key = form.initial['key']
|
||||||
|
if key in self.event_value_counts:
|
||||||
|
count = self.event_value_counts[key]
|
||||||
|
uses += [ngettext("%d event", "%d events", count) % count]
|
||||||
|
if key in self.subevent_value_counts:
|
||||||
|
count = self.subevent_value_counts[key]
|
||||||
|
uses += [ngettext("%d subevent", "%d subevents", count) % count]
|
||||||
|
if uses:
|
||||||
|
form.fields['key'].help_text = _("Value can not be changed because it is in use (%s).") % (", ".join(uses))
|
||||||
|
form.fields['key'].widget.attrs['readonly'] = True
|
||||||
|
return formset
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def event_value_counts(self):
|
||||||
|
if self.object:
|
||||||
|
return {
|
||||||
|
d['attr_value']: d['count']
|
||||||
|
for d in self.request.organizer.events.annotate(
|
||||||
|
attr_value=Subquery(EventMetaValue.objects.filter(
|
||||||
|
event=OuterRef('pk'),
|
||||||
|
property__name=self.object.name
|
||||||
|
).values('value')), count=Count('attr_value')
|
||||||
|
).values('attr_value', 'count')
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def subevent_value_counts(self):
|
||||||
|
if self.object:
|
||||||
|
return {
|
||||||
|
d['attr_value']: d['count']
|
||||||
|
for d in SubEvent.objects.filter(event__organizer=self.request.organizer).annotate(
|
||||||
|
attr_value=Subquery(SubEventMetaValue.objects.filter(
|
||||||
|
subevent=OuterRef('pk'),
|
||||||
|
property__name=self.object.name,
|
||||||
|
).values('value')), count=Count('attr_value')
|
||||||
|
).values('attr_value', 'count')
|
||||||
|
}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
@@ -2493,10 +2535,28 @@ class EventMetaPropertyEditorMixin:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def all_existing_values_valid(self):
|
||||||
|
if not self.event_value_counts and not self.subevent_value_counts:
|
||||||
|
return True
|
||||||
|
choice_keys = set(
|
||||||
|
f.cleaned_data.get("key") for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
||||||
|
)
|
||||||
|
if not choice_keys:
|
||||||
|
return True
|
||||||
|
existing_values = (self.event_value_counts.keys() | self.subevent_value_counts.keys()) - {None}
|
||||||
|
missing_choices = existing_values - choice_keys
|
||||||
|
if missing_choices:
|
||||||
|
messages.error(self.request, _(
|
||||||
|
"When restricting the allowed values, you need to allow all values that already exist "
|
||||||
|
"on your events. Missing values: %s"
|
||||||
|
) % (", ".join(missing_choices)))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object(self.get_queryset())
|
self.object = self.get_object(self.get_queryset())
|
||||||
self.form = self.get_form()
|
self.form = self.get_form()
|
||||||
if self.form.is_valid() and self.formset.is_valid() and self.is_default_valid():
|
if self.form.is_valid() and self.formset.is_valid() and self.is_default_valid() and self.all_existing_values_valid():
|
||||||
return self.form_valid(self.form)
|
return self.form_valid(self.form)
|
||||||
else:
|
else:
|
||||||
return self.form_invalid(self.form)
|
return self.form_invalid(self.form)
|
||||||
@@ -2851,12 +2911,10 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['redirect_uri'] = eventreverse_absolute(
|
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return',
|
||||||
self.request.organizer, 'presale:organizer.customer.login.return',
|
kwargs={
|
||||||
kwargs={
|
'provider': self.object.pk
|
||||||
'provider': self.object.pk
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
@@ -3087,7 +3145,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
|
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
|
||||||
ctx = self.customer.get_email_context()
|
ctx = self.customer.get_email_context()
|
||||||
token = TokenGenerator().make_token(self.customer)
|
token = TokenGenerator().make_token(self.customer)
|
||||||
ctx['url'] = eventreverse_absolute(
|
ctx['url'] = build_absolute_uri(
|
||||||
self.request.organizer,
|
self.request.organizer,
|
||||||
'presale:organizer.customer.recoverpw'
|
'presale:organizer.customer.recoverpw'
|
||||||
) + '?id=' + self.customer.identifier + '&token=' + token
|
) + '?id=' + self.customer.identifier + '&token=' + token
|
||||||
@@ -3386,10 +3444,8 @@ class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequire
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.organizer.reusable_media.select_related(
|
qs = self.request.organizer.reusable_media.select_related(
|
||||||
'customer',
|
'customer', 'linked_orderposition', 'linked_orderposition__order', 'linked_orderposition__order__event',
|
||||||
'linked_giftcard',
|
'linked_giftcard'
|
||||||
).prefetch_related(
|
|
||||||
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order", "order__event"))
|
|
||||||
)
|
)
|
||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
qs = self.filter_form.filter_qs(qs)
|
qs = self.filter_form.filter_qs(qs)
|
||||||
@@ -3437,14 +3493,10 @@ class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
r = super().form_valid(form)
|
r = super().form_valid(form)
|
||||||
|
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data={
|
||||||
data = {
|
|
||||||
k: getattr(form.instance, k)
|
k: getattr(form.instance, k)
|
||||||
for k in form.changed_data
|
for k in form.changed_data
|
||||||
}
|
})
|
||||||
if "linked_orderpositions" in data:
|
|
||||||
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
|
|
||||||
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data=data)
|
|
||||||
messages.success(self.request, _('Your changes have been saved.'))
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@@ -3469,40 +3521,13 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
|
|||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
prev_linked_ops_pks = list(getattr(self.object, "linked_orderpositions").values_list("pk", flat=True))
|
|
||||||
result = super().form_valid(form)
|
|
||||||
if form.has_changed():
|
if form.has_changed():
|
||||||
data = {
|
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data={
|
||||||
k: getattr(self.object, k)
|
k: getattr(self.object, k)
|
||||||
for k in form.changed_data
|
for k in form.changed_data
|
||||||
}
|
})
|
||||||
if "linked_orderpositions" in data:
|
|
||||||
# handle changes to linked_orderpositions separately
|
|
||||||
linked_ops_pks = data["linked_orderpositions"].values_list("pk", flat=True)
|
|
||||||
del data["linked_orderpositions"]
|
|
||||||
for op_pk in prev_linked_ops_pks:
|
|
||||||
if op_pk not in linked_ops_pks:
|
|
||||||
self.object.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.removed',
|
|
||||||
user=self.request.user,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': op_pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for op_pk in linked_ops_pks:
|
|
||||||
if op_pk not in prev_linked_ops_pks:
|
|
||||||
self.object.log_action(
|
|
||||||
'pretix.reusable_medium.linked_orderposition.added',
|
|
||||||
user=self.request.user,
|
|
||||||
data={
|
|
||||||
'linked_orderposition': op_pk,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if data:
|
|
||||||
# log change-action only for changes other than linked_orderpositions
|
|
||||||
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data=data)
|
|
||||||
messages.success(self.request, _('Your changes have been saved.'))
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
return result
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse('control:organizer.reusable_medium', kwargs={
|
return reverse('control:organizer.reusable_medium', kwargs={
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
ctx['pdf'] = self.get_current_background()
|
ctx['pdf'] = self.get_current_background()
|
||||||
ctx['variables'] = self.get_variables()
|
ctx['variables'] = self.get_variables()
|
||||||
ctx['images'] = self.get_images()
|
ctx['images'] = self.get_images()
|
||||||
ctx['layout'] = self.get_current_layout()
|
ctx['layout'] = json.dumps(self.get_current_layout())
|
||||||
ctx['title'] = self.title
|
ctx['title'] = self.title
|
||||||
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
||||||
ctx['maxfilesize'] = self.maxfilesize
|
ctx['maxfilesize'] = self.maxfilesize
|
||||||
|
|||||||
@@ -41,9 +41,7 @@ from django.contrib import messages
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import Count, F, Prefetch, ProtectedError
|
||||||
Count, Exists, F, OuterRef, Prefetch, ProtectedError, Subquery,
|
|
||||||
)
|
|
||||||
from django.db.models.functions import Coalesce, TruncDate, TruncTime
|
from django.db.models.functions import Coalesce, TruncDate, TruncTime
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import inlineformset_factory
|
||||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||||
@@ -51,21 +49,17 @@ from django.shortcuts import redirect, render
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.formats import get_format
|
from django.utils.formats import get_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import (
|
from django.views.generic import CreateView, FormView, ListView, UpdateView
|
||||||
CreateView, DetailView, FormView, ListView, UpdateView,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pretix.base.models import CartPosition, LogEntry, OrderPosition
|
from pretix.base.models import CartPosition, LogEntry
|
||||||
from pretix.base.models.checkin import CheckinList
|
from pretix.base.models.checkin import CheckinList
|
||||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
Item, ItemVariation, Quota, SubEventItem, SubEventItemVariation,
|
ItemVariation, Quota, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import CancellationRequest
|
|
||||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||||
from pretix.base.services import tickets
|
from pretix.base.services import tickets
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
@@ -511,69 +505,9 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
|||||||
) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms)
|
) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms)
|
||||||
|
|
||||||
|
|
||||||
class SubEventDetail(EventPermissionRequiredMixin, DetailView):
|
|
||||||
model = SubEvent
|
|
||||||
template_name = 'pretixcontrol/subevents/detail.html'
|
|
||||||
permission = None
|
|
||||||
context_object_name = 'subevent'
|
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> SubEvent:
|
|
||||||
try:
|
|
||||||
return self.request.event.subevents.get(
|
|
||||||
id=self.kwargs['subevent']
|
|
||||||
)
|
|
||||||
except SubEvent.DoesNotExist:
|
|
||||||
raise Http404(pgettext_lazy("subevent", "The requested date does not exist."))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
oqs = self.request.event.orders.filter(
|
|
||||||
Exists(
|
|
||||||
OrderPosition.objects.filter(
|
|
||||||
subevent=self.object,
|
|
||||||
order_id=OuterRef("id"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).annotate(
|
|
||||||
pcnt=Subquery(
|
|
||||||
OrderPosition.objects.filter(
|
|
||||||
subevent=self.object,
|
|
||||||
order_id=OuterRef("id"),
|
|
||||||
).values("subevent").annotate(c=Count("*")).values("c")
|
|
||||||
),
|
|
||||||
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))),
|
|
||||||
).select_related("invoice_address").prefetch_related("sales_channel")
|
|
||||||
ctx = {
|
|
||||||
"quotas": self.object.quotas.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
"items",
|
|
||||||
queryset=Item.objects.annotate(
|
|
||||||
has_variations=Exists(ItemVariation.objects.filter(item=OuterRef("pk")))
|
|
||||||
),
|
|
||||||
to_attr="cached_items"
|
|
||||||
),
|
|
||||||
"variations",
|
|
||||||
"variations__item",
|
|
||||||
).order_by("name", "pk"),
|
|
||||||
"checkinlists": self.object.checkinlist_set.prefetch_related("limit_products"),
|
|
||||||
"orders": oqs[:11],
|
|
||||||
"order_count": oqs.count(),
|
|
||||||
}
|
|
||||||
|
|
||||||
qa = QuotaAvailability()
|
|
||||||
qa.queue(*ctx["quotas"])
|
|
||||||
qa.compute()
|
|
||||||
for quota in ctx["quotas"]:
|
|
||||||
quota.cached_avail = qa.results[quota]
|
|
||||||
|
|
||||||
return super().get_context_data(
|
|
||||||
**kwargs,
|
|
||||||
**ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
|
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
template_name = 'pretixcontrol/subevents/edit.html'
|
template_name = 'pretixcontrol/subevents/detail.html'
|
||||||
permission = 'event.subevents:write'
|
permission = 'event.subevents:write'
|
||||||
context_object_name = 'subevent'
|
context_object_name = 'subevent'
|
||||||
form_class = SubEventForm
|
form_class = SubEventForm
|
||||||
@@ -639,28 +573,20 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
|
|||||||
return HttpResponseRedirect(self.get_success_url())
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
|
return reverse('control:event.subevents', kwargs={
|
||||||
return self.request.GET.get("next")
|
|
||||||
return reverse('control:event.subevent', kwargs={
|
|
||||||
'organizer': self.request.event.organizer.slug,
|
'organizer': self.request.event.organizer.slug,
|
||||||
'event': self.request.event.slug,
|
'event': self.request.event.slug,
|
||||||
'subevent': self.object.pk,
|
}) + ('?' + self.request.GET.get('returnto') if 'returnto' in self.request.GET else '')
|
||||||
})
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs['event'] = self.request.event
|
kwargs['event'] = self.request.event
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
return super().get_context_data(
|
|
||||||
next_url=self.get_success_url()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
|
class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
template_name = 'pretixcontrol/subevents/edit.html'
|
template_name = 'pretixcontrol/subevents/detail.html'
|
||||||
permission = 'event.subevents:write'
|
permission = 'event.subevents:write'
|
||||||
context_object_name = 'subevent'
|
context_object_name = 'subevent'
|
||||||
form_class = SubEventForm
|
form_class = SubEventForm
|
||||||
|
|||||||
@@ -145,21 +145,11 @@ def event_list(request):
|
|||||||
if 'can_copy' in request.GET:
|
if 'can_copy' in request.GET:
|
||||||
qs = EventWizardCopyForm.copy_from_queryset(request.user, request.session)
|
qs = EventWizardCopyForm.copy_from_queryset(request.user, request.session)
|
||||||
else:
|
else:
|
||||||
permission = request.GET.get('permission')
|
qs = request.user.get_events_with_any_permission(request)
|
||||||
if permission:
|
|
||||||
qs = request.user.get_events_with_permission(permission, request)
|
|
||||||
else:
|
|
||||||
qs = request.user.get_events_with_any_permission(request)
|
|
||||||
|
|
||||||
name_slug_q = Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query)
|
|
||||||
organizer = request.GET.get('organizer')
|
|
||||||
if organizer:
|
|
||||||
qs = qs.filter(organizer__slug=organizer)
|
|
||||||
else:
|
|
||||||
name_slug_q |= Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
|
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
name_slug_q
|
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) |
|
||||||
|
Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
|
||||||
).annotate(
|
).annotate(
|
||||||
min_from=Min('subevents__date_from'),
|
min_from=Min('subevents__date_from'),
|
||||||
max_from=Max('subevents__date_from'),
|
max_from=Max('subevents__date_from'),
|
||||||
@@ -172,19 +162,10 @@ def event_list(request):
|
|||||||
total = qs.count()
|
total = qs.count()
|
||||||
pagesize = 20
|
pagesize = 20
|
||||||
offset = (page - 1) * pagesize
|
offset = (page - 1) * pagesize
|
||||||
results = []
|
|
||||||
if page == 1 and 'include_none' in request.GET and not query:
|
|
||||||
results.append({
|
|
||||||
'id': "_none",
|
|
||||||
'text': _("No event"),
|
|
||||||
'name': _("No event"),
|
|
||||||
'type': "event",
|
|
||||||
})
|
|
||||||
results += [
|
|
||||||
serialize_event(e) for e in qs.select_related('organizer')[offset:offset + pagesize]
|
|
||||||
]
|
|
||||||
doc = {
|
doc = {
|
||||||
'results': results,
|
'results': [
|
||||||
|
serialize_event(e) for e in qs.select_related('organizer')[offset:offset + pagesize]
|
||||||
|
],
|
||||||
'pagination': {
|
'pagination': {
|
||||||
"more": total >= (offset + pagesize)
|
"more": total >= (offset + pagesize)
|
||||||
}
|
}
|
||||||
@@ -200,7 +181,7 @@ def giftcard_select2(request, **kwargs):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
page = 1
|
page = 1
|
||||||
|
|
||||||
if request.user.has_organizer_permission(request.organizer, 'organizer.giftcards:read', request):
|
if request.user.has_organizer_permission(request.organizer, 'organizer.giftcards:write', request):
|
||||||
qs = request.organizer.issued_gift_cards.filter(
|
qs = request.organizer.issued_gift_cards.filter(
|
||||||
Q(secret__icontains=query)
|
Q(secret__icontains=query)
|
||||||
).order_by('secret')
|
).order_by('secret')
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ from pretix.control.views import PaginationMixin
|
|||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||||
@@ -338,7 +338,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
}
|
}
|
||||||
if self.object.subevent_id:
|
if self.object.subevent_id:
|
||||||
url_params['subevent'] = self.object.subevent_id
|
url_params['subevent'] = self.object.subevent_id
|
||||||
ctx['url'] = eventreverse_absolute(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
|
ctx['url'] = build_absolute_uri(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,3 @@ class PretixHelpersConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
from .monkeypatching import monkeypatch_all_at_ready
|
from .monkeypatching import monkeypatch_all_at_ready
|
||||||
monkeypatch_all_at_ready()
|
monkeypatch_all_at_ready()
|
||||||
|
|
||||||
# Ensure reportlab does not make any calls to the internet or the local disk
|
|
||||||
from reportlab import rl_config
|
|
||||||
rl_config.trustedHosts = []
|
|
||||||
rl_config.trustedSchemes = ['data']
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ from datetime import datetime
|
|||||||
from http import cookies
|
from http import cookies
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import SuspiciousFileOperation
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib3.connection import HTTPConnection, HTTPSConnection
|
from urllib3.connection import HTTPConnection, HTTPSConnection
|
||||||
@@ -41,10 +40,6 @@ from urllib3.util.connection import (
|
|||||||
)
|
)
|
||||||
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
||||||
|
|
||||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
|
||||||
|
|
||||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_vobject_performance():
|
def monkeypatch_vobject_performance():
|
||||||
"""
|
"""
|
||||||
@@ -151,15 +146,12 @@ def monkeypatch_urllib3_ssrf_protection():
|
|||||||
|
|
||||||
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||||
ip_addr = ipaddress.ip_address(sa[0])
|
ip_addr = ipaddress.ip_address(sa[0])
|
||||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
|
||||||
if ip_addr.is_multicast:
|
if ip_addr.is_multicast:
|
||||||
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
|
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
|
||||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||||
raise HTTPError(f"Request to local address {sa[0]} blocked")
|
raise HTTPError(f"Request to local address {sa[0]} blocked")
|
||||||
if ip_addr.is_private:
|
if ip_addr.is_private:
|
||||||
raise HTTPError(f"Request to private address {sa[0]} blocked")
|
raise HTTPError(f"Request to private address {sa[0]} blocked")
|
||||||
if check_ip4 in _cgnat_net:
|
|
||||||
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
|
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
try:
|
try:
|
||||||
@@ -234,27 +226,9 @@ def monkeypatch_cookie_morsel():
|
|||||||
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
|
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_reportlab_imagereader():
|
|
||||||
from reportlab.lib import utils
|
|
||||||
old_init = utils.ImageReader.__init__
|
|
||||||
|
|
||||||
def new_init(self, fileName, ident=None): # noqa
|
|
||||||
if not isinstance(fileName, Image.Image) and not hasattr(fileName, 'read') and not hasattr(fileName, 'str'):
|
|
||||||
if not isinstance(self, ThumbnailingImageReader):
|
|
||||||
# ThumbnailingImageReader is only used by us explicitly and not by using <img> in html, so it is safe
|
|
||||||
raise SuspiciousFileOperation("reportlab should not be reading images from disk")
|
|
||||||
|
|
||||||
return types.MethodType(old_init, self)(
|
|
||||||
fileName, ident
|
|
||||||
)
|
|
||||||
|
|
||||||
utils.ImageReader.__init__ = new_init
|
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_all_at_ready():
|
def monkeypatch_all_at_ready():
|
||||||
monkeypatch_vobject_performance()
|
monkeypatch_vobject_performance()
|
||||||
monkeypatch_pillow_safer()
|
monkeypatch_pillow_safer()
|
||||||
monkeypatch_requests_timeout()
|
monkeypatch_requests_timeout()
|
||||||
monkeypatch_urllib3_ssrf_protection()
|
monkeypatch_urllib3_ssrf_protection()
|
||||||
monkeypatch_cookie_morsel()
|
monkeypatch_cookie_morsel()
|
||||||
monkeypatch_reportlab_imagereader()
|
|
||||||
|
|||||||
@@ -20,19 +20,14 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import unicodedata
|
|
||||||
|
|
||||||
from arabic_reshaper import ArabicReshaper
|
from arabic_reshaper import ArabicReshaper
|
||||||
from bidi import get_display
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.html import escape
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from reportlab.lib.styles import ParagraphStyle
|
from reportlab.lib.styles import ParagraphStyle
|
||||||
from reportlab.lib.utils import ImageReader
|
from reportlab.lib.utils import ImageReader
|
||||||
from reportlab.pdfbase import pdfmetrics
|
from reportlab.pdfbase import pdfmetrics
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
from reportlab.platypus import Paragraph
|
from reportlab.platypus import Paragraph
|
||||||
|
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
@@ -75,20 +70,6 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
def normalize_text(text: str) -> str:
|
|
||||||
# reportlab does not support unicode combination characters
|
|
||||||
# It's important we do this before we use ArabicReshaper
|
|
||||||
text = unicodedata.normalize("NFKC", text)
|
|
||||||
|
|
||||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
|
||||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
|
||||||
try:
|
|
||||||
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
|
|
||||||
except:
|
|
||||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
class FontFallbackParagraph(Paragraph):
|
class FontFallbackParagraph(Paragraph):
|
||||||
def __init__(self, text, style=None, *args, **kwargs):
|
def __init__(self, text, style=None, *args, **kwargs):
|
||||||
if style is None:
|
if style is None:
|
||||||
@@ -106,8 +87,6 @@ class FontFallbackParagraph(Paragraph):
|
|||||||
if not text:
|
if not text:
|
||||||
return True
|
return True
|
||||||
font = pdfmetrics.getFont(font_name)
|
font = pdfmetrics.getFont(font_name)
|
||||||
if not isinstance(font, TTFont):
|
|
||||||
return True
|
|
||||||
return all(
|
return all(
|
||||||
ord(c) in font.face.charToGlyph or not c.isprintable()
|
ord(c) in font.face.charToGlyph or not c.isprintable()
|
||||||
for c in text
|
for c in text
|
||||||
@@ -123,24 +102,6 @@ class FontFallbackParagraph(Paragraph):
|
|||||||
return family
|
return family
|
||||||
|
|
||||||
|
|
||||||
class PlainTextParagraph(FontFallbackParagraph):
|
|
||||||
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
|
|
||||||
if not isinstance(text, str):
|
|
||||||
if hasattr(text, '__html__'):
|
|
||||||
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
|
|
||||||
text = str(text)
|
|
||||||
|
|
||||||
# Normalize unicode and apply reshaping
|
|
||||||
text = normalize_text(text)
|
|
||||||
|
|
||||||
# Escape any HTML in the text
|
|
||||||
text = escape(text)
|
|
||||||
|
|
||||||
if linebreaks:
|
|
||||||
text = text.strip().replace("\n", "<br />\n")
|
|
||||||
super().__init__(text, style, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def register_ttf_font_if_new(name, path):
|
def register_ttf_font_if_new(name, path):
|
||||||
from reportlab.pdfbase import pdfmetrics
|
from reportlab.pdfbase import pdfmetrics
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from geoip2.errors import AddressNotFoundError
|
|||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
from pretix.helpers.http import get_client_ip
|
from pretix.helpers.http import get_client_ip
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ def handle_login_source(user, request):
|
|||||||
'source': src,
|
'source': src,
|
||||||
'country': Country(str(country)).name if country else _('Unknown country'),
|
'country': Country(str(country)).name if country else _('Unknown country'),
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'url': mainreverse_absolute('control:user.settings')
|
'url': build_absolute_uri('control:user.settings')
|
||||||
},
|
},
|
||||||
event=None,
|
event=None,
|
||||||
user=user,
|
user=user,
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django import template
|
|
||||||
from django.utils.html import format_html
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def wrap_in(content, tag_name):
|
|
||||||
return format_html(f'<{tag_name}>{{}}</{tag_name}>', content)
|
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import warnings
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -27,15 +26,6 @@ from django.urls import reverse
|
|||||||
|
|
||||||
|
|
||||||
def build_absolute_uri(urlname, args=None, kwargs=None):
|
def build_absolute_uri(urlname, args=None, kwargs=None):
|
||||||
warnings.warn(
|
|
||||||
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
|
|
||||||
'Replace this usage with ',
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
return mainreverse_absolute(urlname, args, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def mainreverse_absolute(urlname, args=None, kwargs=None):
|
|
||||||
from pretix.multidomain import maindomain_urlconf
|
from pretix.multidomain import maindomain_urlconf
|
||||||
|
|
||||||
return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf))
|
return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user