mirror of
https://github.com/pretix/pretix.git
synced 2026-08-04 09:47:50 +00:00
Compare commits
136
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f88912adfe | ||
|
|
8f7220b574 | ||
|
|
5adbdb80a8 | ||
|
|
3717c4b553 | ||
|
|
609f45d818 | ||
|
|
1d49c98cf2 | ||
|
|
586f42557f | ||
|
|
e3f219366d | ||
|
|
c571b269ff | ||
|
|
6d57501c5c | ||
|
|
5f3e039b2e | ||
|
|
8fa7aeef78 | ||
|
|
3b5baa7701 | ||
|
|
c6bb3e71bf | ||
|
|
104607d34e | ||
|
|
714ef0d3b6 | ||
|
|
db7c52ca93 | ||
|
|
fc94fbd9c8 | ||
|
|
61b3207ea2 | ||
|
|
ccf17db972 | ||
|
|
456bee7efa | ||
|
|
ccfdd364a3 | ||
|
|
cf92988eae | ||
|
|
6c561b1908 | ||
|
|
5634a16a85 | ||
|
|
6883ae268f | ||
|
|
f75f8dead6 | ||
|
|
0b28df8b83 | ||
|
|
0ffffc6a51 | ||
|
|
3f95f06845 | ||
|
|
22bb4a9ac4 | ||
|
|
ee50ee8e99 | ||
|
|
63a6b17229 | ||
|
|
f33153ef01 | ||
|
|
09517837ba | ||
|
|
0f9ec8beca | ||
|
|
6d604889f2 | ||
|
|
f9da500c06 | ||
|
|
8f3b92a5b4 | ||
|
|
c82aa891e6 | ||
|
|
591ff61d1b | ||
|
|
af3ba16631 | ||
|
|
dce0bba707 | ||
|
|
0a942a670f | ||
|
|
310b1f50bc | ||
|
|
0cef7029e1 | ||
|
|
fbc2a4cdc2 | ||
|
|
2daf6f6d97 | ||
|
|
1fe80fa8c5 | ||
|
|
fa0b31b19f | ||
|
|
3a77eeaa91 | ||
|
|
a1faa66ecd | ||
|
|
1e458d21f9 | ||
|
|
d1a051544f | ||
|
|
8bd4ddcd0d | ||
|
|
59a16789ea | ||
|
|
f4ce3654bb | ||
|
|
3ad99d8239 | ||
|
|
b415393ccf | ||
|
|
84dbd93d9e | ||
|
|
5a4f990ab9 | ||
|
|
35f3d95a46 | ||
|
|
c729b71320 | ||
|
|
8eb7c8db9e | ||
|
|
d5609f6ab0 | ||
|
|
5d8fa31bdf | ||
|
|
9360b1fd90 | ||
|
|
51da6570bf | ||
|
|
fbdbddd555 | ||
|
|
eb3edd83b8 | ||
|
|
25f5fe54a9 | ||
|
|
7bf153bb3b | ||
|
|
48e64071a1 | ||
|
|
95ea4fd4c9 | ||
|
|
206b57adfd | ||
|
|
b7f3f7a7a1 | ||
|
|
34e7a0fc31 | ||
|
|
cc7f249cb8 | ||
|
|
147061eaa4 | ||
|
|
c16491889b | ||
|
|
1eb1d8df5f | ||
|
|
3f47cf785c | ||
|
|
e8859cb2e2 | ||
|
|
61ab6f729d | ||
|
|
79c9ba3cf3 | ||
|
|
1d86f7a0c3 | ||
|
|
e259b3994a | ||
|
|
18e97624fd | ||
|
|
1c9a245231 | ||
|
|
b51ca58820 | ||
|
|
7a48cac862 | ||
|
|
1bdcc4580e | ||
|
|
dd10bdd433 | ||
|
|
f7a74c2e74 | ||
|
|
4037e1886d | ||
|
|
c4ae363fdb | ||
|
|
3df64a46e7 | ||
|
|
69502986ad | ||
|
|
51ea63335c | ||
|
|
dc76b554f8 | ||
|
|
f8be8296dd | ||
|
|
b3c917925c | ||
|
|
4954373a04 | ||
|
|
5571ec3858 | ||
|
|
9ef3139905 | ||
|
|
3139b9fe6f | ||
|
|
437d33ba79 | ||
|
|
0a9890b1b0 | ||
|
|
1420ad43db | ||
|
|
30da7a6429 | ||
|
|
a2f3dcce02 | ||
|
|
41f5ca3f9d | ||
|
|
817f1e0371 | ||
|
|
35fc001768 | ||
|
|
002416e435 | ||
|
|
4917249bab | ||
|
|
afd2468375 | ||
|
|
54d06dd7f8 | ||
|
|
5e59844cf5 | ||
|
|
0d2a981674 | ||
|
|
943aeaa31f | ||
|
|
cfe0f67f0d | ||
|
|
635bb94cc4 | ||
|
|
cf732ce173 | ||
|
|
74e9a4ad2d | ||
|
|
570357e9be | ||
|
|
473375d4ae | ||
|
|
a78b698520 | ||
|
|
332c968294 | ||
|
|
ad12c344c5 | ||
|
|
91c0db1ac0 | ||
|
|
4d231b70aa | ||
|
|
ab2f6f6bed | ||
|
|
28458f7b85 | ||
|
|
50ff968c17 | ||
|
|
0b4064f14f |
+1
-2
@@ -33,8 +33,7 @@ RUN apt-get update && \
|
||||
mkdir /static && \
|
||||
mkdir /etc/supervisord && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
curl -qL https://www.npmjs.com/install.sh | sh
|
||||
apt-get install -y nodejs
|
||||
|
||||
|
||||
ENV LC_ALL=C.UTF-8 \
|
||||
|
||||
@@ -26,7 +26,7 @@ installation guides):
|
||||
* `Docker`_
|
||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||
* A `PostgreSQL`_ 9.6+ database server
|
||||
* A `PostgreSQL`_ 11+ database server
|
||||
* A `redis`_ server
|
||||
|
||||
We also recommend that you use a firewall, although this is not a pretix-specific recommendation. If you're new to
|
||||
@@ -321,11 +321,11 @@ workers, e.g. ``docker run … taskworker -Q notifications --concurrency 32``.
|
||||
|
||||
|
||||
.. _Docker: https://docs.docker.com/engine/installation/linux/debian/
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
|
||||
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
.. _pretix.eu: https://pretix.eu/
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _redis website: https://redis.io/topics/security
|
||||
|
||||
@@ -21,6 +21,7 @@ Requirements
|
||||
Please set up the following systems beforehand, we'll not explain them here in detail (but see these links for external
|
||||
installation guides):
|
||||
|
||||
* A python 3.9+ installation
|
||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
|
||||
* A `PostgreSQL`_ 11+ database server
|
||||
@@ -323,11 +324,11 @@ Then, proceed like after any plugin installation::
|
||||
(venv)$ python -m pretix updatestyles
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
|
||||
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
.. _pretix.eu: https://pretix.eu/
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-22-04
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
|
||||
@@ -32,6 +32,8 @@ as well as the type of underlying hardware. Example:
|
||||
"token": "kpp4jn8g2ynzonp6",
|
||||
"hardware_brand": "Samsung",
|
||||
"hardware_model": "Galaxy S",
|
||||
"os_name": "Android",
|
||||
"os_version": "2.3.6",
|
||||
"software_brand": "pretixdroid",
|
||||
"software_version": "4.0.0"
|
||||
}
|
||||
@@ -98,6 +100,8 @@ following endpoint:
|
||||
{
|
||||
"hardware_brand": "Samsung",
|
||||
"hardware_model": "Galaxy S",
|
||||
"os_name": "Android",
|
||||
"os_version": "2.3.6",
|
||||
"software_brand": "pretixdroid",
|
||||
"software_version": "4.1.0",
|
||||
"info": {"arbitrary": "data"}
|
||||
|
||||
@@ -24,6 +24,8 @@ all_events boolean Whether this de
|
||||
limit_events list List of event slugs this device has access to
|
||||
hardware_brand string Device hardware manufacturer (read-only)
|
||||
hardware_model string Device hardware model (read-only)
|
||||
os_name string Device operating system name (read-only)
|
||||
os_version string Device operating system version (read-only)
|
||||
software_brand string Device software product (read-only)
|
||||
software_version string Device software version (read-only)
|
||||
created datetime Creation time
|
||||
@@ -76,6 +78,8 @@ Device endpoints
|
||||
"security_profile": "full",
|
||||
"hardware_brand": "Zebra",
|
||||
"hardware_model": "TC25",
|
||||
"os_name": "Android",
|
||||
"os_version": "8.1.0",
|
||||
"software_brand": "pretixSCAN",
|
||||
"software_version": "1.5.1"
|
||||
}
|
||||
@@ -123,6 +127,8 @@ Device endpoints
|
||||
"security_profile": "full",
|
||||
"hardware_brand": "Zebra",
|
||||
"hardware_model": "TC25",
|
||||
"os_name": "Android",
|
||||
"os_version": "8.1.0",
|
||||
"software_brand": "pretixSCAN",
|
||||
"software_version": "1.5.1"
|
||||
}
|
||||
@@ -173,6 +179,8 @@ Device endpoints
|
||||
"initialized": null
|
||||
"hardware_brand": null,
|
||||
"hardware_model": null,
|
||||
"os_name": null,
|
||||
"os_version": null,
|
||||
"software_brand": null,
|
||||
"software_version": null
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ Endpoints
|
||||
|
||||
The ``public_url`` field has been added.
|
||||
|
||||
.. versionchanged:: 5.0
|
||||
|
||||
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
|
||||
added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/
|
||||
|
||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||
@@ -141,6 +146,10 @@ Endpoints
|
||||
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
|
||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
|
||||
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
|
||||
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
|
||||
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
|
||||
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
|
||||
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
|
||||
|
||||
@@ -111,7 +111,7 @@ Listing available exporters
|
||||
"input_parameters": [
|
||||
{
|
||||
"name": "events",
|
||||
"required": true
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "_format",
|
||||
|
||||
@@ -19,6 +19,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the medium
|
||||
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
|
||||
organizer string Organizer slug of the organizer who "owns" this medium.
|
||||
identifier string Unique identifier of the medium. The format depends on the ``type``.
|
||||
active boolean Whether this medium may be used.
|
||||
created datetime Date of creation
|
||||
@@ -67,6 +68,7 @@ Endpoints
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"organizer": "bigevents",
|
||||
"identifier": "ABCDEFGH",
|
||||
"created": "2021-04-06T13:44:22.809377Z",
|
||||
"updated": "2021-04-06T13:44:22.809377Z",
|
||||
@@ -123,6 +125,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"organizer": "bigevents",
|
||||
"identifier": "ABCDEFGH",
|
||||
"created": "2021-04-06T13:44:22.809377Z",
|
||||
"updated": "2021-04-06T13:44:22.809377Z",
|
||||
@@ -152,6 +155,9 @@ Endpoints
|
||||
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
|
||||
medium behind the scenes.
|
||||
|
||||
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
|
||||
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -176,6 +182,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"organizer": "bigevents",
|
||||
"identifier": "ABCDEFGH",
|
||||
"created": "2021-04-06T13:44:22.809377Z",
|
||||
"updated": "2021-04-06T13:44:22.809377Z",
|
||||
@@ -235,6 +242,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"organizer": "bigevents",
|
||||
"identifier": "ABCDEFGH",
|
||||
"created": "2021-04-06T13:44:22.809377Z",
|
||||
"updated": "2021-04-06T13:44:22.809377Z",
|
||||
@@ -291,6 +299,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"organizer": "bigevents",
|
||||
"identifier": "ABCDEFGH",
|
||||
"created": "2021-04-06T13:44:22.809377Z",
|
||||
"updated": "2021-04-06T13:44:22.809377Z",
|
||||
|
||||
@@ -63,6 +63,11 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``search`` query parameter has been added to filter sub-events by their name or location in any language.
|
||||
|
||||
.. versionchanged:: 5.0
|
||||
|
||||
The ``date_from_before``, ``date_from_after``, ``date_to_before``, and ``date_to_after`` query parameters have been
|
||||
added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -130,6 +135,10 @@ Endpoints
|
||||
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
|
||||
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
|
||||
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
|
||||
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:query search: Only return events matching a given search query.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
@@ -458,6 +467,10 @@ Endpoints
|
||||
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||
:query date_from_after: If set to a date and time, only events that start at or after the given time are returned.
|
||||
:query date_from_before: If set to a date and time, only events that start at or before the given time are returned.
|
||||
:query date_to_after: If set to a date and time, only events that have an end date and end at or after the given time are returned.
|
||||
:query date_to_before: If set to a date and time, only events that have an end date and end at or before the given time are returned.
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
|
||||
@@ -20,11 +20,16 @@ internal_name string An optional nam
|
||||
rate decimal (string) Tax rate in percent
|
||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||
the specified product price
|
||||
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
|
||||
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
|
||||
be ignored if custom rules are set.
|
||||
home_country string Merchant country (required for reverse charge), can be
|
||||
``null`` or empty string
|
||||
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
||||
rules keep the gross price constant (default is ``false``)
|
||||
custom_rules object Dynamic rules specification. Each list element
|
||||
corresponds to one rule that will be processed in order.
|
||||
The current version of the schema in use can be found
|
||||
`here`_.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -32,6 +37,10 @@ keep_gross_if_rate_changes boolean If ``true``, ch
|
||||
|
||||
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
|
||||
|
||||
.. versionchanged:: 2023.6
|
||||
|
||||
The ``custom_rules`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -68,6 +77,7 @@ Endpoints
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"custom_rules": null,
|
||||
"home_country": "DE"
|
||||
}
|
||||
]
|
||||
@@ -108,6 +118,7 @@ Endpoints
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"custom_rules": null,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
@@ -156,6 +167,7 @@ Endpoints
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"custom_rules": null,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
@@ -203,6 +215,7 @@ Endpoints
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"keep_gross_if_rate_changes": false,
|
||||
"custom_rules": null,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
@@ -242,3 +255,5 @@ Endpoints
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
||||
|
||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json
|
||||
|
||||
@@ -50,6 +50,10 @@ The following values for ``action_types`` are valid with pretix core:
|
||||
* ``pretix.event.order.payment.confirmed``
|
||||
* ``pretix.event.order.approved``
|
||||
* ``pretix.event.order.denied``
|
||||
* ``pretix.event.orders.waitinglist.added``
|
||||
* ``pretix.event.orders.waitinglist.changed``
|
||||
* ``pretix.event.orders.waitinglist.deleted``
|
||||
* ``pretix.event.orders.waitinglist.voucher_assigned``
|
||||
* ``pretix.event.checkin``
|
||||
* ``pretix.event.checkin.reverted``
|
||||
* ``pretix.event.added``
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
ePayBL
|
||||
======
|
||||
|
||||
.. note::
|
||||
|
||||
Since ePayBL is only available to german federal, provincial and communal entities, the following page is also
|
||||
only provided in german. Should you require assistance with ePayBL and do not speak this language, please feel free
|
||||
reach out to support@pretix.eu.
|
||||
|
||||
|
||||
Einführung
|
||||
----------
|
||||
|
||||
.. note::
|
||||
|
||||
Sollten Sie lediglich schnell entscheiden wollen, welcher Kontierungsmodus in den Einstellungen des pretix
|
||||
ePayBL-plugins gewählt werden soll, so springen Sie direkt zur Sektion :ref:`Kontierungsmodus`.
|
||||
|
||||
|
||||
`ePayBL`_ - das ePayment-System von Bund und Länder - ist das am weitesten verbreitete Zahlungssystem für Bundes-, Länder-
|
||||
sowie kommunale Aufgabenträger. Während es nur wie eines von vielen anderen Zahlungssystemen scheint, so bietet es
|
||||
seinen Nutzern besondere Vorteile, wie die automatische Erfassung von Zahlungsbelegen, dem Übertragen von Buchungen in
|
||||
Haushaltskassen/-systeme sowie die automatische Erfassung von Kontierungen und Steuermerkmalen.
|
||||
|
||||
Rein technisch gesehen ist ePayBL hierbei nicht ein eigenständiger Zahlungsdienstleister sondern nur ein eine Komponente
|
||||
im komplexen System, dass die Zahlungsabwicklung für Kommunen und Behörden ist.
|
||||
|
||||
Im folgenden der schematische Aufbau einer Umgebung, in welcher ePayBL zum Einsatz kommt:
|
||||
|
||||
.. figure:: img/epaybl_flowchart.png
|
||||
:class: screenshot
|
||||
|
||||
Quelle: Integrationshandbuch ePayBL-Konnektor, DResearch Digital Media Systems GmbH
|
||||
|
||||
|
||||
In diesem Schaubild stellt pretix, bzw. die von Ihnen als Veranstalter angelegten Ticketshops, das Fachverfahren dar.
|
||||
|
||||
ePayBL stellt das Bindeglied zwischen den Fachverfahren, Haushaltssystemen und dem eigentlichen Zahlungsdienstleister,
|
||||
dem sog. ZV-Provider dar. Dieser ZV-Provider ist die Stelle, welche die eigentlichen Kundengelder einzieht und an den
|
||||
Händler auszahlt. Das Gros der Zahlungsdienstleister unterstützt pretix hierbei auch direkt; sprich: Sollten Sie die
|
||||
Anbindung an Ihre Haushaltssysteme nicht benötigen, kann eine direkte Anbindung in der Regel ebenso - und dies bei meist
|
||||
vermindertem Aufwand - vorgenommen werden.
|
||||
|
||||
In der Vergangenheit zeigte sich jedoch schnell, dass nicht jeder IT-Dienstleister immer sofort die neueste Version von
|
||||
ePayBL seinen Nutzern angeboten hat. Die Gründe hierfür sind mannigfaltig: Von fest vorgegebenen Update-Zyklen bis hin
|
||||
zu Systeme mit speziellen Anpassungen, kann leider nicht davon ausgegangen werden, dass alle ePayBL-Systeme exakt gleich
|
||||
ansprechbar sind - auch wenn es sich dabei eigentlich um einen standardisierten Dienst handelt.
|
||||
|
||||
Aus diesem Grund gibt es mit dem ePayBL-Konnektor eine weitere Abstraktionsschicht welche optional zwischen den
|
||||
Fachverfahren und dem ePayBL-Server sitzt. Dieser Konnektor wird so gepflegt, dass er zum einen eine dauerhaft
|
||||
gleichartige Schnittstelle den Fachverfahren bietet aber gleichzeitig auch mit jeder Version des ePayBL-Servers
|
||||
kommunizieren kann - egal wie neu oder alt, wie regulär oder angepasst diese ist.
|
||||
|
||||
Im Grunde müsste daher eigentlich immer gesagt werden, dass pretix eine Anbindung an den ePayBL-Konnektor bietet; nicht
|
||||
an "ePayBL" oder den "ePayBL-Server". Diese Unterscheidung kann bei der Ersteinrichtung und Anforderung von Zugangsdaten
|
||||
von Relevanz sein. Da in der Praxis jedoch beide Begriffe gleichbedeutend genutzt werden, wird im Folgenden auch nur von
|
||||
einer ePayBL-Anbindung die Rede sein - auch wenn explizit der Konnektor gemeint ist.
|
||||
|
||||
|
||||
.. _`Kontierungsmodus`:
|
||||
|
||||
Kontierungsmodus
|
||||
----------------
|
||||
|
||||
ePayBL ist ein Produkt, welches für die Abwicklung von Online-Zahlungsvorgängen in der Verwaltung geschaffen wurde. Ein
|
||||
Umfeld, in dem klar definiert ist, was ein Kunde gerade bezahlt und wohin das Geld genau fließt. Diese Annahmen lassen
|
||||
sich in einem Ticketshop wie pretix jedoch nur teilweise genauso abbilden.
|
||||
|
||||
Die ePayBL-Integration für pretix bietet daher zwei unterschiedliche Modi an, wie Buchungen erfasst und an ePayBL und
|
||||
damit auch an die dahinterliegenden Haushaltssysteme gemeldet werden können.
|
||||
|
||||
Kontierung pro Position/Artikel
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Dieser Modus versucht den klassischen, behördentypischen ePayBL-Zahlungsvorgang abzubilden: Jede einzelne Position, die
|
||||
ein Kunde in den Warenkorb legt, wird auch genauso 1:1 an ePayBL und die Hintergrundsysteme übermittelt.
|
||||
|
||||
Hierbei muss zwingend auch für jede Position ein Kennzeichen für Haushaltsstelle und Objektnummer, sowie optional ein
|
||||
Kontierungsobjekt (``HREF``; bspw. ``stsl=Steuerschlüssel;psp=gsb:Geschäftsbereich,auft:Innenauftrag,kst:Kostenstelle;``
|
||||
) übermittelt werden.
|
||||
|
||||
Diese Daten sind vom Veranstalter entsprechend für jeden in der Veranstaltung angelegten Artikel innerhalb des Tabs
|
||||
"Zusätzliche Einstellungen" der Produkteinstellungen zu hinterlegen.
|
||||
|
||||
Während diese Einstellung eine größtmögliche Menge an Kontierungsdaten überträgt und auch ein separates Verbuchen von
|
||||
Leistungen auf unterschiedliche Haushaltsstellen erlaubt, so hat diese Option auch einen großen Nachteil: Der Kunde kann
|
||||
nur eine Zahlung für seine Bestellung leisten.
|
||||
|
||||
Während sich dies nicht nach einem großen Problem anhört, so kann dies beim Kunden zu Frust führen. pretix bietet die
|
||||
Option an, dass ein Veranstalter eine Bestellung jederzeit verändern kann: Ändern von Preisen von Positionen in einer
|
||||
aufgegebenen Bestellung, Zubuchen und Entfernen von Bestellpositionen, etc. Hat der Kunde seine ursprüngliche Bestellung
|
||||
jedoch schon bezahlt, kann pretix nicht mehr die komplette Bestellung mit den passenden Kontierungen übertragen - es
|
||||
müsste nur ein Differenz-Abbild zwischen Ursprungsbestellung und aktueller Bestellung übertragen werden. Aber auch wenn
|
||||
eine "Nachmeldung" möglich wäre, so wäre ein konkretes Auflösen für was jetzt genau gezahlt wird, nicht mehr möglich.
|
||||
|
||||
Daher gilt bei der Nutzung der Kontierung pro Position/Artikel: Der Kunde kann nur eine (erfolgreiche) Zahlung auf seine
|
||||
Bestellung leisten.
|
||||
|
||||
Eine weitere Einschränkung dieses Modus ist, dass aktuell keine Gebühren-Positionen (Versandkosten, Zahlungs-, Storno-
|
||||
oder Servicegebühren) in diesem Modus übertragen werden können. Bitte wenden Sie sich an uns, wenn Sie diese
|
||||
Funktionalität benötigen.
|
||||
|
||||
|
||||
Kontierung pro Zahlvorgang
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Dieser Modus verabschiedet sich vom behördlichen "Jede Position gehört genau zu einem Haushaltskonto und muss genau
|
||||
zugeordnet werden". Stattdessen werden alle Bestellpositionen - inklusive eventuell definierter Gebühren - vermengt und
|
||||
nur als ein großer Warenkorb, genauer gesagt: eine einzige Position an ePayBL sowie die Hintergrundsysteme gemeldet.
|
||||
|
||||
Während im "pro Postion/Artikel"-Modus jeder Artikel einzeln übermittelt wird und damit auch korrekt pro Artikel der
|
||||
jeweilige Brutto- und Nettopreis, sowie der anfallende Steuerbetrag und ein Steuerkennzeichen (mit Hilfe des optionalen
|
||||
``HREF``-Attributs) übermittelt werden, ist dies im "pro Zahlvorgang"-Modus nicht möglich.
|
||||
|
||||
Stattdessen übermittelt pretix nur einen Betrag für den gesamten Warenkorb: Bruttopreis == Nettopreis. Der Steuerbetrag
|
||||
wird hierbei als 0 übermittelt.
|
||||
|
||||
Die Angabe einer Haushaltsstelle und Objektnummer, sowie optional der ``HREF``-Kontierungsinformationen ist jedoch
|
||||
weiterhin notwendig - allerdings nicht mehr individuell für jeden Artikel/jede Position sondern nur für die gesamte
|
||||
Bestellung. Diese Daten sind direkt in den ePayBL-Einstellungen der Veranstaltung unter Einstellungen -> Zahlung ->
|
||||
ePayBL vorzunehmen
|
||||
|
||||
In der Praxis bedeutet dies, dass in einem angeschlossenen Haushaltssystem nicht nachvollzogen kann, welche Positionen
|
||||
konkret erworben und bezahlt wurden - stattdessen kann nur der Fakt, dass etwas verkauft wurde erfasst werden.
|
||||
|
||||
Je nach Aufbau und Vorgaben der Finanzbuchhaltung kann dies jedoch ausreichend sein - wenn bspw. eine Ferienfahrt
|
||||
angeboten wird und seitens der Haushaltssysteme nicht erfasst werden muss, wie viel vom Gesamtbetrag einer Bestellung
|
||||
auf die Ferienfahrt an sich, auf einen Zubringerbus und einen Satz Bettwäsche entfallen ist, sondern (vereinfacht
|
||||
gesagt) es ausreichend ist, dass "Eine Summe X für die Haushaltsstelle/Objektnummer geflossen ist".
|
||||
|
||||
Dieser Modus der Kontierung bietet Ihnen auch als Vorteil gegenüber dem vorhergehenden an, dass die Bestellungen der
|
||||
Kunden jederzeit erweitert und verändert werden können - auch wenn die Ursprungsbestellung schon bezahlt wurde und nur
|
||||
noch eine Differenz gezahlt wird.
|
||||
|
||||
|
||||
Einschränkungen
|
||||
---------------
|
||||
|
||||
Zum aktuellen Zeitpunkt erlaubt die pretix-Anbindung an ePayBL nicht das durchführen von Erstattungen von bereits
|
||||
geleisteten Zahlungen. Der Prozess hierfür unterscheidet sich von Behörde zu Behörde und muss daher händisch
|
||||
durchgeführt werden.
|
||||
|
||||
.. _ePayBL: https://www.epaybl.de/
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -18,6 +18,7 @@ If you want to **create** a plugin, please go to the
|
||||
campaigns
|
||||
certificates
|
||||
digital
|
||||
epaybl
|
||||
exhibitors
|
||||
shipping
|
||||
imported_secrets
|
||||
|
||||
+6
-5
@@ -30,7 +30,7 @@ dependencies = [
|
||||
"babel",
|
||||
"BeautifulSoup4==4.12.*",
|
||||
"bleach==5.0.*",
|
||||
"celery==5.2.*",
|
||||
"celery==5.3.*",
|
||||
"chardet==5.1.*",
|
||||
"cryptography>=3.4.2",
|
||||
"css-inline==0.8.*",
|
||||
@@ -59,10 +59,10 @@ dependencies = [
|
||||
"dnspython==2.3.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==4.*",
|
||||
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.2.*",
|
||||
"kombu==5.3.*",
|
||||
"libsass==0.22.*",
|
||||
"lxml",
|
||||
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
@@ -73,9 +73,10 @@ dependencies = [
|
||||
"packaging",
|
||||
"paypalrestsdk==1.13.*",
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.6.*",
|
||||
"PyJWT==2.7.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==9.5.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==4.23.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
@@ -91,7 +92,7 @@ dependencies = [
|
||||
"qrcode==7.4.*",
|
||||
"redis==4.5.*,>=4.5.4",
|
||||
"reportlab==4.0.*",
|
||||
"requests==2.30.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==1.15.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
|
||||
@@ -29,7 +29,6 @@ sys.path.append(str(Path.cwd() / 'src'))
|
||||
|
||||
|
||||
def _CustomBuild(*args, **kwargs):
|
||||
print(sys.path)
|
||||
from pretix._build import CustomBuild
|
||||
return CustomBuild(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "4.21.0.dev0"
|
||||
__version__ = "2023.7.0.dev0"
|
||||
|
||||
@@ -45,6 +45,10 @@ def npm_install():
|
||||
|
||||
class CustomBuild(build):
|
||||
def run(self):
|
||||
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
|
||||
# Only run this command on the pretix module, not on other modules even if it's registered globally
|
||||
# in some cases
|
||||
return build.run(self)
|
||||
if "PRETIX_DOCKER_BUILD" in os.environ:
|
||||
return # this is a hack to allow calling this file early in our docker build to make use of caching
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings")
|
||||
@@ -68,6 +72,10 @@ class CustomBuild(build):
|
||||
|
||||
class CustomBuildExt(build_ext):
|
||||
def run(self):
|
||||
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
|
||||
# Only run this command on the pretix module, not on other modules even if it's registered globally
|
||||
# in some cases
|
||||
return build_ext.run(self)
|
||||
if "PRETIX_DOCKER_BUILD" in os.environ:
|
||||
return # this is a hack to allow calling this file early in our docker build to make use of caching
|
||||
npm_install()
|
||||
|
||||
@@ -59,7 +59,7 @@ class IdempotencyMiddleware:
|
||||
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
||||
idempotency_key = request.headers.get('X-Idempotency-Key', '')
|
||||
|
||||
with transaction.atomic():
|
||||
with transaction.atomic(durable=True):
|
||||
call, created = ApiCall.objects.select_for_update(of=OF_SELF).get_or_create(
|
||||
auth_hash=auth_hash,
|
||||
idempotency_key=idempotency_key,
|
||||
@@ -75,7 +75,7 @@ class IdempotencyMiddleware:
|
||||
|
||||
if created:
|
||||
resp = self.get_response(request)
|
||||
with transaction.atomic():
|
||||
with transaction.atomic(durable=True):
|
||||
if resp.status_code in (409, 429, 500, 503):
|
||||
# This is the exception: These calls are *meant* to be retried!
|
||||
call.delete()
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# 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 json
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
@@ -46,3 +48,16 @@ class AsymmetricField(serializers.Field):
|
||||
|
||||
def run_validation(self, data=serializers.empty):
|
||||
return self.write.run_validation(data)
|
||||
|
||||
|
||||
class CompatibleJSONField(serializers.JSONField):
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
return json.dumps(data)
|
||||
except (TypeError, ValueError):
|
||||
self.fail('invalid')
|
||||
|
||||
def to_representation(self, value):
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
@@ -46,6 +46,7 @@ from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
@@ -53,6 +54,7 @@ from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.models.tax import CustomRulesValidator
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
@@ -650,9 +652,16 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
custom_rules = CompatibleJSONField(
|
||||
validators=[CustomRulesValidator()],
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes')
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
|
||||
'keep_gross_if_rate_changes', 'custom_rules')
|
||||
|
||||
|
||||
class EventSettingsSerializer(SettingsSerializer):
|
||||
@@ -719,6 +728,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'payment_term_minutes',
|
||||
'payment_term_last',
|
||||
'payment_term_expire_automatically',
|
||||
'payment_term_expire_delay_days',
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
'payment_pending_hidden',
|
||||
|
||||
@@ -93,7 +93,7 @@ class JobRunSerializer(serializers.Serializer):
|
||||
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
|
||||
self.fields["events"] = serializers.SlugRelatedField(
|
||||
queryset=events,
|
||||
required=True,
|
||||
required=False,
|
||||
allow_empty=False,
|
||||
slug_field='slug',
|
||||
many=True
|
||||
@@ -156,8 +156,9 @@ class JobRunSerializer(serializers.Serializer):
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, QueryDict):
|
||||
data = data.copy()
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if isinstance(v, serializers.ManyRelatedField) and k not in data:
|
||||
if isinstance(v, serializers.ManyRelatedField) and k not in data and k != "events":
|
||||
data[k] = []
|
||||
|
||||
for fk in self.fields.keys():
|
||||
|
||||
@@ -60,6 +60,8 @@ class NestedGiftCardSerializer(GiftCardSerializer):
|
||||
|
||||
|
||||
class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -111,6 +113,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
model = ReusableMedium
|
||||
fields = (
|
||||
'id',
|
||||
'organizer',
|
||||
'created',
|
||||
'updated',
|
||||
'type',
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import Counter, defaultdict
|
||||
@@ -39,6 +38,7 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
@@ -896,19 +896,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class CompatibleJSONField(serializers.JSONField):
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
return json.dumps(data)
|
||||
except (TypeError, ValueError):
|
||||
self.fail('invalid')
|
||||
|
||||
def to_representation(self, value):
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
class WrappedList:
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
|
||||
@@ -36,9 +36,9 @@ from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
|
||||
Team, TeamAPIToken, TeamInvite, User,
|
||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -183,8 +183,11 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(
|
||||
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||
Q(issuer=self.context["organizer"]) |
|
||||
Q(issuer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=self.context["organizer"],
|
||||
active=True,
|
||||
).values_list('issuer', flat=True))
|
||||
)
|
||||
if self.instance:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
@@ -248,6 +251,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
unique_serial = serializers.CharField(read_only=True)
|
||||
hardware_brand = serializers.CharField(read_only=True)
|
||||
hardware_model = serializers.CharField(read_only=True)
|
||||
os_name = serializers.CharField(read_only=True)
|
||||
os_version = serializers.CharField(read_only=True)
|
||||
software_brand = serializers.CharField(read_only=True)
|
||||
software_version = serializers.CharField(read_only=True)
|
||||
created = serializers.DateTimeField(read_only=True)
|
||||
@@ -260,7 +265,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
fields = (
|
||||
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
|
||||
'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model',
|
||||
'software_brand', 'software_version', 'security_profile'
|
||||
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -396,7 +396,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,
|
||||
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
|
||||
source_type='barcode', legacy_url_support=False):
|
||||
source_type='barcode', legacy_url_support=False, simulate=False):
|
||||
if not checkinlists:
|
||||
raise ValidationError('No check-in list passed.')
|
||||
|
||||
@@ -433,6 +433,8 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
)
|
||||
raw_barcode_for_checkin = None
|
||||
from_revoked_secret = False
|
||||
if simulate:
|
||||
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
|
||||
|
||||
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
|
||||
# parent secret
|
||||
@@ -472,13 +474,14 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
revoked_matches = list(
|
||||
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
|
||||
if len(revoked_matches) == 0:
|
||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': checkinlists[0].pk,
|
||||
'barcode': raw_barcode,
|
||||
'searched_lists': [cl.pk for cl in checkinlists]
|
||||
}, user=user, auth=auth)
|
||||
if not simulate:
|
||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': checkinlists[0].pk,
|
||||
'barcode': raw_barcode,
|
||||
'searched_lists': [cl.pk for cl in checkinlists]
|
||||
}, user=user, auth=auth)
|
||||
|
||||
for cl in checkinlists:
|
||||
for k, s in cl.event.ticket_secret_generators.items():
|
||||
@@ -492,12 +495,13 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
except:
|
||||
pass
|
||||
|
||||
Checkin.objects.create(
|
||||
position=None,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_INVALID,
|
||||
**common_checkin_args,
|
||||
)
|
||||
if not simulate:
|
||||
Checkin.objects.create(
|
||||
position=None,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_INVALID,
|
||||
**common_checkin_args,
|
||||
)
|
||||
|
||||
if force and legacy_url_support and isinstance(auth, Device):
|
||||
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
|
||||
@@ -539,19 +543,20 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
from_revoked_secret = True
|
||||
else:
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[revoked_matches[0].event_id].pk,
|
||||
'barcode': raw_barcode
|
||||
}, user=user, auth=auth)
|
||||
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_REVOKED,
|
||||
**common_checkin_args
|
||||
)
|
||||
if not simulate:
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[revoked_matches[0].event_id].pk,
|
||||
'barcode': raw_barcode
|
||||
}, user=user, auth=auth)
|
||||
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_REVOKED,
|
||||
**common_checkin_args
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_REVOKED,
|
||||
@@ -588,24 +593,25 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
# 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.
|
||||
op = op_candidates[0]
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'force': force,
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[op.order.event_id].pk,
|
||||
}, user=user, auth=auth)
|
||||
common_checkin_args['list'] = list_by_event[op.order.event_id]
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_AMBIGUOUS,
|
||||
error_explanation=None,
|
||||
**common_checkin_args,
|
||||
)
|
||||
if not simulate:
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'force': force,
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[op.order.event_id].pk,
|
||||
}, user=user, auth=auth)
|
||||
common_checkin_args['list'] = list_by_event[op.order.event_id]
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_AMBIGUOUS,
|
||||
error_explanation=None,
|
||||
**common_checkin_args,
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_AMBIGUOUS,
|
||||
@@ -652,6 +658,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
raw_barcode=raw_barcode_for_checkin,
|
||||
raw_source_type=source_type,
|
||||
from_revoked_secret=from_revoked_secret,
|
||||
simulate=simulate,
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
return Response({
|
||||
@@ -664,23 +671,24 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
except CheckInError as e:
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': e.code,
|
||||
'reason_explanation': e.reason,
|
||||
'force': force,
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[op.order.event_id].pk,
|
||||
}, user=user, auth=auth)
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=e.code,
|
||||
error_explanation=e.reason,
|
||||
**common_checkin_args,
|
||||
)
|
||||
if not simulate:
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': e.code,
|
||||
'reason_explanation': e.reason,
|
||||
'force': force,
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[op.order.event_id].pk,
|
||||
}, user=user, auth=auth)
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=e.code,
|
||||
error_explanation=e.reason,
|
||||
**common_checkin_args,
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': e.code,
|
||||
|
||||
@@ -42,6 +42,8 @@ class InitializationRequestSerializer(serializers.Serializer):
|
||||
token = serializers.CharField(max_length=190)
|
||||
hardware_brand = serializers.CharField(max_length=190)
|
||||
hardware_model = serializers.CharField(max_length=190)
|
||||
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||
software_brand = serializers.CharField(max_length=190)
|
||||
software_version = serializers.CharField(max_length=190)
|
||||
info = serializers.JSONField(required=False, allow_null=True)
|
||||
@@ -50,6 +52,8 @@ class InitializationRequestSerializer(serializers.Serializer):
|
||||
class UpdateRequestSerializer(serializers.Serializer):
|
||||
hardware_brand = serializers.CharField(max_length=190)
|
||||
hardware_model = serializers.CharField(max_length=190)
|
||||
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||
software_brand = serializers.CharField(max_length=190)
|
||||
software_version = serializers.CharField(max_length=190)
|
||||
info = serializers.JSONField(required=False, allow_null=True)
|
||||
@@ -99,6 +103,8 @@ class InitializeView(APIView):
|
||||
device.initialized = now()
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
device.os_name = serializer.validated_data.get('os_name')
|
||||
device.os_version = serializer.validated_data.get('os_version')
|
||||
device.software_brand = serializer.validated_data.get('software_brand')
|
||||
device.software_version = serializer.validated_data.get('software_version')
|
||||
device.info = serializer.validated_data.get('info')
|
||||
@@ -120,6 +126,8 @@ class UpdateView(APIView):
|
||||
device = request.auth
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
device.os_name = serializer.validated_data.get('os_name')
|
||||
device.os_version = serializer.validated_data.get('os_version')
|
||||
device.software_brand = serializer.validated_data.get('software_brand')
|
||||
device.software_version = serializer.validated_data.get('software_version')
|
||||
device.info = serializer.validated_data.get('info')
|
||||
|
||||
@@ -71,6 +71,8 @@ with scopes_disabled():
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
|
||||
search = django_filters.rest_framework.CharFilter(method='search_qs')
|
||||
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
|
||||
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
@@ -336,6 +338,8 @@ with scopes_disabled():
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
|
||||
search = django_filters.rest_framework.CharFilter(method='search_qs')
|
||||
date_from = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
|
||||
date_to = django_filters.rest_framework.IsoDateTimeFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
|
||||
@@ -133,7 +133,12 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
def exporters(self):
|
||||
exporters = []
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
for ex in sorted([response(self.request.event, self.request.organizer) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
|
||||
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||
raw_exporters = [
|
||||
ex for ex in raw_exporters
|
||||
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
@@ -166,7 +171,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
if (
|
||||
not isinstance(ex, OrganizerLevelExportMixin) or
|
||||
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
|
||||
)
|
||||
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex, events=events)
|
||||
|
||||
@@ -39,7 +39,8 @@ from pretix.api.serializers.media import (
|
||||
)
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
|
||||
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
||||
ReusableMedium,
|
||||
)
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
@@ -135,12 +136,28 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
except ReusableMedium.DoesNotExist:
|
||||
mt = MEDIA_TYPES.get(s.validated_data["type"])
|
||||
if mt:
|
||||
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
|
||||
if m:
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
try:
|
||||
with scopes_disabled():
|
||||
m = ReusableMedium.objects.get(
|
||||
organizer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=request.organizer,
|
||||
active=True,
|
||||
reusable_media=True,
|
||||
).values_list('issuer', flat=True),
|
||||
type=s.validated_data["type"],
|
||||
identifier=s.validated_data["identifier"],
|
||||
)
|
||||
m.linked_orderposition = None # not relevant for cross-organizer
|
||||
m.customer = None # not relevant for cross-organizer
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
except ReusableMedium.DoesNotExist:
|
||||
mt = MEDIA_TYPES.get(s.validated_data["type"])
|
||||
if mt:
|
||||
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
|
||||
if m:
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
|
||||
return Response({"result": None})
|
||||
|
||||
|
||||
@@ -661,7 +661,16 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
|
||||
with language(order.locale, self.request.event.settings.region):
|
||||
payment = order.payments.last()
|
||||
|
||||
# OrderCreateSerializer creates at most one payment
|
||||
if payment and payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
order.log_action(
|
||||
'pretix.event.order.payment.confirmed', {
|
||||
'local_id': payment.local_id,
|
||||
'provider': payment.provider,
|
||||
},
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth,
|
||||
)
|
||||
order_placed.send(self.request.event, order=order)
|
||||
if order.status == Order.STATUS_PAID:
|
||||
order_paid.send(self.request.event, order=order)
|
||||
|
||||
@@ -189,6 +189,19 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
return d
|
||||
|
||||
|
||||
class ParametrizedWaitingListEntryWebhookEvent(ParametrizedWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
# do not use content_object, this is also called in deletion
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'organizer': logentry.event.organizer.slug,
|
||||
'event': logentry.event.slug,
|
||||
'waitinglistentry': logentry.object_id,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
|
||||
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
|
||||
def register_default_webhook_events(sender, **kwargs):
|
||||
return (
|
||||
@@ -321,6 +334,22 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.testmode.deactivated',
|
||||
_('Test-Mode of shop has been deactivated'),
|
||||
),
|
||||
ParametrizedWaitingListEntryWebhookEvent(
|
||||
'pretix.event.orders.waitinglist.added',
|
||||
_('Waiting list entry added'),
|
||||
),
|
||||
ParametrizedWaitingListEntryWebhookEvent(
|
||||
'pretix.event.orders.waitinglist.changed',
|
||||
_('Waiting list entry changed'),
|
||||
),
|
||||
ParametrizedWaitingListEntryWebhookEvent(
|
||||
'pretix.event.orders.waitinglist.deleted',
|
||||
_('Waiting list entry deleted'),
|
||||
),
|
||||
ParametrizedWaitingListEntryWebhookEvent(
|
||||
'pretix.event.orders.waitinglist.voucher_assigned',
|
||||
_('Waiting list entry received voucher'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -157,6 +157,13 @@ class BaseExporter:
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def available_for_user(self, user) -> bool:
|
||||
"""
|
||||
Allows to do additional checks whether an exporter is available based on the user who calls it. Note that
|
||||
``user`` may be ``None`` e.g. during API usage.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class OrganizerLevelExportMixin:
|
||||
@property
|
||||
|
||||
@@ -850,6 +850,8 @@ class TransactionListExporter(ListExporter):
|
||||
_('Tax rule ID'),
|
||||
_('Tax rule'),
|
||||
_('Tax value'),
|
||||
_('Gross total'),
|
||||
_('Tax total'),
|
||||
]
|
||||
|
||||
if form_data.get('_format') == 'xlsx':
|
||||
@@ -901,6 +903,8 @@ class TransactionListExporter(ListExporter):
|
||||
t.tax_rule_id or '',
|
||||
str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '',
|
||||
t.tax_value,
|
||||
t.price * t.count,
|
||||
t.tax_value * t.count,
|
||||
]
|
||||
|
||||
if form_data.get('_format') == 'xlsx':
|
||||
@@ -1143,7 +1147,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
def iterate_list(self, form_data):
|
||||
qs = GiftCardTransaction.objects.filter(
|
||||
card__issuer=self.organizer,
|
||||
).order_by('datetime').select_related('card', 'order', 'order__event')
|
||||
).order_by('datetime').select_related('card', 'order', 'order__event', 'acceptor')
|
||||
|
||||
if form_data.get('date_range'):
|
||||
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
|
||||
@@ -1159,6 +1163,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
_('Amount'),
|
||||
_('Currency'),
|
||||
_('Order'),
|
||||
_('Organizer'),
|
||||
]
|
||||
yield headers
|
||||
|
||||
@@ -1170,6 +1175,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
obj.value,
|
||||
obj.card.currency,
|
||||
obj.order.full_code if obj.order else None,
|
||||
str(obj.acceptor or ""),
|
||||
]
|
||||
yield row
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
from geoip2.errors import AddressNotFoundError
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
@@ -356,9 +357,12 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
def guess_country_from_request(request, event):
|
||||
if settings.HAS_GEOIP:
|
||||
g = GeoIP2()
|
||||
res = g.country(get_client_ip(request))
|
||||
if res['country_code'] and len(res['country_code']) == 2:
|
||||
return Country(res['country_code'])
|
||||
try:
|
||||
res = g.country(get_client_ip(request))
|
||||
if res['country_code'] and len(res['country_code']) == 2:
|
||||
return Country(res['country_code'])
|
||||
except AddressNotFoundError:
|
||||
pass
|
||||
return guess_country(event)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 3.2.18 on 2023-05-12 10:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0241_itemmetaproperties_required_values'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='giftcardacceptance',
|
||||
old_name='collector',
|
||||
new_name='acceptor',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='giftcardacceptance',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='giftcardacceptance',
|
||||
name='reusable_media',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='giftcardacceptance',
|
||||
name='issuer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gift_card_acceptor_acceptance', to='pretixbase.organizer'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='giftcardacceptance',
|
||||
unique_together={('issuer', 'acceptor')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.9 on 2023-06-26 10:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0242_auto_20230512_1008'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='os_name',
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='os_version',
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
]
|
||||
@@ -143,6 +143,14 @@ class Device(LoggedModel):
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
os_name = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
os_version = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
software_brand = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
|
||||
@@ -1277,6 +1277,9 @@ class Event(EventMixin, LoggedModel):
|
||||
return not self.orders.exists() and not self.invoices.exists()
|
||||
|
||||
def delete_sub_objects(self):
|
||||
from .checkin import Checkin
|
||||
|
||||
Checkin.all.filter(successful=False, list__event=self).delete()
|
||||
self.cartposition_set.filter(addon_to__isnull=False).delete()
|
||||
self.cartposition_set.all().delete()
|
||||
self.vouchers.all().delete()
|
||||
|
||||
@@ -46,14 +46,19 @@ def gen_giftcard_secret(length=8):
|
||||
class GiftCardAcceptance(models.Model):
|
||||
issuer = models.ForeignKey(
|
||||
'Organizer',
|
||||
related_name='gift_card_collector_acceptance',
|
||||
related_name='gift_card_acceptor_acceptance',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
collector = models.ForeignKey(
|
||||
acceptor = models.ForeignKey(
|
||||
'Organizer',
|
||||
related_name='gift_card_issuer_acceptance',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
active = models.BooleanField(default=True)
|
||||
reusable_media = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('issuer', 'acceptor'),)
|
||||
|
||||
|
||||
class GiftCard(LoggedModel):
|
||||
@@ -114,7 +119,7 @@ class GiftCard(LoggedModel):
|
||||
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
|
||||
|
||||
def accepted_by(self, organizer):
|
||||
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
|
||||
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, acceptor=organizer, active=True).exists()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.secret:
|
||||
|
||||
@@ -896,6 +896,28 @@ class Order(LockModel, LoggedModel):
|
||||
), tz)
|
||||
return term_last
|
||||
|
||||
@property
|
||||
def payment_term_expire_date(self):
|
||||
delay = self.event.settings.get('payment_term_expire_delay_days', as_type=int)
|
||||
if not delay: # performance saver + backwards compatibility
|
||||
return self.expires
|
||||
|
||||
term_last = self.payment_term_last
|
||||
if term_last and self.expires > term_last: # backwards compatibility
|
||||
return self.expires
|
||||
|
||||
expires = self.expires.date() + timedelta(days=delay)
|
||||
|
||||
tz = ZoneInfo(self.event.settings.timezone)
|
||||
expires = make_aware(datetime.combine(
|
||||
expires,
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
if term_last:
|
||||
return min(expires, term_last)
|
||||
else:
|
||||
return expires
|
||||
|
||||
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||
|
||||
@@ -40,7 +40,7 @@ from django.conf import settings
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -157,17 +157,19 @@ class Organizer(LoggedModel):
|
||||
return self.cache.get_or_set(
|
||||
key='has_gift_cards',
|
||||
timeout=15,
|
||||
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.exists()
|
||||
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.filter(active=True).exists()
|
||||
)
|
||||
|
||||
@property
|
||||
def accepted_gift_cards(self):
|
||||
from .giftcards import GiftCard, GiftCardAcceptance
|
||||
|
||||
return GiftCard.objects.annotate(
|
||||
accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self))
|
||||
).filter(
|
||||
Q(issuer=self) | Q(accepted=True)
|
||||
return GiftCard.objects.filter(
|
||||
Q(issuer=self) |
|
||||
Q(issuer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=self,
|
||||
active=True,
|
||||
).values_list('issuer', flat=True))
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -22,9 +22,12 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
import jsonschema
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
from i18nfield.fields import I18nCharField
|
||||
@@ -135,6 +138,25 @@ def cc_to_vat_prefix(country_code):
|
||||
return country_code
|
||||
|
||||
|
||||
@deconstructible
|
||||
class CustomRulesValidator:
|
||||
def __call__(self, value):
|
||||
if not isinstance(value, dict):
|
||||
try:
|
||||
val = json.loads(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_('Your layout file is not a valid JSON file.'))
|
||||
else:
|
||||
val = value
|
||||
with open(finders.find('schema/tax-rules-custom.schema.json'), 'r') as f:
|
||||
schema = json.loads(f.read())
|
||||
try:
|
||||
jsonschema.validate(val, schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
e = str(e).replace('%', '%%')
|
||||
raise ValidationError(_('Your set of rules is not valid. Error message: {}').format(e))
|
||||
|
||||
|
||||
class TaxRule(LoggedModel):
|
||||
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
|
||||
internal_name = models.CharField(
|
||||
|
||||
@@ -218,7 +218,7 @@ class WaitingListEntry(LoggedModel):
|
||||
'waitinglistentry': self.pk,
|
||||
'subevent': self.subevent.pk if self.subevent else None,
|
||||
}, user=user, auth=auth)
|
||||
self.log_action('pretix.waitinglist.voucher', user=user, auth=auth)
|
||||
self.log_action('pretix.event.orders.waitinglist.voucher_assigned', user=user, auth=auth)
|
||||
self.voucher = v
|
||||
self.save()
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import pycountry
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models import Q
|
||||
from django.utils import formats
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import (
|
||||
@@ -42,8 +43,8 @@ from phonenumbers import SUPPORTED_REGIONS
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms.questions import guess_country
|
||||
from pretix.base.models import (
|
||||
ItemVariation, OrderPosition, Question, QuestionAnswer, QuestionOption,
|
||||
Seat, SubEvent,
|
||||
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
|
||||
QuestionOption, Seat, SubEvent,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.settings import (
|
||||
@@ -826,6 +827,28 @@ class QuestionColumn(ImportColumn):
|
||||
a.options.add(*a._options)
|
||||
|
||||
|
||||
class CustomerColumn(ImportColumn):
|
||||
identifier = 'customer'
|
||||
verbose_name = gettext_lazy('Customer')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
try:
|
||||
value = self.event.organizer.customers.get(
|
||||
Q(identifier=value) | Q(email__iexact=value) | Q(external_identifier=value)
|
||||
)
|
||||
except Customer.MultipleObjectsReturned:
|
||||
value = self.event.organizer.customers.get(
|
||||
Q(identifier=value)
|
||||
)
|
||||
except Customer.DoesNotExist:
|
||||
raise ValidationError(_('No matching customer was found.'))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.customer = value
|
||||
|
||||
|
||||
def get_all_columns(event):
|
||||
default = []
|
||||
if event.has_subevents:
|
||||
@@ -837,6 +860,10 @@ def get_all_columns(event):
|
||||
Variation(event),
|
||||
InvoiceAddressCompany(event),
|
||||
]
|
||||
if event.settings.customer_accounts:
|
||||
default += [
|
||||
CustomerColumn(event),
|
||||
]
|
||||
scheme = PERSON_NAME_SCHEMES.get(event.settings.name_scheme)
|
||||
for n, l, w in scheme['fields']:
|
||||
default.append(InvoiceAddressNamePart(event, n, l))
|
||||
|
||||
@@ -60,7 +60,7 @@ from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
|
||||
OrderRefund, Quota,
|
||||
OrderRefund, Quota, TaxRule,
|
||||
)
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
@@ -1015,7 +1015,11 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
|
||||
cart = get_cart(request)
|
||||
total = get_cart_total(request)
|
||||
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
|
||||
try:
|
||||
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
|
||||
except TaxRule.SaleNotAllowed:
|
||||
# ignore for now, will fail on order creation
|
||||
pass
|
||||
return total == 0
|
||||
|
||||
def order_change_allowed(self, order: Order) -> bool:
|
||||
|
||||
+28
-15
@@ -860,22 +860,32 @@ class Renderer:
|
||||
image_file = None
|
||||
|
||||
if image_file:
|
||||
ir = ThumbnailingImageReader(image_file)
|
||||
try:
|
||||
ir = ThumbnailingImageReader(image_file)
|
||||
ir.resize(float(o['width']) * mm, float(o['height']) * mm, 300)
|
||||
canvas.drawImage(
|
||||
image=ir,
|
||||
x=float(o['left']) * mm,
|
||||
y=float(o['bottom']) * mm,
|
||||
width=float(o['width']) * mm,
|
||||
height=float(o['height']) * mm,
|
||||
preserveAspectRatio=True,
|
||||
anchor='c', # centered in frame
|
||||
mask='auto'
|
||||
)
|
||||
except:
|
||||
logger.exception("Can not resize image")
|
||||
pass
|
||||
canvas.drawImage(
|
||||
image=ir,
|
||||
x=float(o['left']) * mm,
|
||||
y=float(o['bottom']) * mm,
|
||||
width=float(o['width']) * mm,
|
||||
height=float(o['height']) * mm,
|
||||
preserveAspectRatio=True,
|
||||
anchor='c', # centered in frame
|
||||
mask='auto'
|
||||
)
|
||||
logger.exception("Can not load or resize image")
|
||||
canvas.saveState()
|
||||
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
|
||||
canvas.rect(
|
||||
x=float(o['left']) * mm,
|
||||
y=float(o['bottom']) * mm,
|
||||
width=float(o['width']) * mm,
|
||||
height=float(o['height']) * mm,
|
||||
stroke=0,
|
||||
fill=1,
|
||||
)
|
||||
canvas.restoreState()
|
||||
else:
|
||||
canvas.saveState()
|
||||
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
|
||||
@@ -929,7 +939,7 @@ class Renderer:
|
||||
|
||||
# reportlab does not support unicode combination characters
|
||||
# It's important we do this before we use ArabicReshaper
|
||||
text = unicodedata.normalize("NFKC", text)
|
||||
text = unicodedata.normalize("NFC", 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.
|
||||
@@ -982,7 +992,10 @@ class Renderer:
|
||||
elif o['type'] == "poweredby":
|
||||
self._draw_poweredby(canvas, op, o)
|
||||
if self.bg_pdf:
|
||||
page_size = (self.bg_pdf.pages[0].mediabox[2], self.bg_pdf.pages[0].mediabox[3])
|
||||
page_size = (
|
||||
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
|
||||
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
|
||||
)
|
||||
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
|
||||
# swap dimensions due to pdf being rotated
|
||||
page_size = page_size[::-1]
|
||||
|
||||
@@ -317,6 +317,9 @@ class CartManager:
|
||||
def _delete_out_of_timeframe(self):
|
||||
err = None
|
||||
for cp in self.positions:
|
||||
if not cp.pk:
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start:
|
||||
err = error_messages['some_subevent_not_started']
|
||||
cp.addons.all().delete()
|
||||
|
||||
@@ -53,7 +53,8 @@ from django.utils.translation import gettext as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
|
||||
Checkin, CheckinList, Device, Event, ItemVariation, Order, OrderPosition,
|
||||
QuestionOption,
|
||||
)
|
||||
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
||||
from pretix.helpers import OF_SELF
|
||||
@@ -65,12 +66,13 @@ from pretix.helpers.jsonlogic_query import (
|
||||
)
|
||||
|
||||
|
||||
def _build_time(t=None, value=None, ev=None):
|
||||
def _build_time(t=None, value=None, ev=None, now_dt=None):
|
||||
now_dt = now_dt or now()
|
||||
if t == "custom":
|
||||
return dateutil.parser.parse(value)
|
||||
elif t == "customtime":
|
||||
parsed = dateutil.parser.parse(value)
|
||||
return now().astimezone(ev.timezone).replace(
|
||||
return now_dt.astimezone(ev.timezone).replace(
|
||||
hour=parsed.hour,
|
||||
minute=parsed.minute,
|
||||
second=parsed.second,
|
||||
@@ -84,7 +86,42 @@ def _build_time(t=None, value=None, ev=None):
|
||||
return ev.date_admission or ev.date_from
|
||||
|
||||
|
||||
def _logic_explain(rules, ev, rule_data):
|
||||
def _logic_annotate_for_graphic_explain(rules, ev, rule_data):
|
||||
logic_environment = _get_logic_environment(ev)
|
||||
event = ev if isinstance(ev, Event) else ev.event
|
||||
|
||||
def _evaluate_inners(r):
|
||||
if not isinstance(r, dict):
|
||||
return r
|
||||
operator = list(r.keys())[0]
|
||||
values = r[operator]
|
||||
if operator in ("and", "or"):
|
||||
return {operator: [_evaluate_inners(v) for v in values]}
|
||||
result = logic_environment.apply(r, rule_data)
|
||||
return {**r, '__result': result}
|
||||
|
||||
def _add_var_values(r):
|
||||
if not isinstance(r, dict):
|
||||
return r
|
||||
operator = [k for k in r.keys() if not k.startswith("__")][0]
|
||||
values = r[operator]
|
||||
if operator == "var":
|
||||
var = values[0] if isinstance(values, list) else values
|
||||
val = rule_data[var]
|
||||
if var == "product":
|
||||
val = str(event.items.get(pk=val))
|
||||
elif var == "variation":
|
||||
val = str(ItemVariation.objects.get(item__event=event, pk=val))
|
||||
elif isinstance(val, datetime):
|
||||
val = date_format(val.astimezone(ev.timezone), "SHORT_DATETIME_FORMAT")
|
||||
return {"var": var, "__result": val}
|
||||
else:
|
||||
return {**r, operator: [_add_var_values(v) for v in values]}
|
||||
|
||||
return _add_var_values(_evaluate_inners(rules))
|
||||
|
||||
|
||||
def _logic_explain(rules, ev, rule_data, now_dt=None):
|
||||
"""
|
||||
Explains when the logic denied the check-in. Only works for a denied check-in.
|
||||
|
||||
@@ -114,6 +151,7 @@ def _logic_explain(rules, ev, rule_data):
|
||||
Additionally, we favor a "close failure". Therefore, in the above example, we'd show "You can only
|
||||
get in before 17:00". In the middle of the night it would switch to "You can only get in after 09:00".
|
||||
"""
|
||||
now_dt = now_dt or now()
|
||||
logic_environment = _get_logic_environment(ev)
|
||||
_var_values = {'False': False, 'True': True}
|
||||
_var_explanations = {}
|
||||
@@ -198,9 +236,9 @@ def _logic_explain(rules, ev, rule_data):
|
||||
else:
|
||||
compare_to -= tolerance
|
||||
|
||||
var_weights[vname] = (200, abs(now() - compare_to).total_seconds())
|
||||
var_weights[vname] = (200, abs(now_dt - compare_to).total_seconds())
|
||||
|
||||
if abs(now() - compare_to) < timedelta(hours=12):
|
||||
if abs(now_dt - compare_to) < timedelta(hours=12):
|
||||
compare_to_text = date_format(compare_to, 'TIME_FORMAT')
|
||||
else:
|
||||
compare_to_text = date_format(compare_to, 'SHORT_DATETIME_FORMAT')
|
||||
@@ -357,7 +395,7 @@ class LazyRuleVars:
|
||||
@cached_property
|
||||
def entries_today(self):
|
||||
tz = self._clist.event.timezone
|
||||
midnight = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
midnight = self._dt.astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
|
||||
|
||||
@cached_property
|
||||
@@ -378,7 +416,7 @@ class LazyRuleVars:
|
||||
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
|
||||
# consistent.
|
||||
return -1
|
||||
return (now() - last_entry.datetime).total_seconds() // 60
|
||||
return (self._dt - last_entry.datetime).total_seconds() // 60
|
||||
|
||||
@cached_property
|
||||
def minutes_since_first_entry(self):
|
||||
@@ -390,7 +428,7 @@ class LazyRuleVars:
|
||||
# between platforms (None<1 is true on some, but not all), we rather choose something that is at least
|
||||
# consistent.
|
||||
return -1
|
||||
return (now() - last_entry.datetime).total_seconds() // 60
|
||||
return (self._dt - last_entry.datetime).total_seconds() // 60
|
||||
|
||||
|
||||
class SQLLogic:
|
||||
@@ -693,7 +731,7 @@ def _save_answers(op, answers, given_answers):
|
||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False):
|
||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False):
|
||||
"""
|
||||
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.
|
||||
@@ -707,6 +745,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
:param questions_supported: When set to False, questions are ignored
|
||||
:param nonce: A random nonce to prevent race conditions.
|
||||
:param datetime: The datetime of the checkin, defaults to now.
|
||||
:param simulate: If true, the check-in is not saved.
|
||||
"""
|
||||
|
||||
# !!!!!!!!!
|
||||
@@ -734,7 +773,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
'blocked'
|
||||
)
|
||||
|
||||
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > now():
|
||||
if type != Checkin.TYPE_EXIT and op.valid_from and op.valid_from > dt:
|
||||
if force:
|
||||
force_used = True
|
||||
else:
|
||||
@@ -748,7 +787,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
),
|
||||
)
|
||||
|
||||
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < now():
|
||||
if type != Checkin.TYPE_EXIT and op.valid_until and op.valid_until < dt:
|
||||
if force:
|
||||
force_used = True
|
||||
else:
|
||||
@@ -773,7 +812,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
if q not in given_answers and q not in answers:
|
||||
require_answers.append(q)
|
||||
|
||||
_save_answers(op, answers, given_answers)
|
||||
if not simulate:
|
||||
_save_answers(op, answers, given_answers)
|
||||
|
||||
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
|
||||
@@ -859,30 +899,33 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
return
|
||||
|
||||
if entry_allowed or force:
|
||||
ci = Checkin.objects.create(
|
||||
position=op,
|
||||
type=type,
|
||||
list=clist,
|
||||
datetime=dt,
|
||||
device=device,
|
||||
gate=device.gate if device else None,
|
||||
nonce=nonce,
|
||||
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
||||
force_sent=force,
|
||||
raw_barcode=raw_barcode,
|
||||
raw_source_type=raw_source_type,
|
||||
)
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': True,
|
||||
'forced': force or op.order.status != Order.STATUS_PAID,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'answers': {k.pk: str(v) for k, v in given_answers.items()},
|
||||
'list': clist.pk
|
||||
}, user=user, auth=auth)
|
||||
checkin_created.send(op.order.event, checkin=ci)
|
||||
if simulate:
|
||||
return True
|
||||
else:
|
||||
ci = Checkin.objects.create(
|
||||
position=op,
|
||||
type=type,
|
||||
list=clist,
|
||||
datetime=dt,
|
||||
device=device,
|
||||
gate=device.gate if device else None,
|
||||
nonce=nonce,
|
||||
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
||||
force_sent=force,
|
||||
raw_barcode=raw_barcode,
|
||||
raw_source_type=raw_source_type,
|
||||
)
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': True,
|
||||
'forced': force or op.order.status != Order.STATUS_PAID,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'answers': {k.pk: str(v) for k, v in given_answers.items()},
|
||||
'list': clist.pk
|
||||
}, user=user, auth=auth)
|
||||
checkin_created.send(op.order.event, checkin=ci)
|
||||
else:
|
||||
raise CheckInError(
|
||||
_('This ticket has already been redeemed.'),
|
||||
|
||||
@@ -290,6 +290,8 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
|
||||
if isinstance(exporter, OrganizerLevelExportMixin):
|
||||
if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission):
|
||||
has_permission = False
|
||||
if exporter and not exporter.available_for_user(schedule.owner):
|
||||
has_permission = False
|
||||
|
||||
_run_scheduled_export(
|
||||
schedule,
|
||||
|
||||
@@ -935,7 +935,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
raise OrderError(e.message)
|
||||
|
||||
require_approval = any(p.requires_approval(invoice_address=address) for p in positions)
|
||||
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
|
||||
try:
|
||||
fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(error_messages['country_blocked'])
|
||||
total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees])
|
||||
|
||||
order = Order(
|
||||
@@ -968,7 +971,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
|
||||
for fee in fees:
|
||||
fee.order = order
|
||||
fee._calculate_tax()
|
||||
try:
|
||||
fee._calculate_tax()
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(error_messages['country_blocked'])
|
||||
if fee.tax_rule and not fee.tax_rule.pk:
|
||||
fee.tax_rule = None # TODO: deprecate
|
||||
fee.save()
|
||||
@@ -1264,12 +1270,12 @@ def expire_orders(sender, **kwargs):
|
||||
Exists(
|
||||
OrderFee.objects.filter(order_id=OuterRef('pk'), fee_type=OrderFee.FEE_TYPE_CANCELLATION)
|
||||
)
|
||||
).select_related('event').order_by('event_id')
|
||||
).prefetch_related('event').order_by('event_id')
|
||||
for o in qs:
|
||||
if o.event_id != event_id:
|
||||
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
|
||||
event_id = o.event_id
|
||||
if expire:
|
||||
if expire and now() >= o.payment_term_expire_date:
|
||||
mark_order_expired(o)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
@@ -31,7 +32,7 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import inspect
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from tempfile import NamedTemporaryFile
|
||||
@@ -41,10 +42,13 @@ from zipfile import ZipFile
|
||||
from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedFile, Event, User, cachedfile_name
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.shredder import ShredError
|
||||
from pretix.celery_app import app
|
||||
@@ -101,8 +105,20 @@ def export(event: Event, shredders: List[str], session_key=None, cfid=None) -> N
|
||||
return cf.pk
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, throws=(ShredError,))
|
||||
def shred(event: Event, fileid: str, confirm_code: str) -> None:
|
||||
@app.task(base=ProfiledEventTask, throws=(ShredError,), bind=True)
|
||||
def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, locale: str='en') -> None:
|
||||
steps = []
|
||||
|
||||
if user:
|
||||
user = User.objects.get(pk=user)
|
||||
|
||||
def set_progress(val):
|
||||
if not self.request.called_directly:
|
||||
self.update_state(
|
||||
state='PROGRESS',
|
||||
meta={'value': val, 'steps': steps}
|
||||
)
|
||||
|
||||
known_shredders = event.get_data_shredders()
|
||||
try:
|
||||
cf = CachedFile.objects.get(pk=fileid)
|
||||
@@ -124,8 +140,52 @@ def shred(event: Event, fileid: str, confirm_code: str) -> None:
|
||||
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):
|
||||
raise ShredError(_("Something happened in your event after the export, please try again."))
|
||||
|
||||
for shredder in shredders:
|
||||
shredder.shred_data()
|
||||
event.log_action(
|
||||
'pretix.event.shredder.started', user=user, data={
|
||||
'indexdata': indexdata
|
||||
}
|
||||
)
|
||||
|
||||
for i, shredder in enumerate(shredders):
|
||||
with language(locale):
|
||||
steps.append({'label': str(shredder.verbose_name), 'done': False})
|
||||
set_progress(i * 100 / len(shredders))
|
||||
if 'progress_callback' in inspect.signature(shredder.shred_data).parameters:
|
||||
shredder.shred_data(
|
||||
progress_callback=lambda y: set_progress(
|
||||
i * 100 / len(shredders) + min(max(y, 0), 100) / 100 * 100 / len(shredders)
|
||||
)
|
||||
)
|
||||
else:
|
||||
shredder.shred_data()
|
||||
steps[-1]['done'] = True
|
||||
|
||||
cf.file.delete(save=False)
|
||||
cf.delete()
|
||||
|
||||
event.log_action(
|
||||
'pretix.event.shredder.completed', user=user, data={
|
||||
'indexdata': indexdata
|
||||
}
|
||||
)
|
||||
|
||||
if user:
|
||||
with language(user.locale):
|
||||
try:
|
||||
mail(
|
||||
user.email,
|
||||
_('Data shredding completed'),
|
||||
'pretixbase/email/shred_completed.txt',
|
||||
{
|
||||
'user': user,
|
||||
'organizer': event.organizer.name,
|
||||
'event': str(event.name),
|
||||
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
|
||||
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
|
||||
},
|
||||
event=None,
|
||||
user=user,
|
||||
locale=user.locale,
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
|
||||
+89
-62
@@ -893,6 +893,28 @@ DEFAULTS = {
|
||||
"the pool and can be ordered by other people."),
|
||||
)
|
||||
},
|
||||
'payment_term_expire_delay_days': {
|
||||
'default': '0',
|
||||
'type': int,
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Expiration delay'),
|
||||
help_text=_("The order will only actually expire this many days after the expiration date communicated "
|
||||
"to the customer. However, this will not delay beyond the \"last date of payments\" "
|
||||
"configured above, which is always enforced. The delay may also end on a weekend regardless "
|
||||
"of the other settings above."),
|
||||
# Every order in between the official expiry date and the delayed expiry date has a performance penalty
|
||||
# for the cron job, so we limit this feature to 30 days to prevent arbitrary numbers of orders needing
|
||||
# to be checked.
|
||||
min_value=0,
|
||||
max_value=30,
|
||||
),
|
||||
'serializer_kwargs': dict(
|
||||
min_value=0,
|
||||
max_value=30,
|
||||
),
|
||||
},
|
||||
'payment_pending_hidden': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -2018,8 +2040,8 @@ to your order for {event}.
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_resend_all_links': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2034,8 +2056,8 @@ The list is as follows:
|
||||
|
||||
{orders}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_free_attendee': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2050,8 +2072,8 @@ you have been registered for {event} successfully.
|
||||
You can view the details and status of your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_send_order_free_attendee': {
|
||||
'type': bool,
|
||||
@@ -2071,8 +2093,8 @@ no payment is required.
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_placed_require_approval': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2089,8 +2111,8 @@ be patient and wait for our next email.
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_placed': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2108,8 +2130,8 @@ of {total_with_currency}. Please complete your payment before {expire_date}.
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_attachment_new_order': {
|
||||
'default': None,
|
||||
@@ -2119,11 +2141,14 @@ Your {event} team"""))
|
||||
label=_('Attachment for new orders'),
|
||||
ext_whitelist=(".pdf",),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT,
|
||||
help_text=_('This file will be attached to the first email that we send for every new order. Therefore it will be '
|
||||
'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent '
|
||||
'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use '
|
||||
'it to send non-public information as this file might be sent before payment is confirmed or the order '
|
||||
'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.').format(
|
||||
help_text=format_lazy(
|
||||
_(
|
||||
'This file will be attached to the first email that we send for every new order. Therefore it will be '
|
||||
'combined with the "Placed order", "Free order", or "Received order" texts from above. It will be sent '
|
||||
'to both order contacts and attendees. You can use this e.g. to send your terms of service. Do not use '
|
||||
'it to send non-public information as this file might be sent before payment is confirmed or the order '
|
||||
'is approved. To avoid this vital email going to spam, you can only upload PDF files of up to {size} MB.'
|
||||
),
|
||||
size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT // (1024 * 1024),
|
||||
)
|
||||
),
|
||||
@@ -2152,8 +2177,8 @@ a ticket for {event} has been ordered for you.
|
||||
You can view the details and status of your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_changed': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2168,8 +2193,8 @@ your order for {event} has been changed.
|
||||
You can view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_paid': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2186,8 +2211,8 @@ we successfully received your payment for {event}. Thank you!
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_send_order_paid_attendee': {
|
||||
'type': bool,
|
||||
@@ -2206,8 +2231,8 @@ a ticket for {event} that has been ordered for you is now paid.
|
||||
You can view the details and status of your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_days_order_expire_warning': {
|
||||
'form_class': forms.IntegerField,
|
||||
@@ -2239,8 +2264,8 @@ your payment before {expire_date}.
|
||||
You can view the payment information and the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_pending_warning': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2256,8 +2281,8 @@ Please keep in mind that you are required to pay before {expire_date}.
|
||||
You can view the payment information and the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_incomplete_payment': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2276,8 +2301,8 @@ missing additional payment of **{pending_sum}**.
|
||||
You can view the payment information and the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_waiting_list': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2309,8 +2334,8 @@ as possible to the next person on the waiting list:
|
||||
|
||||
{url_remove}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_canceled': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2327,8 +2352,8 @@ your order {code} for {event} has been canceled.
|
||||
You can view the details of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_approved': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2347,8 +2372,8 @@ You can select a payment method and perform the payment here:
|
||||
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_send_order_approved_attendee': {
|
||||
'type': bool,
|
||||
@@ -2367,8 +2392,8 @@ we approved a ticket ordered for you for {event}.
|
||||
You can view the details and status of your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_approved_free': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2384,8 +2409,8 @@ at our event. As you only ordered free products, no payment is required.
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_send_order_approved_free_attendee': {
|
||||
'type': bool,
|
||||
@@ -2404,8 +2429,8 @@ we approved a ticket ordered for you for {event}.
|
||||
You can view the details and status of your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_order_denied': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2423,8 +2448,8 @@ You can view the details of your order here:
|
||||
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_text_order_custom_mail': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2433,8 +2458,8 @@ Your {event} team"""))
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_days_download_reminder': {
|
||||
'type': int,
|
||||
@@ -2452,13 +2477,13 @@ Your {event} team"""))
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||
|
||||
you are registered for {event}.
|
||||
you are registered for {event}.
|
||||
|
||||
If you did not do so already, you can download your ticket here:
|
||||
{url}
|
||||
If you did not do so already, you can download your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_download_reminder': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2473,8 +2498,8 @@ you bought a ticket for {event}.
|
||||
If you did not do so already, you can download your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
Best regards,
|
||||
Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_customer_registration': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2494,9 +2519,9 @@ This link is valid for one day.
|
||||
|
||||
If you did not sign up yourself, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
Best regards,
|
||||
|
||||
Your {organizer} team"""))
|
||||
Your {organizer} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_customer_email_change': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2516,9 +2541,9 @@ This link is valid for one day.
|
||||
|
||||
If you did not request this, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
Best regards,
|
||||
|
||||
Your {organizer} team"""))
|
||||
Your {organizer} team""")) # noqa: W291
|
||||
},
|
||||
'mail_subject_customer_reset': {
|
||||
'type': LazyI18nString,
|
||||
@@ -2538,9 +2563,9 @@ This link is valid for one day.
|
||||
|
||||
If you did not request a new password, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
Best regards,
|
||||
|
||||
Your {organizer} team"""))
|
||||
Your {organizer} team""")) # noqa: W291
|
||||
},
|
||||
'smtp_use_custom': {
|
||||
'default': 'False',
|
||||
@@ -3583,8 +3608,10 @@ class SettingsSandbox:
|
||||
def __delattr__(self, key: str) -> None:
|
||||
del self._event.settings[self._convert_key(key)]
|
||||
|
||||
def get(self, key: str, default: Any = None, as_type: type = str):
|
||||
return self._event.settings.get(self._convert_key(key), default=default, as_type=as_type)
|
||||
def get(self, key: str, default: Any = None, as_type: type = str, binary_file: bool = False):
|
||||
return self._event.settings.get(
|
||||
self._convert_key(key), default=default, as_type=as_type, binary_file=binary_file
|
||||
)
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
self._event.settings.set(self._convert_key(key), value)
|
||||
|
||||
+232
-66
@@ -32,11 +32,12 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import List, Tuple
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Max, Q
|
||||
from django.db.models.functions import Greatest
|
||||
from django.dispatch import receiver
|
||||
@@ -99,11 +100,13 @@ class BaseDataShredder:
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def shred_data(self):
|
||||
def shred_data(self, progress_callback=None):
|
||||
"""
|
||||
This method is called to actually remove the data from the system. You should remove any database objects
|
||||
here.
|
||||
|
||||
You can call ``progress_callback`` with an integer value between 0 and 100 to communicate back your progress.
|
||||
|
||||
You should never delete ``LogEntry`` objects, but you might modify them to remove personal data. In this
|
||||
case, set the ``LogEntry.shredded`` attribute to ``True`` to show that this is no longer original log data.
|
||||
"""
|
||||
@@ -151,6 +154,7 @@ class BaseDataShredder:
|
||||
|
||||
def shred_log_fields(logentry, banlist=None, whitelist=None):
|
||||
d = logentry.parsed_data
|
||||
initial_data = copy.copy(d)
|
||||
shredded = False
|
||||
if whitelist:
|
||||
for k, v in d.items():
|
||||
@@ -162,9 +166,61 @@ def shred_log_fields(logentry, banlist=None, whitelist=None):
|
||||
if f in d:
|
||||
d[f] = '█'
|
||||
shredded = True
|
||||
logentry.data = json.dumps(d)
|
||||
logentry.shredded = logentry.shredded or shredded
|
||||
logentry.save(update_fields=['data', 'shredded'])
|
||||
if d != initial_data:
|
||||
logentry.data = json.dumps(d)
|
||||
logentry.shredded = logentry.shredded or shredded
|
||||
logentry.save(update_fields=['data', 'shredded'])
|
||||
|
||||
|
||||
def slow_update(qs, batch_size=1000, sleep_time=.5, progress_callback=None, progress_offset=0, progress_total=None, **update):
|
||||
"""
|
||||
Doing UPDATE queries on hundreds of thousands of rows can cause outages due to high write load on the database.
|
||||
This provides a throttled way to update rows. The condition for this to work properly is that the queryset has a
|
||||
filter condition that no longer applies after the update!
|
||||
Otherwise, this will be an endless loop!
|
||||
"""
|
||||
total_updated = 0
|
||||
while True:
|
||||
updated = qs.order_by().filter(
|
||||
pk__in=qs.order_by().values_list('pk', flat=True)[:batch_size]
|
||||
).update(**update)
|
||||
total_updated += updated
|
||||
if not updated:
|
||||
break
|
||||
if total_updated >= 0.8 * batch_size:
|
||||
time.sleep(sleep_time)
|
||||
if progress_callback and progress_total:
|
||||
progress_callback((progress_offset + total_updated) / progress_total)
|
||||
|
||||
return total_updated
|
||||
|
||||
|
||||
def slow_delete(qs, batch_size=1000, sleep_time=.5, progress_callback=None, progress_offset=0, progress_total=None):
|
||||
"""
|
||||
Doing DELETE queries on hundreds of thousands of rows can cause outages due to high write load on the database.
|
||||
This provides a throttled way to update rows.
|
||||
"""
|
||||
total_deleted = 0
|
||||
while True:
|
||||
deleted = qs.order_by().filter(
|
||||
pk__in=qs.order_by().values_list('pk', flat=True)[:batch_size]
|
||||
).delete()[0]
|
||||
total_deleted += deleted
|
||||
if not deleted:
|
||||
break
|
||||
if total_deleted >= 0.8 * batch_size:
|
||||
time.sleep(sleep_time)
|
||||
return total_deleted
|
||||
|
||||
|
||||
def _progress_helper(queryset, progress_callback, offset, total):
|
||||
if not progress_callback:
|
||||
yield from queryset
|
||||
else:
|
||||
for i, o in enumerate(queryset):
|
||||
yield o
|
||||
if i % 10 == 0:
|
||||
progress_callback((i + offset) / total * 100)
|
||||
|
||||
|
||||
class PhoneNumberShredder(BaseDataShredder):
|
||||
@@ -177,18 +233,26 @@ class PhoneNumberShredder(BaseDataShredder):
|
||||
o.code: o.phone for o in self.event.orders.filter(phone__isnull=False)
|
||||
}, cls=CustomJSONEncoder, indent=4)
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
for o in self.event.orders.all():
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_orders = self.event.orders.all()
|
||||
qs_orders_cnt = qs_orders.count()
|
||||
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.phone.changed")
|
||||
qs_le_cnt = qs_le.count()
|
||||
total = qs_le_cnt + qs_orders_cnt
|
||||
|
||||
for o in _progress_helper(qs_orders, progress_callback, 0, total):
|
||||
changed = bool(o.phone)
|
||||
o.phone = None
|
||||
d = o.meta_info_data
|
||||
if d:
|
||||
if 'contact_form_data' in d and 'phone' in d['contact_form_data']:
|
||||
changed = True
|
||||
del d['contact_form_data']['phone']
|
||||
o.meta_info = json.dumps(d)
|
||||
o.save(update_fields=['meta_info', 'phone'])
|
||||
o.meta_info = json.dumps(d)
|
||||
if changed:
|
||||
o.save(update_fields=['meta_info', 'phone'])
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.phone.changed"):
|
||||
for le in _progress_helper(qs_le, progress_callback, qs_orders_cnt, total):
|
||||
shred_log_fields(le, banlist=['old_phone', 'new_phone'])
|
||||
|
||||
|
||||
@@ -207,37 +271,66 @@ class EmailAddressShredder(BaseDataShredder):
|
||||
for op in OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False)
|
||||
}, indent=4)
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False).update(attendee_email=None)
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_op = OrderPosition.all.filter(order__event=self.event, attendee_email__isnull=False)
|
||||
qs_op_cnt = qs_op.count()
|
||||
|
||||
for o in self.event.orders.all():
|
||||
qs_orders = self.event.orders.all()
|
||||
qs_orders_cnt = qs_orders.count()
|
||||
|
||||
qs_le = self.event.logentry_set.filter(
|
||||
Q(action_type__contains="order.email") | Q(action_type__contains="position.email") |
|
||||
Q(action_type="pretix.event.order.contact.changed") |
|
||||
Q(action_type="pretix.event.order.modified")
|
||||
).exclude(data="")
|
||||
qs_le_cnt = qs_le.count()
|
||||
|
||||
total = qs_op_cnt + qs_orders_cnt + qs_le_cnt
|
||||
|
||||
slow_update(
|
||||
qs_op,
|
||||
attendee_email=None,
|
||||
progress_callback=progress_callback,
|
||||
progress_offset=0,
|
||||
progress_total=total,
|
||||
# Updates to order position table are slow, since PostgreSQL needs to update many indexes, so let's
|
||||
# take them really slowly to not overwhelm the database.
|
||||
batch_size=100,
|
||||
sleep_time=2,
|
||||
)
|
||||
|
||||
for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total):
|
||||
changed = bool(o.email) or bool(o.customer)
|
||||
o.email = None
|
||||
o.customer = None
|
||||
d = o.meta_info_data
|
||||
if d:
|
||||
if 'contact_form_data' in d and 'email' in d['contact_form_data']:
|
||||
del d['contact_form_data']['email']
|
||||
o.meta_info = json.dumps(d)
|
||||
o.save(update_fields=['meta_info', 'email', 'customer'])
|
||||
changed = True
|
||||
o.meta_info = json.dumps(d)
|
||||
if 'contact_form_data' in d and 'email_repeat' in d['contact_form_data']:
|
||||
del d['contact_form_data']['email_repeat']
|
||||
changed = True
|
||||
if changed:
|
||||
if d:
|
||||
o.meta_info = json.dumps(d)
|
||||
o.save(update_fields=['meta_info', 'email', 'customer'])
|
||||
|
||||
for le in self.event.logentry_set.filter(
|
||||
Q(action_type__contains="order.email") | Q(action_type__contains="position.email"),
|
||||
):
|
||||
shred_log_fields(le, banlist=['recipient', 'message', 'subject', 'full_mail'])
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.contact.changed"):
|
||||
shred_log_fields(le, banlist=['old_email', 'new_email'])
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
if 'data' in d:
|
||||
for row in d['data']:
|
||||
if 'attendee_email' in row:
|
||||
row['attendee_email'] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
for le in _progress_helper(qs_le, progress_callback, qs_op_cnt + qs_orders_cnt, total):
|
||||
if le.action_type == "pretix.event.order.modified":
|
||||
d = le.parsed_data
|
||||
if 'data' in d:
|
||||
for row in d['data']:
|
||||
if 'attendee_email' in row:
|
||||
row['attendee_email'] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
else:
|
||||
shred_log_fields(le, banlist=[
|
||||
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
|
||||
])
|
||||
|
||||
|
||||
class WaitingListShredder(BaseDataShredder):
|
||||
@@ -251,16 +344,35 @@ class WaitingListShredder(BaseDataShredder):
|
||||
for wle in self.event.waitinglistentries.all()
|
||||
], indent=4)
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
self.event.waitinglistentries.update(name_cached=None, name_parts={'_shredded': True}, email='█', phone='█')
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_wle = self.event.waitinglistentries.exclude(email='█')
|
||||
qs_wle_cnt = qs_wle.count()
|
||||
|
||||
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
|
||||
qs_voucher = self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False)
|
||||
qs_voucher_cnt = qs_voucher.count()
|
||||
|
||||
qs_le = self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data="")
|
||||
qs_le_cnt = qs_le.count()
|
||||
|
||||
total = qs_voucher_cnt + qs_wle_cnt + qs_le_cnt
|
||||
|
||||
slow_update(
|
||||
qs_wle,
|
||||
name_cached=None,
|
||||
name_parts={'_shredded': True},
|
||||
email='█',
|
||||
phone='█',
|
||||
progress_callback=progress_callback,
|
||||
progress_offset=0,
|
||||
progress_total=total,
|
||||
)
|
||||
|
||||
for wle in _progress_helper(qs_voucher, progress_callback, qs_wle_cnt, total):
|
||||
if '@' in wle.voucher.comment:
|
||||
wle.voucher.comment = '█'
|
||||
wle.voucher.save(update_fields=['comment'])
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
|
||||
for le in _progress_helper(qs_le, progress_callback, qs_wle_cnt + qs_voucher_cnt, total):
|
||||
d = le.parsed_data
|
||||
if 'name' in d:
|
||||
d['name'] = '█'
|
||||
@@ -298,17 +410,41 @@ class AttendeeInfoShredder(BaseDataShredder):
|
||||
)
|
||||
}, indent=4)
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
OrderPosition.all.filter(
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_op = OrderPosition.all.filter(
|
||||
order__event=self.event
|
||||
).filter(
|
||||
Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
|
||||
Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
|
||||
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
|
||||
zipcode=None, city=None)
|
||||
Q(attendee_name_cached__isnull=False) |
|
||||
Q(company__isnull=False) |
|
||||
Q(street__isnull=False) |
|
||||
Q(zipcode__isnull=False) |
|
||||
Q(city__isnull=False)
|
||||
)
|
||||
qs_op_cnt = qs_op.count()
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
|
||||
qs_le_cnt = qs_le.count()
|
||||
|
||||
total = qs_op_cnt + qs_le_cnt
|
||||
|
||||
slow_update(
|
||||
qs_op,
|
||||
attendee_name_cached=None,
|
||||
attendee_name_parts={'_shredded': True},
|
||||
company=None,
|
||||
street=None,
|
||||
zipcode=None,
|
||||
city=None,
|
||||
progress_callback=progress_callback,
|
||||
progress_total=total,
|
||||
progress_offset=0,
|
||||
# Updates to order position table are slow, since PostgreSQL needs to update many indexes, so let's
|
||||
# take them really slowly to not overwhelm the database.
|
||||
batch_size=100,
|
||||
sleep_time=2,
|
||||
)
|
||||
|
||||
for le in _progress_helper(qs_le, progress_callback, qs_op_cnt, total):
|
||||
d = le.parsed_data
|
||||
if 'data' in d:
|
||||
for i, row in enumerate(d['data']):
|
||||
@@ -343,11 +479,18 @@ class InvoiceAddressShredder(BaseDataShredder):
|
||||
for ia in InvoiceAddress.objects.filter(order__event=self.event)
|
||||
}, indent=4)
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
InvoiceAddress.objects.filter(order__event=self.event).delete()
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_ia = InvoiceAddress.objects.filter(order__event=self.event)
|
||||
qs_ia_cnt = qs_ia.count()
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
|
||||
qs_le_cnt = qs_le.count()
|
||||
|
||||
total = qs_ia_cnt + qs_le_cnt
|
||||
|
||||
slow_delete(qs_ia, progress_callback=progress_callback, progress_total=total, progress_offset=0)
|
||||
|
||||
for le in _progress_helper(qs_le, progress_callback, qs_ia_cnt, total):
|
||||
d = le.parsed_data
|
||||
if 'invoice_data' in d and not isinstance(d['invoice_data'], bool):
|
||||
for field in d['invoice_data']:
|
||||
@@ -375,11 +518,18 @@ class QuestionAnswerShredder(BaseDataShredder):
|
||||
).data
|
||||
yield 'question-answers.json', 'application/json', json.dumps(d, indent=4)
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
QuestionAnswer.objects.filter(orderposition__order__event=self.event).delete()
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_qa = QuestionAnswer.objects.filter(orderposition__order__event=self.event)
|
||||
qs_qa_cnt = qs_qa.count()
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||
qs_le = self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data="")
|
||||
qs_le_cnt = qs_le.count()
|
||||
|
||||
total = qs_qa_cnt + qs_le_cnt
|
||||
|
||||
slow_delete(qs_qa, progress_callback=progress_callback, progress_total=total, progress_offset=0)
|
||||
|
||||
for le in _progress_helper(qs_le, progress_callback, qs_qa_cnt, total):
|
||||
d = le.parsed_data
|
||||
if 'data' in d:
|
||||
for i, row in enumerate(d['data']):
|
||||
@@ -408,9 +558,11 @@ class InvoiceShredder(BaseDataShredder):
|
||||
yield 'invoices/{}.pdf'.format(i.number), 'application/pdf', i.file.read()
|
||||
i.file.close()
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
for i in self.event.invoices.filter(shredded=False):
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_i = self.event.invoices.filter(shredded=False)
|
||||
total = qs_i.count()
|
||||
|
||||
for i in _progress_helper(qs_i, progress_callback, 0, total):
|
||||
if i.file:
|
||||
i.file.delete()
|
||||
i.shredded = True
|
||||
@@ -430,10 +582,17 @@ class CachedTicketShredder(BaseDataShredder):
|
||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||
pass
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
CachedTicket.objects.filter(order_position__order__event=self.event).delete()
|
||||
CachedCombinedTicket.objects.filter(order__event=self.event).delete()
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_1 = CachedTicket.objects.filter(order_position__order__event=self.event)
|
||||
qs_1_cnt = qs_1.count()
|
||||
|
||||
qs_2 = CachedCombinedTicket.objects.filter(order__event=self.event)
|
||||
qs_2_cnt = qs_2.count()
|
||||
|
||||
total = qs_1_cnt + qs_2_cnt
|
||||
|
||||
slow_delete(qs_1, progress_callback=progress_callback, progress_total=total, progress_offset=0)
|
||||
slow_delete(qs_2, progress_callback=progress_callback, progress_total=total, progress_offset=qs_1_cnt)
|
||||
|
||||
|
||||
class PaymentInfoShredder(BaseDataShredder):
|
||||
@@ -446,14 +605,21 @@ class PaymentInfoShredder(BaseDataShredder):
|
||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||
pass
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
def shred_data(self, progress_callback=None):
|
||||
qs_p = OrderPayment.objects.filter(order__event=self.event)
|
||||
qs_p_count = qs_p.count()
|
||||
qs_r = OrderRefund.objects.filter(order__event=self.event)
|
||||
qs_r_count = qs_r.count()
|
||||
|
||||
total = qs_p_count + qs_r_count
|
||||
|
||||
provs = self.event.get_payment_providers()
|
||||
for obj in OrderPayment.objects.filter(order__event=self.event):
|
||||
for obj in _progress_helper(qs_p, progress_callback, 0, total):
|
||||
pprov = provs.get(obj.provider)
|
||||
if pprov:
|
||||
pprov.shred_payment_info(obj)
|
||||
for obj in OrderRefund.objects.filter(order__event=self.event):
|
||||
|
||||
for obj in _progress_helper(qs_r, progress_callback, qs_p_count, total):
|
||||
pprov = provs.get(obj.provider)
|
||||
if pprov:
|
||||
pprov.shred_payment_info(obj)
|
||||
|
||||
@@ -175,6 +175,10 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre, pre code {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
{% if rtl %}
|
||||
body {
|
||||
direction: rtl;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% load i18n %}
|
||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
||||
|
||||
we hereby confirm that the following data shredding job has been completed:
|
||||
|
||||
Organizer: {{ organizer }}
|
||||
|
||||
Event: {{ event }}
|
||||
|
||||
Data selection: {{ shredders }}
|
||||
|
||||
Start time: {{ start_time }} (new data added after this time might not have been deleted)
|
||||
|
||||
Best regards,
|
||||
|
||||
Your pretix team
|
||||
{% endblocktrans %}
|
||||
@@ -91,12 +91,9 @@
|
||||
/* These are technically the same, but use both */
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
||||
-ms-word-break: break-all;
|
||||
/* This is the dangerous one in WebKit, as it breaks things wherever */
|
||||
word-break: break-all;
|
||||
/* Instead use this non-standard one: */
|
||||
word-break: break-word;
|
||||
|
||||
/* Adds a hyphen where the word breaks, if supported (No Blink) */
|
||||
-ms-hyphens: auto;
|
||||
@@ -105,6 +102,15 @@
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p a {
|
||||
word-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
@@ -177,6 +183,7 @@
|
||||
}
|
||||
|
||||
.order {
|
||||
border-top: 1px solid #ccc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -194,6 +201,10 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre, pre code {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
{% if rtl %}
|
||||
body {
|
||||
direction: rtl;
|
||||
|
||||
@@ -46,6 +46,10 @@ from django.urls import reverse
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
from markdown import Extension
|
||||
from markdown.inlinepatterns import SubstituteTagInlineProcessor
|
||||
from markdown.postprocessors import Postprocessor
|
||||
from markdown.treeprocessors import UnescapeTreeprocessor
|
||||
from tlds import tld_set
|
||||
|
||||
register = template.Library()
|
||||
@@ -106,6 +110,8 @@ URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, rev
|
||||
|
||||
EMAIL_RE = SimpleLazyObject(lambda: build_email_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
||||
|
||||
DOT_ESCAPE = "|escaped-dot-sGnY9LMK|"
|
||||
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
"""
|
||||
@@ -168,6 +174,124 @@ def abslink_callback(attrs, new=False):
|
||||
return attrs
|
||||
|
||||
|
||||
class EmailNl2BrExtension(Extension):
|
||||
"""
|
||||
In emails (mostly for backwards-compatibility), we do not follow GitHub Flavored Markdown in preserving newlines.
|
||||
Instead, we follow the CommonMark specification:
|
||||
|
||||
"A line ending (not in a code span or HTML tag) that is preceded by two or more spaces and does not occur at the
|
||||
end of a block is parsed as a hard line break (rendered in HTML as a <br /> tag)"
|
||||
"""
|
||||
BR_RE = r' \n'
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
br_tag = SubstituteTagInlineProcessor(self.BR_RE, 'br')
|
||||
md.inlinePatterns.register(br_tag, 'nl', 5)
|
||||
|
||||
|
||||
class LinkifyPostprocessor(Postprocessor):
|
||||
def __init__(self, linker):
|
||||
self.linker = linker
|
||||
super().__init__()
|
||||
|
||||
def run(self, text):
|
||||
return self.linker.linkify(text)
|
||||
|
||||
|
||||
class CleanPostprocessor(Postprocessor):
|
||||
def __init__(self, tags, attributes, protocols, strip):
|
||||
self.tags = tags
|
||||
self.attributes = attributes
|
||||
self.protocols = protocols
|
||||
self.strip = strip
|
||||
super().__init__()
|
||||
|
||||
def run(self, text):
|
||||
return bleach.clean(
|
||||
text,
|
||||
tags=self.tags,
|
||||
attributes=self.attributes,
|
||||
protocols=self.protocols,
|
||||
strip=self.strip
|
||||
)
|
||||
|
||||
|
||||
class CustomUnescapeTreeprocessor(UnescapeTreeprocessor):
|
||||
"""
|
||||
This un-escapes everything except \\.
|
||||
"""
|
||||
|
||||
def _unescape(self, m):
|
||||
if m.group(1) == "46": # 46 is the ASCII position of .
|
||||
return DOT_ESCAPE
|
||||
return chr(int(m.group(1)))
|
||||
|
||||
|
||||
class CustomUnescapePostprocessor(Postprocessor):
|
||||
"""
|
||||
Restore escaped .
|
||||
"""
|
||||
|
||||
def run(self, text):
|
||||
return text.replace(DOT_ESCAPE, ".")
|
||||
|
||||
|
||||
class LinkifyAndCleanExtension(Extension):
|
||||
r"""
|
||||
We want to do:
|
||||
|
||||
input --> markdown --> bleach clean --> linkify --> output
|
||||
|
||||
Internally, the markdown library does:
|
||||
|
||||
source --> parse --> (tree|inline)processors --> serializing --> postprocessors
|
||||
|
||||
All escaped characters such as \. will be turned to something like <STX>46<ETX> in the processors
|
||||
step and then will be converted to . back again in the last tree processor, before serialization.
|
||||
Therefore, linkify does not see the escaped character anymore. This is annoying for the one case
|
||||
where you want to type "rich_text.py" and *not* have it turned into a link, since you can't type
|
||||
"rich_text\.py" either.
|
||||
|
||||
A simple solution would be to run linkify before markdown, but that may cause other issues when
|
||||
linkify messes with the markdown syntax and it makes handling our attributes etc. harder.
|
||||
|
||||
So we do a weird hack where we modify the unescape processor to unescape everything EXCEPT for the
|
||||
dot and then unescape that one manually after linkify. However, to make things even harder, the bleach
|
||||
clean step removes any invisible characters, so we need to cheat a bit more.
|
||||
"""
|
||||
|
||||
def __init__(self, linker, tags, attributes, protocols, strip):
|
||||
self.linker = linker
|
||||
self.tags = tags
|
||||
self.attributes = attributes
|
||||
self.protocols = protocols
|
||||
self.strip = strip
|
||||
super().__init__()
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
md.treeprocessors.deregister('unescape')
|
||||
md.treeprocessors.register(
|
||||
CustomUnescapeTreeprocessor(md),
|
||||
'unescape',
|
||||
0
|
||||
)
|
||||
md.postprocessors.register(
|
||||
CleanPostprocessor(self.tags, self.attributes, self.protocols, self.strip),
|
||||
'clean',
|
||||
2
|
||||
)
|
||||
md.postprocessors.register(
|
||||
LinkifyPostprocessor(self.linker),
|
||||
'linkify',
|
||||
1
|
||||
)
|
||||
md.postprocessors.register(
|
||||
CustomUnescapePostprocessor(self.linker),
|
||||
'unescape_dot',
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
def markdown_compile_email(source):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
@@ -175,18 +299,20 @@ def markdown_compile_email(source):
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
return linker.linkify(bleach.clean(
|
||||
markdown.markdown(
|
||||
source,
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
# 'markdown.extensions.nl2br' # disabled for backwards-compatibility
|
||||
]
|
||||
),
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
))
|
||||
return markdown.markdown(
|
||||
source,
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
EmailNl2BrExtension(),
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
strip=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SnippetExtension(markdown.extensions.Extension):
|
||||
@@ -196,23 +322,24 @@ class SnippetExtension(markdown.extensions.Extension):
|
||||
md.parser.blockprocessors.deregister('quote')
|
||||
|
||||
|
||||
def markdown_compile(source, snippet=False):
|
||||
def markdown_compile(source, linker, snippet=False):
|
||||
tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
|
||||
exts = [
|
||||
'markdown.extensions.sane_lists',
|
||||
'markdown.extensions.nl2br'
|
||||
'markdown.extensions.nl2br',
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
tags=tags,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
strip=snippet,
|
||||
)
|
||||
]
|
||||
if snippet:
|
||||
exts.append(SnippetExtension())
|
||||
return bleach.clean(
|
||||
markdown.markdown(
|
||||
source,
|
||||
extensions=exts
|
||||
),
|
||||
strip=snippet,
|
||||
tags=tags,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
return markdown.markdown(
|
||||
source,
|
||||
extensions=exts
|
||||
)
|
||||
|
||||
|
||||
@@ -228,7 +355,7 @@ def rich_text(text: str, **kwargs):
|
||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||
parse_email=True
|
||||
)
|
||||
body_md = linker.linkify(markdown_compile(text))
|
||||
body_md = markdown_compile(text, linker)
|
||||
return mark_safe(body_md)
|
||||
|
||||
|
||||
@@ -244,5 +371,5 @@ def rich_text_snippet(text: str, **kwargs):
|
||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||
parse_email=True
|
||||
)
|
||||
body_md = linker.linkify(markdown_compile(text, snippet=True))
|
||||
body_md = markdown_compile(text, linker, snippet=True)
|
||||
return mark_safe(body_md)
|
||||
|
||||
@@ -115,7 +115,8 @@ class AsyncMixin:
|
||||
elif state == 'PROGRESS':
|
||||
data.update({
|
||||
'started': True,
|
||||
'percentage': info.get('value', 0) if isinstance(info, dict) else 0
|
||||
'percentage': info.get('value', 0) if isinstance(info, dict) else 0,
|
||||
'steps': info.get('steps', []) if isinstance(info, dict) else None,
|
||||
})
|
||||
elif state == 'STARTED':
|
||||
data.update({
|
||||
|
||||
@@ -31,7 +31,8 @@ from django_scopes.forms import (
|
||||
)
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models.checkin import Checkin, CheckinList
|
||||
from pretix.control.forms import ItemMultipleChoiceField
|
||||
from pretix.control.forms.widgets import Select2
|
||||
|
||||
@@ -177,3 +178,26 @@ class SimpleCheckinListForm(forms.ModelForm):
|
||||
'subevent': SafeModelChoiceField,
|
||||
'gates': SafeModelMultipleChoiceField,
|
||||
}
|
||||
|
||||
|
||||
class CheckinListSimulatorForm(forms.Form):
|
||||
raw_barcode = forms.CharField(
|
||||
label=_("Barcode"),
|
||||
)
|
||||
datetime = forms.SplitDateTimeField(
|
||||
label=_("Check-in time"),
|
||||
widget=SplitDateTimePickerWidget(),
|
||||
)
|
||||
checkin_type = forms.ChoiceField(
|
||||
label=_("Check-in type"),
|
||||
choices=Checkin.CHECKIN_TYPES,
|
||||
)
|
||||
ignore_unpaid = forms.BooleanField(
|
||||
label=_("Allow check-in of unpaid order (if check-in list permits it)"),
|
||||
required=False,
|
||||
)
|
||||
questions_supported = forms.BooleanField(
|
||||
label=_("Support for check-in questions"),
|
||||
initial=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
@@ -749,6 +749,7 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'payment_term_minutes',
|
||||
'payment_term_last',
|
||||
'payment_term_expire_automatically',
|
||||
'payment_term_expire_delay_days',
|
||||
'payment_term_accept_late',
|
||||
'payment_pending_hidden',
|
||||
'payment_explanation',
|
||||
@@ -1262,8 +1263,12 @@ class MailSettingsForm(SettingsForm):
|
||||
'mail_subject_order_placed_require_approval': ['event', 'order'],
|
||||
'mail_text_order_approved': ['event', 'order'],
|
||||
'mail_subject_order_approved': ['event', 'order'],
|
||||
'mail_text_order_approved_attendee': ['event', 'order'],
|
||||
'mail_subject_order_approved_attendee': ['event', 'order'],
|
||||
'mail_text_order_approved_free': ['event', 'order'],
|
||||
'mail_subject_order_approved_free': ['event', 'order'],
|
||||
'mail_text_order_approved_free_attendee': ['event', 'order'],
|
||||
'mail_subject_order_approved_free_attendee': ['event', 'order'],
|
||||
'mail_text_order_denied': ['event', 'order', 'comment'],
|
||||
'mail_subject_order_denied': ['event', 'order', 'comment'],
|
||||
'mail_text_order_paid': ['event', 'order', 'payment_info'],
|
||||
@@ -1396,7 +1401,7 @@ class CommentForm(I18nModelForm):
|
||||
fields = ['comment']
|
||||
widgets = {
|
||||
'comment': forms.Textarea(attrs={
|
||||
'rows': 3,
|
||||
'rows': 6,
|
||||
'class': 'helper-width-100',
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -573,6 +573,11 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
label=_('Sales channel'),
|
||||
required=False,
|
||||
)
|
||||
has_checkin = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=FilterNullBooleanSelect,
|
||||
label=_('At least one ticket with check-in'),
|
||||
)
|
||||
checkin_attention = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=FilterNullBooleanSelect,
|
||||
@@ -745,6 +750,12 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
qs = qs.filter(
|
||||
all_positions__country=fdata.get('attendee_address_country')
|
||||
).distinct()
|
||||
if fdata.get('has_checkin') is not None:
|
||||
qs = qs.annotate(
|
||||
has_checkin=Exists(
|
||||
Checkin.all.filter(position__order_id=OuterRef('pk'))
|
||||
)
|
||||
).filter(has_checkin=fdata['has_checkin'])
|
||||
if fdata.get('ticket_secret'):
|
||||
qs = qs.filter(
|
||||
all_positions__secret__icontains=fdata.get('ticket_secret')
|
||||
@@ -1743,9 +1754,9 @@ class CheckinListAttendeeFilterForm(FilterForm):
|
||||
label=_('Check-in status'),
|
||||
choices=(
|
||||
('', _('All attendees')),
|
||||
('3', pgettext_lazy('checkin state', 'Checked in but left')),
|
||||
('2', pgettext_lazy('checkin state', 'Present')),
|
||||
('1', _('Checked in')),
|
||||
('2', pgettext_lazy('checkin state', 'Present')),
|
||||
('3', pgettext_lazy('checkin state', 'Checked in but left')),
|
||||
('0', _('Not checked in')),
|
||||
),
|
||||
required=False,
|
||||
|
||||
@@ -895,7 +895,7 @@ class EventCancelForm(forms.Form):
|
||||
'Hello,\n\n'
|
||||
'with this email, we regret to inform you that {event} has been canceled.\n\n'
|
||||
'We will refund you {refund_amount} to your original payment method.\n\n'
|
||||
'You can view the current state of your order here:\n\n{url}\n\nBest regards,\n\n'
|
||||
'You can view the current state of your order here:\n\n{url}\n\nBest regards, \n\n'
|
||||
'Your {event} team'
|
||||
))
|
||||
)
|
||||
@@ -922,7 +922,7 @@ class EventCancelForm(forms.Form):
|
||||
'Hello,\n\n'
|
||||
'with this email, we regret to inform you that {event} has been canceled.\n\n'
|
||||
'You will therefore not receive a ticket from the waiting list.\n\n'
|
||||
'Best regards,\n\n'
|
||||
'Best regards, \n\n'
|
||||
'Your {event} team'
|
||||
))
|
||||
)
|
||||
|
||||
@@ -65,8 +65,8 @@ from pretix.base.forms.questions import (
|
||||
)
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import (
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
|
||||
MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.organizer import OrganizerFooterLink
|
||||
@@ -637,7 +637,11 @@ class GiftCardCreateForm(forms.ModelForm):
|
||||
if GiftCard.objects.filter(
|
||||
secret__iexact=s
|
||||
).filter(
|
||||
Q(issuer=self.organizer) | Q(issuer__gift_card_collector_acceptance__collector=self.organizer)
|
||||
Q(issuer=self.organizer) |
|
||||
Q(issuer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=self.organizer,
|
||||
active=True,
|
||||
).values_list('issuer', flat=True))
|
||||
).exists():
|
||||
raise ValidationError(
|
||||
_('A gift card with the same secret already exists in your or an affiliated organizer account.')
|
||||
@@ -790,6 +794,7 @@ class ReusableMediumCreateForm(ReusableMediumUpdateForm):
|
||||
|
||||
class CustomerUpdateForm(forms.ModelForm):
|
||||
error_messages = {
|
||||
'duplicate_identifier': _("An account with this customer ID is already registered."),
|
||||
'duplicate': _("An account with this email address is already registered."),
|
||||
}
|
||||
|
||||
@@ -824,6 +829,7 @@ class CustomerUpdateForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
identifier = self.cleaned_data.get('identifier')
|
||||
|
||||
if email is not None:
|
||||
try:
|
||||
@@ -836,6 +842,17 @@ class CustomerUpdateForm(forms.ModelForm):
|
||||
code='duplicate',
|
||||
)
|
||||
|
||||
if identifier is not None:
|
||||
try:
|
||||
self.instance.organizer.customers.exclude(pk=self.instance.pk).get(identifier=identifier)
|
||||
except Customer.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['duplicate_identifier'],
|
||||
code='duplicate_identifier',
|
||||
)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
@@ -1013,3 +1030,32 @@ class SSOClientForm(I18nModelForm):
|
||||
else:
|
||||
del self.fields['client_id']
|
||||
del self.fields['regenerate_client_secret']
|
||||
|
||||
|
||||
class GiftCardAcceptanceInviteForm(forms.Form):
|
||||
acceptor = forms.CharField(
|
||||
label=_("Organizer short name"),
|
||||
required=True,
|
||||
)
|
||||
reusable_media = forms.BooleanField(
|
||||
label=_("Allow access to reusable media"),
|
||||
help_text=_("This is required if you want the other organizer to participate in a shared system with e.g. "
|
||||
"NFC payment chips. You should only use this option for organizers you trust, since (depending "
|
||||
"on the activated medium types) this will grant the other organizer access to cryptographic key "
|
||||
"material required to interact with the media type."),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_acceptor(self):
|
||||
val = self.cleaned_data['acceptor']
|
||||
try:
|
||||
acceptor = Organizer.objects.exclude(pk=self.organizer.pk).get(slug=val)
|
||||
except Organizer.DoesNotExist:
|
||||
raise ValidationError(_('The selected organizer does not exist or cannot be invited.'))
|
||||
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
|
||||
raise ValidationError(_('The selected organizer has already been invited.'))
|
||||
return acceptor
|
||||
|
||||
@@ -271,7 +271,7 @@ class VoucherBulkForm(VoucherForm):
|
||||
required=False,
|
||||
initial=_('Hello,\n\n'
|
||||
'with this email, we\'re sending you one or more vouchers for {event}:\n\n{voucher_list}\n\n'
|
||||
'You can redeem them here in our ticket shop:\n\n{url}\n\nBest regards,\n\n'
|
||||
'You can redeem them here in our ticket shop:\n\n{url}\n\nBest regards, \n'
|
||||
'Your {event} team')
|
||||
)
|
||||
send_recipients = forms.CharField(
|
||||
|
||||
@@ -340,6 +340,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
|
||||
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
|
||||
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
|
||||
'pretix.giftcards.acceptance.issuer.removed': _('A gift card issuer has been removed or declined.'),
|
||||
'pretix.giftcards.acceptance.issuer.accepted': _('A new gift card issuer has been accepted.'),
|
||||
'pretix.webhook.created': _('The webhook has been created.'),
|
||||
'pretix.webhook.changed': _('The webhook has been changed.'),
|
||||
'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'),
|
||||
@@ -369,6 +372,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
|
||||
'pretix.event.canceled': _('The event has been canceled.'),
|
||||
'pretix.event.deleted': _('An event has been deleted.'),
|
||||
'pretix.event.shredder.started': _('A removal process for personal data has been started.'),
|
||||
'pretix.event.shredder.completed': _('A removal process for personal data has been completed.'),
|
||||
'pretix.event.order.modified': _('The order details have been changed.'),
|
||||
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
|
||||
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
|
||||
@@ -528,9 +533,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
|
||||
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
|
||||
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'), # legacy
|
||||
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.transferred': _('An entry has been transferred to another waiting list.'),
|
||||
'pretix.event.order.waitinglist.transferred': _('An entry has been transferred to another waiting list.'), # legacy
|
||||
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
|
||||
'pretix.team.created': _('The team has been created.'),
|
||||
|
||||
@@ -519,13 +519,32 @@ def get_organizer_navigation(request):
|
||||
})
|
||||
|
||||
if 'can_manage_gift_cards' in request.orgapermset:
|
||||
children = []
|
||||
children.append({
|
||||
'label': _('Gift cards'),
|
||||
'url': reverse('control:organizer.giftcards', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.giftcard' in url.url_name and 'acceptance' not in url.url_name,
|
||||
'children': children,
|
||||
})
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
children.append(
|
||||
{
|
||||
'label': _('Acceptance'),
|
||||
'url': reverse('control:organizer.giftcards.acceptance', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.giftcards.acceptance' in url.url_name,
|
||||
}
|
||||
)
|
||||
nav.append({
|
||||
'label': _('Gift cards'),
|
||||
'url': reverse('control:organizer.giftcards', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.giftcard' in url.url_name,
|
||||
'icon': 'credit-card',
|
||||
'children': children,
|
||||
})
|
||||
|
||||
if request.organizer.settings.customer_accounts:
|
||||
|
||||
@@ -470,6 +470,8 @@
|
||||
<div class="progress-bar progress-bar-success">
|
||||
</div>
|
||||
</div>
|
||||
<div class="steps">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,12 +16,17 @@
|
||||
{% trans "Edit list configuration" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}"
|
||||
<a href="{% url "control:event.orders.checkinlists.simulator" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-flask"></span>
|
||||
{% trans "Check-in simulator" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlistpdf&checkinlistpdf-list={{ checkinlist.pk }}{% if "status" in request.GET %}&checkinlistpdf-status={{ request.GET.status|urlencode }}{% endif %}"
|
||||
class="btn btn-default" target="_blank">
|
||||
<span class="fa fa-download"></span>
|
||||
{% trans "PDF" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlist&checkinlist-list={{ checkinlist.pk }}"
|
||||
<a href="{% url "control:event.orders.export" event=request.event.slug organizer=request.event.organizer.slug %}?identifier=checkinlist&checkinlist-list={{ checkinlist.pk }}{% if "status" in request.GET %}&checkinlist-status={{ request.GET.status|urlencode }}{% endif %}"
|
||||
class="btn btn-default" target="_blank">
|
||||
<span class="fa fa-download"></span>
|
||||
{% trans "CSV" %}
|
||||
|
||||
@@ -12,7 +12,15 @@
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
{% if checkinlist %}
|
||||
<h1>{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}</h1>
|
||||
<h1>
|
||||
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||
<a href="{% url "control:event.orders.checkinlists.simulator" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||
target="_blank"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-flask"></span>
|
||||
{% trans "Check-in simulator" %}
|
||||
</a>
|
||||
</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Check-in list" %}</h1>
|
||||
{% endif %}
|
||||
|
||||
@@ -133,6 +133,9 @@
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% load getitem %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% block title %}{% trans "Check-in simulator" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-wrench"></span>
|
||||
{% trans "Edit list configuration" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<h2>{% trans "Check-in simulator" %}</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This tool allows you to validate your check-in configuration. You can enter a barcode plus some
|
||||
optional parameters and we will show you the response of the check-in list. No actual check-in will
|
||||
be performed and no modification to the system state is made.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.raw_barcode layout="control" %}
|
||||
{% bootstrap_field form.datetime layout="control" %}
|
||||
{% bootstrap_field form.checkin_type layout="control" %}
|
||||
{% bootstrap_field form.ignore_unpaid layout="control" %}
|
||||
{% bootstrap_field form.questions_supported layout="control" %}
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% trans "Simulate" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if result %}
|
||||
<hr>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{% trans "Result" %}</div>
|
||||
</div>
|
||||
<div class="panel-body checkin-sim-result checkin-sim-result-status-{{ result.status }} checkin-sim-result-reason-{{ result.reason }}">
|
||||
{% if result.status == "ok" %}
|
||||
<span class="fa fa-check-circle"></span>
|
||||
{% elif result.status == "incomplete" %}
|
||||
<span class="fa fa-question-circle"></span>
|
||||
{% elif result.status == "error" %}
|
||||
{% if result.reason == "already_redeemed" %}
|
||||
<span class="fa fa-warning"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-exclamation-circle"></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if result.status == "ok" %}
|
||||
<h3 class="nomargin-top">{% trans "Valid check-in" %}</h3>
|
||||
{% elif result.status == "incomplete" %}
|
||||
<h3 class="nomargin-top">{% trans "Additional information required" %}</h3>
|
||||
<p>
|
||||
{% trans "The following questions must be answered before check-in can be completed:" %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for q in result.questions %}
|
||||
<li>
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||
{{ q.question }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif result.status == "error" %}
|
||||
<h3 class="nomargin-top">{{ reason_labels|getitem:result.reason }}</h3>
|
||||
{% if result.reason_explanation %}
|
||||
<p>{{ result.reason_explanation }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if result.position %}
|
||||
{% if result.position.require_attention %}
|
||||
<p>
|
||||
<span class="fa fa-info-circle fa-fw"></span> {% trans "Special attention required" %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<span class="fa fa-ticket fa-fw"></span>
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=result.position.order %}">
|
||||
{{ result.position.order }}</a>-{{ result.position.positionid }}
|
||||
</p>
|
||||
{% if result.position.attendee_name %}
|
||||
<p>
|
||||
<span class="fa fa-user fa-fw"></span>
|
||||
{{ result.position.attendee_name }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if result.rule_graph %}
|
||||
<div id="rules-editor" class="form-inline">
|
||||
<div role="tabpanel" class="tab-pane" id="rules-viz">
|
||||
<checkin-rules-visualization></checkin-rules-visualization>
|
||||
</div>
|
||||
<textarea id="id_rules" class="sr-only">{{ result.rule_graph|attr_escapejson_dumps }}</textarea>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if DEBUG %}
|
||||
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
|
||||
{% else %}
|
||||
<script type="text/javascript" src="{% static "vuejs/vue.min.js" %}"></script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "d3/d3.v6.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-color.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-dispatch.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-ease.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-interpolate.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-selection.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-timer.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-transition.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script>
|
||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/viz-node.vue' %}"></script>
|
||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/checkin-rules-visualization.vue' %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,7 @@ If that was you, please enter the following confirmation code:
|
||||
|
||||
If this was not requested by you, you can safely ignore this email.
|
||||
|
||||
Best regards,
|
||||
Best regards,
|
||||
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
@@ -4,6 +4,6 @@ you requested a new password. Please go to the following page to reset your pass
|
||||
|
||||
{{ url }}
|
||||
|
||||
Best regards,
|
||||
Best regards,
|
||||
Your pretix team
|
||||
{% endblocktrans %}
|
||||
@@ -11,7 +11,7 @@ If you want to join that team, just click on the following link:
|
||||
|
||||
If you do not want to join, you can safely ignore or delete this email.
|
||||
|
||||
Best regards,
|
||||
Best regards,
|
||||
|
||||
Your pretix team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -11,6 +11,6 @@ You can review and change your account settings here:
|
||||
|
||||
{{ url }}
|
||||
|
||||
Best regards,
|
||||
Best regards,
|
||||
Your pretix team
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -65,6 +65,8 @@
|
||||
{% bootstrap_field form.payment_term_minutes layout="control" %}
|
||||
{% bootstrap_field form.payment_term_last layout="control" %}
|
||||
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
|
||||
{% trans "days" context "unit" as days %}
|
||||
{% bootstrap_field form.payment_term_expire_delay_days layout="control" addon_after=days %}
|
||||
{% bootstrap_field form.payment_term_accept_late layout="control" %}
|
||||
{% bootstrap_field form.payment_pending_hidden layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
|
||||
{{ gc.secret }}
|
||||
</a>
|
||||
{% if gc.issuer != request.organizer %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<span class="fa fa-group"></span> {{ gc.issuer }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Issuer" %}</dt>
|
||||
<dd>{{ gc.issuer }}</dd>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Invite organizer" %}
|
||||
</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,150 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Gift cards acceptance" %}
|
||||
</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This feature allows you to configure acceptance of gift cards across multiple organizer accounts.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<h2>
|
||||
{% trans "Other organizers you accept gift cards from" %}
|
||||
</h2>
|
||||
|
||||
{% if issuer_acceptance|length == 0 and not filter_form.filtered %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You are not accepting gift cards from other organizers yet. If you want to do so, the other
|
||||
organizer can add you to their list and afterwards, you can confirm this here.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Organizer" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Reusable media" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for gca in issuer_acceptance %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ gca.issuer.name }}<br><code>{{ gca.issuer.slug }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.active %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "invited" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.reusable_media %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "disabled" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if gca.active %}
|
||||
<button class="btn btn-danger" name="delete_issuer" value="{{ gca.issuer.slug }}">
|
||||
{% trans "Remove" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" name="accept_issuer" value="{{ gca.issuer.slug }}">
|
||||
{% trans "Accept" %}
|
||||
</button>
|
||||
<button class="btn btn-danger" name="delete_issuer" value="{{ gca.issuer.slug }}">
|
||||
{% trans "Decline" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<h2>
|
||||
{% trans "Other organizers accepting gift cards from you" %}
|
||||
</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can invite other organizers to accept your gift cards. After you have done so, they need to go
|
||||
to the same page in their account and accept your invitation. Note that other organizers will be able
|
||||
to add money to gift cards as well that you will need to collect form them. It is your responsibility
|
||||
to handle the exchange of money to offset the transactions between the two organizers.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can optionally control whether they can access your reusable media. This is required if you want
|
||||
them to participate in a shared system with e.g. NFC payment chips.
|
||||
{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
You should only use this option for organizers you trust, since (depending on the activated medium types)
|
||||
this will grant the other organizer access to cryptographic key material required to interact with
|
||||
the media type.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% url "control:organizer.giftcards.acceptance.invite" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
{% trans "Invite new organizer" %}
|
||||
</a>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Organizer" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Reusable media" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for gca in acceptor_acceptance %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ gca.acceptor.name }}<br><code>{{ gca.acceptor.slug }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.active %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "invited" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.reusable_media %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "disabled" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-danger" name="delete_acceptor" value="{{ gca.acceptor.slug }}">
|
||||
{% trans "Remove" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -99,44 +99,4 @@
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% if not is_paginated or page_obj.number == 1 %}
|
||||
<form action="" method="post" class="form-inline">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Accepted gift cards of other organizers" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you have access to multiple organizer accounts, you can configure that ticket shops in
|
||||
this account will also accept gift codes issued through a different organizer account, and
|
||||
vice versa.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for gca in request.organizer.gift_card_issuer_acceptance.all %}
|
||||
<li>
|
||||
<strong>{{ gca.issuer }}</strong>
|
||||
<button type="submit" name="del" value="{{ gca.issuer.slug }}" class="btn btn-xs btn-danger">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>
|
||||
<em>{% trans "You are currently not accepting gift cards from other organizers." %}</em>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if other_organizers %}
|
||||
<li>
|
||||
<select name="add" class="form-control input-sm">
|
||||
<option></option>
|
||||
{% for o in other_organizers %}
|
||||
<option value="{{ o.slug }}">{{ o }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" type="submit"><span class="fa fa-plus"></span></button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% trans "Data shredder" %}
|
||||
</h1>
|
||||
<form action="{% url "control:event.shredder.shred" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
method="post" class="form-horizontal" data-asynctask>
|
||||
method="post" class="form-horizontal" data-asynctask data-asynctask-long>
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% if download_on_shred %}
|
||||
@@ -55,6 +55,12 @@
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<input type="hidden" name="file" value="{{ file.pk }}">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Depending on the amount of data in your event, the following step may take a while to complete.
|
||||
We will inform you via email once it has been completed.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Continue" %}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<form action="{% url "control:event.shredder.export" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
method="post" class="form-horizontal" data-asynctask>
|
||||
method="post" class="form-horizontal" data-asynctask data-asynctask-long>
|
||||
<legend>{% trans "Data selection" %}</legend>
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="payment_accordion">
|
||||
|
||||
@@ -130,7 +130,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}">
|
||||
{{ s.name }}</a></strong>
|
||||
{{ s.name }}</a></strong><br>
|
||||
<small class="text-muted">
|
||||
#{{ s.pk }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ s.get_date_from_display }}<br>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{% if redeemed_in_carts %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed with number=redeemed_in_carts %}
|
||||
This voucher is currently used in {{ number }} cart sessions and there might not be free to use until the cart sessions
|
||||
This voucher is currently used in {{ number }} cart sessions and might not be free to use until the cart sessions
|
||||
expire.
|
||||
{% endblocktrans %}
|
||||
<p class="text-right">
|
||||
|
||||
@@ -176,6 +176,10 @@ urlpatterns = [
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/edit$', organizer.GiftCardUpdateView.as_view(),
|
||||
name='organizer.giftcard.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcards/acceptance$', organizer.GiftCardAcceptanceListView.as_view(),
|
||||
name='organizer.giftcards.acceptance'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcards/acceptance/invite$', organizer.GiftCardAcceptanceInviteView.as_view(),
|
||||
name='organizer.giftcards.acceptance.invite'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
|
||||
name='organizer.webhook.add'),
|
||||
@@ -429,6 +433,7 @@ urlpatterns = [
|
||||
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
|
||||
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
|
||||
re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
|
||||
re_path(r'^checkinlists/(?P<list>\d+)/simulator$', checkin.CheckInListSimulator.as_view(), name='event.orders.checkinlists.simulator'),
|
||||
re_path(r'^checkinlists/(?P<list>\d+)/bulk_action$', checkin.CheckInListBulkActionView.as_view(), name='event.orders.checkinlists.bulk_action'),
|
||||
re_path(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
|
||||
name='event.orders.checkinlists.edit'),
|
||||
|
||||
@@ -19,6 +19,19 @@
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, jasonwaiting@live.hk, pajowu
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import secrets
|
||||
from datetime import timezone
|
||||
|
||||
import dateutil.parser
|
||||
@@ -32,14 +45,21 @@ from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import is_aware, make_aware, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import FormView, ListView
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.api.views.checkin import _redeem_process
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.models import Checkin, Order, OrderPosition
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.services.checkin import (
|
||||
LazyRuleVars, _logic_annotate_for_graphic_explain,
|
||||
)
|
||||
from pretix.base.signals import checkin_created
|
||||
from pretix.base.views.tasks import AsyncPostView
|
||||
from pretix.control.forms.checkin import CheckinListForm
|
||||
from pretix.control.forms.checkin import (
|
||||
CheckinListForm, CheckinListSimulatorForm,
|
||||
)
|
||||
from pretix.control.forms.filter import (
|
||||
CheckinFilterForm, CheckinListAttendeeFilterForm,
|
||||
)
|
||||
@@ -48,18 +68,6 @@ from pretix.control.views import CreateView, PaginationMixin, UpdateView
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, jasonwaiting@live.hk, pajowu
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
class CheckInListQueryMixin:
|
||||
|
||||
@@ -469,3 +477,63 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
ctx = super().get_context_data()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
|
||||
class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
||||
template_name = 'pretixcontrol/checkin/simulator.html'
|
||||
permission = 'can_view_orders'
|
||||
form_class = CheckinListSimulatorForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||
self.result = None
|
||||
r = super().dispatch(request, *args, **kwargs)
|
||||
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
||||
return r
|
||||
|
||||
def get_initial(self):
|
||||
return {
|
||||
'datetime': now()
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
**kwargs,
|
||||
checkinlist=self.list,
|
||||
result=self.result,
|
||||
reason_labels=dict(Checkin.REASONS),
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
self.result = _redeem_process(
|
||||
checkinlists=[self.list],
|
||||
raw_barcode=form.cleaned_data["raw_barcode"],
|
||||
answers_data={},
|
||||
datetime=form.cleaned_data["datetime"],
|
||||
force=False,
|
||||
checkin_type=form.cleaned_data["checkin_type"],
|
||||
ignore_unpaid=form.cleaned_data["ignore_unpaid"],
|
||||
untrusted_input=True,
|
||||
user=self.request.user,
|
||||
auth=None,
|
||||
expand=[],
|
||||
nonce=secrets.token_hex(12),
|
||||
pdf_data=False,
|
||||
questions_supported=form.cleaned_data["questions_supported"],
|
||||
canceled_supported=False,
|
||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||
legacy_url_support=False,
|
||||
simulate=True,
|
||||
).data
|
||||
|
||||
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
|
||||
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"])
|
||||
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data)
|
||||
self.result["rule_graph"] = rule_graph
|
||||
|
||||
if self.result.get("questions"):
|
||||
for q in self.result["questions"]:
|
||||
q["question"] = LazyI18nString(q["question"])
|
||||
return self.get(self.request, self.args, self.kwargs)
|
||||
|
||||
@@ -62,7 +62,7 @@ from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
|
||||
from django.views.generic import FormView, ListView
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
@@ -1511,12 +1511,12 @@ class QuickSetupView(FormView):
|
||||
event=self.request.event,
|
||||
initial=[
|
||||
{
|
||||
'name': LazyI18nString.from_gettext(gettext('Regular ticket')),
|
||||
'name': LazyI18nString.from_gettext(gettext_noop('Regular ticket')),
|
||||
'default_price': Decimal('35.00'),
|
||||
'quota': 100,
|
||||
},
|
||||
{
|
||||
'name': LazyI18nString.from_gettext(gettext('Reduced ticket')),
|
||||
'name': LazyI18nString.from_gettext(gettext_noop('Reduced ticket')),
|
||||
'default_price': Decimal('29.00'),
|
||||
'quota': 50,
|
||||
},
|
||||
|
||||
@@ -1918,7 +1918,7 @@ class OrderContactChange(OrderView):
|
||||
'pretix.event.order.contact.changed',
|
||||
data={
|
||||
'old_email': old_email,
|
||||
'new_email': self.form.cleaned_data['email'],
|
||||
'new_email': self.form.cleaned_data.get('email'),
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
@@ -1930,7 +1930,7 @@ class OrderContactChange(OrderView):
|
||||
'pretix.event.order.phone.changed',
|
||||
data={
|
||||
'old_phone': old_phone,
|
||||
'new_phone': self.form.cleaned_data['phone'],
|
||||
'new_phone': self.form.cleaned_data.get('phone'),
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
@@ -2261,8 +2261,13 @@ class ExportMixin:
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||
raw_exporters = [
|
||||
ex for ex in raw_exporters
|
||||
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
return sorted(
|
||||
[response(self.request.event, self.request.organizer) for r, response in responses if response],
|
||||
raw_exporters,
|
||||
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
||||
)
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ from pretix.base.models import (
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
||||
from pretix.base.models.giftcards import (
|
||||
GiftCardTransaction, gen_giftcard_secret,
|
||||
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
|
||||
)
|
||||
from pretix.base.models.orders import CancellationRequest
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
@@ -96,12 +96,12 @@ from pretix.control.forms.filter import (
|
||||
from pretix.control.forms.orders import ExporterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
|
||||
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
|
||||
ReusableMediumUpdateForm, SSOClientForm, SSOProviderForm, TeamForm,
|
||||
WebHookForm,
|
||||
EventMetaPropertyForm, GateForm, GiftCardAcceptanceInviteForm,
|
||||
GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
|
||||
MembershipTypeForm, MembershipUpdateForm, OrganizerDeleteForm,
|
||||
OrganizerFooterLinkFormset, OrganizerForm, OrganizerSettingsForm,
|
||||
OrganizerUpdateForm, ReusableMediumCreateForm, ReusableMediumUpdateForm,
|
||||
SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
@@ -181,7 +181,8 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
return self.request.organizer
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.user.get_events_with_any_permission(self.request).select_related('organizer').prefetch_related(
|
||||
qs = self.request.user.get_events_with_any_permission(self.request).select_related(
|
||||
'organizer').prefetch_related(
|
||||
'organizer', '_settings_objects', 'organizer___settings_objects',
|
||||
'organizer__meta_properties',
|
||||
Prefetch(
|
||||
@@ -211,7 +212,8 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['meta_fields'] = [
|
||||
self.filter_form['meta_{}'.format(p.name)] for p in self.organizer.meta_properties.filter(filter_allowed=True)
|
||||
self.filter_form['meta_{}'.format(p.name)] for p in
|
||||
self.organizer.meta_properties.filter(filter_allowed=True)
|
||||
]
|
||||
return ctx
|
||||
|
||||
@@ -316,7 +318,8 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(MailSettingsForm.base_context[item]).items():
|
||||
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(
|
||||
MailSettingsForm.base_context[item]).items():
|
||||
if s.strip().startswith('*'):
|
||||
ctx[p] = s
|
||||
else:
|
||||
@@ -341,7 +344,8 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
||||
if idx in self.supported_locale:
|
||||
with language(self.supported_locale[idx], self.request.organizer.settings.region):
|
||||
if k.startswith('mail_subject_'):
|
||||
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
|
||||
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v),
|
||||
self.placeholders(preview_item))
|
||||
else:
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
format_map(v, self.placeholders(preview_item))
|
||||
@@ -392,10 +396,12 @@ class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
|
||||
)
|
||||
self.request.organizer.delete_sub_objects()
|
||||
self.request.organizer.delete()
|
||||
self.request.organizer = None
|
||||
messages.success(self.request, _('The organizer has been deleted.'))
|
||||
return redirect(self.get_success_url())
|
||||
except ProtectedError as e:
|
||||
err = gettext('The organizer could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
|
||||
err = gettext(
|
||||
'The organizer could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
|
||||
|
||||
# Unlike deleting events (which is done by regular backend users), this feature can only be used by sysadmins,
|
||||
# so we expose more technical / less polished information.
|
||||
@@ -507,7 +513,8 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
|
||||
@cached_property
|
||||
def footer_links_formset(self):
|
||||
return OrganizerFooterLinkFormset(self.request.POST if self.request.method == "POST" else None, organizer=self.object,
|
||||
return OrganizerFooterLinkFormset(self.request.POST if self.request.method == "POST" else None,
|
||||
organizer=self.object,
|
||||
prefix="footer-links", instance=self.object)
|
||||
|
||||
def save_footer_links_formset(self, obj):
|
||||
@@ -664,7 +671,7 @@ class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
def is_allowed(self) -> bool:
|
||||
return self.request.organizer.teams.exclude(pk=self.kwargs.get('team')).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists()
|
||||
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
@@ -748,7 +755,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
else:
|
||||
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists()
|
||||
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
|
||||
if not other_admin_teams and self.object.can_change_teams and self.object.members.count() == 1:
|
||||
messages.error(self.request, _('You cannot remove the last member from this team as no one would '
|
||||
'be left with the permission to change teams.'))
|
||||
@@ -1328,6 +1335,95 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
}))
|
||||
|
||||
|
||||
class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
||||
model = GiftCardAcceptance
|
||||
template_name = 'pretixcontrol/organizers/giftcard_acceptance_invite.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
form_class = GiftCardAcceptanceInviteForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
'organizer': self.request.organizer,
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
self.request.organizer.gift_card_acceptor_acceptance.get_or_create(
|
||||
acceptor=form.cleaned_data['acceptor'],
|
||||
reusable_media=form.cleaned_data['reusable_media'],
|
||||
active=False,
|
||||
)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.acceptor.invited',
|
||||
data={'acceptor': form.cleaned_data['acceptor'].slug,
|
||||
'reusable_media': form.cleaned_data['reusable_media']},
|
||||
user=self.request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected organizer has been invited.'))
|
||||
return redirect(
|
||||
reverse('control:organizer.giftcards.acceptance', kwargs={'organizer': self.request.organizer.slug}))
|
||||
|
||||
|
||||
class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = GiftCardAcceptance
|
||||
template_name = 'pretixcontrol/organizers/giftcard_acceptance_list.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'acceptor_acceptance'
|
||||
paginate_by = 50
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.gift_card_acceptor_acceptance.select_related(
|
||||
'acceptor'
|
||||
).order_by('acceptor__name', 'acceptor_id')
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['issuer_acceptance'] = self.request.organizer.gift_card_issuer_acceptance.select_related(
|
||||
'issuer'
|
||||
)
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "delete_acceptor" in request.POST:
|
||||
done = self.request.organizer.gift_card_acceptor_acceptance.filter(
|
||||
acceptor__slug=request.POST.get("delete_acceptor")
|
||||
).delete()
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.acceptor.removed',
|
||||
data={'acceptor': request.POST.get("delete_acceptor")},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been removed.'))
|
||||
elif "delete_issuer" in request.POST:
|
||||
done = self.request.organizer.gift_card_issuer_acceptance.filter(
|
||||
issuer__slug=request.POST.get("delete_issuer")
|
||||
).delete()
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.issuer.removed',
|
||||
data={'issuer': request.POST.get("delete_acceptor")},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been removed.'))
|
||||
if "accept_issuer" in request.POST:
|
||||
done = self.request.organizer.gift_card_issuer_acceptance.filter(
|
||||
issuer__slug=request.POST.get("accept_issuer")
|
||||
).update(active=True)
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.issuer.accepted',
|
||||
data={'issuer': request.POST.get("accept_issuer")},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been accepted.'))
|
||||
|
||||
return redirect(
|
||||
reverse('control:organizer.giftcards.acceptance', kwargs={'organizer': self.request.organizer.slug}))
|
||||
|
||||
|
||||
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = GiftCard
|
||||
template_name = 'pretixcontrol/organizers/giftcards.html'
|
||||
@@ -1346,39 +1442,6 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
return qs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "add" in request.POST:
|
||||
o = self.request.user.get_organizers_with_permission(
|
||||
'can_manage_gift_cards', self.request
|
||||
).exclude(pk=self.request.organizer.pk).filter(
|
||||
slug=request.POST.get("add")
|
||||
).first()
|
||||
if o:
|
||||
self.request.organizer.gift_card_issuer_acceptance.get_or_create(
|
||||
issuer=o
|
||||
)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.added',
|
||||
data={'issuer': o.slug},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected gift card issuer has been added.'))
|
||||
if "del" in request.POST:
|
||||
o = Organizer.objects.filter(
|
||||
slug=request.POST.get("del")
|
||||
).first()
|
||||
if o:
|
||||
self.request.organizer.gift_card_issuer_acceptance.filter(
|
||||
issuer=o
|
||||
).delete()
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.removed',
|
||||
data={'issuer': o.slug},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected gift card issuer has been removed.'))
|
||||
return redirect(reverse('control:organizer.giftcards', kwargs={'organizer': self.request.organizer.slug}))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
@@ -1621,7 +1684,8 @@ class ExportMixin:
|
||||
def exporters(self):
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
raw_exporters = [
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events, self.request.organizer)
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
|
||||
self.request.organizer)
|
||||
for r, response in responses
|
||||
if response
|
||||
]
|
||||
@@ -1629,12 +1693,14 @@ class ExportMixin:
|
||||
ex for ex in raw_exporters
|
||||
if (
|
||||
not isinstance(ex, OrganizerLevelExportMixin) or
|
||||
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
|
||||
)
|
||||
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission,
|
||||
self.request)
|
||||
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
return sorted(
|
||||
raw_exporters,
|
||||
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
||||
key=lambda ex: (
|
||||
0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -1691,7 +1757,8 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
|
||||
data = self.scheduled.export_form_data
|
||||
else:
|
||||
if not self.exporter.form.is_valid():
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
messages.error(self.request,
|
||||
_('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
data = self.exporter.form.cleaned_data
|
||||
|
||||
@@ -1764,7 +1831,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
else:
|
||||
initial = {}
|
||||
return RRuleForm(
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get(
|
||||
"schedule") == "save" else None,
|
||||
prefix="rrule",
|
||||
initial=initial
|
||||
)
|
||||
@@ -1779,7 +1847,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
if not self.scheduled:
|
||||
initial = {
|
||||
"mail_subject": gettext("Export: {title}").format(title=self.exporter.verbose_name),
|
||||
"mail_template": gettext("Hello,\n\nattached to this email, you can find a new scheduled report for {name}.").format(
|
||||
"mail_template": gettext(
|
||||
"Hello,\n\nattached to this email, you can find a new scheduled report for {name}.").format(
|
||||
name=str(self.request.organizer.name)
|
||||
),
|
||||
"schedule_rrule_time": time(4, 0, 0),
|
||||
@@ -1787,7 +1856,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
else:
|
||||
initial = {}
|
||||
return ScheduledOrganizerExportForm(
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get(
|
||||
"schedule") == "save" else None,
|
||||
prefix="schedule",
|
||||
instance=instance,
|
||||
initial=initial,
|
||||
@@ -1804,7 +1874,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
elif not self.exporter:
|
||||
for s in ctx['scheduled']:
|
||||
try:
|
||||
s.export_verbose_name = [e for e in self.exporters if e.identifier == s.export_identifier][0].verbose_name
|
||||
s.export_verbose_name = [e for e in self.exporters if e.identifier == s.export_identifier][
|
||||
0].verbose_name
|
||||
except IndexError:
|
||||
s.export_verbose_name = "?"
|
||||
return ctx
|
||||
@@ -2236,9 +2307,10 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
|
||||
'provider': self.object.pk
|
||||
})
|
||||
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return',
|
||||
kwargs={
|
||||
'provider': self.object.pk
|
||||
})
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self):
|
||||
|
||||
@@ -37,10 +37,11 @@ import logging
|
||||
from collections import OrderedDict
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
@@ -62,6 +63,16 @@ class ShredderMixin:
|
||||
sorted(self.request.event.get_data_shredders().items(), key=lambda s: s[1].verbose_name)
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
except ShredError as e:
|
||||
messages.error(request, str(e))
|
||||
return redirect(reverse('control:event.shredder.start', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}))
|
||||
|
||||
|
||||
class StartShredView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, TemplateView):
|
||||
permission = 'can_change_orders'
|
||||
@@ -148,10 +159,15 @@ class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
||||
return _('The selected data was deleted successfully.')
|
||||
|
||||
def get_error_url(self):
|
||||
return reverse('control:event.shredder.download', kwargs={
|
||||
if "file" in self.request.POST:
|
||||
return reverse('control:event.shredder.download', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'file': self.request.POST.get("file")
|
||||
})
|
||||
return reverse('control:event.shredder.start', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'file': self.request.POST.get("file")
|
||||
'organizer': self.request.event.organizer.slug
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -162,4 +178,5 @@ class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
||||
if request.event.slug != request.POST.get("slug"):
|
||||
return self.error(ShredError(_("The slug you entered was not correct.")))
|
||||
|
||||
return self.do(self.request.event.id, request.POST.get("file"), request.POST.get("confirm_code"))
|
||||
return self.do(self.request.event.id, request.POST.get("file"), request.POST.get("confirm_code"),
|
||||
self.request.user.pk, get_language())
|
||||
|
||||
@@ -189,6 +189,11 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
|
||||
)
|
||||
wle.priority = self.request.event.waitinglistentries.aggregate(m=Max('priority'))['m'] + 1
|
||||
wle.save(update_fields=['priority'])
|
||||
wle.log_action(
|
||||
'pretix.event.orders.waitinglist.changed',
|
||||
data={'priority': wle.priority},
|
||||
user=self.request.user,
|
||||
)
|
||||
messages.success(request, _('The waiting list entry has been moved to the top.'))
|
||||
return self._redirect_back()
|
||||
except WaitingListEntry.DoesNotExist:
|
||||
@@ -202,6 +207,11 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
|
||||
)
|
||||
wle.priority = self.request.event.waitinglistentries.aggregate(m=Min('priority'))['m'] - 1
|
||||
wle.save(update_fields=['priority'])
|
||||
wle.log_action(
|
||||
'pretix.event.orders.waitinglist.changed',
|
||||
data={'priority': wle.priority},
|
||||
user=self.request.user,
|
||||
)
|
||||
messages.success(request, _('The waiting list entry has been moved to the end of the list.'))
|
||||
return self._redirect_back()
|
||||
except WaitingListEntry.DoesNotExist:
|
||||
@@ -391,7 +401,7 @@ class EntryTransfer(EventPermissionRequiredMixin, UpdateView):
|
||||
messages.success(self.request, _('The waitinglist entry has been transferred.'))
|
||||
if form.has_changed():
|
||||
self.object.log_action(
|
||||
'pretix.event.order.waitinglist.transferred', user=self.request.user, data={
|
||||
'pretix.event.orders.waitinglist.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
|
||||
@@ -213,7 +213,7 @@ class Logic():
|
||||
|
||||
data = data or {}
|
||||
|
||||
operator = list(tests.keys())[0]
|
||||
operator = [k for k in tests.keys() if not k.startswith("__")][0]
|
||||
values = tests[operator]
|
||||
|
||||
# Easy syntax for unary operators, like {"var": "x"} instead of strict
|
||||
|
||||
@@ -25,6 +25,7 @@ import time
|
||||
from django.conf import settings
|
||||
from django.contrib.gis.geoip2 import GeoIP2
|
||||
from django.core.cache import cache
|
||||
from geoip2.errors import AddressNotFoundError
|
||||
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
@@ -50,7 +51,10 @@ def _get_country(request):
|
||||
if not _geoip:
|
||||
_geoip = GeoIP2()
|
||||
|
||||
res = _geoip.country(get_client_ip(request))
|
||||
try:
|
||||
res = _geoip.country(get_client_ip(request))
|
||||
except AddressNotFoundError:
|
||||
return None
|
||||
return res['country_code']
|
||||
|
||||
|
||||
|
||||
+2819
-2413
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-31 09:02+0000\n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -330,8 +330,8 @@ msgstr "حاليا بالداخل"
|
||||
msgid "close"
|
||||
msgstr "إغلاق"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:58
|
||||
#: pretix/static/pretixbase/js/asynctask.js:135
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -339,13 +339,13 @@ msgstr ""
|
||||
"تتم معالجة طلبك حاليا. قد يستغرق الأمر بضع دقائق بناء على حجم الفعالية التي "
|
||||
"اخترت."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:63
|
||||
#: pretix/static/pretixbase/js/asynctask.js:140
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr "طلبك قيد الانتظار وستتم معالجته قريبا."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:69
|
||||
#: pretix/static/pretixbase/js/asynctask.js:146
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -354,36 +354,36 @@ msgstr ""
|
||||
"وصل طلبك للخادم وننتظر تنفيذه. إذا استغرق الأمر أكثر من دقيقتين تواصل معنا "
|
||||
"أو عاود المحاولة مجددا."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:105
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixbase/js/asynctask.js:198
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "حدث خطأ من نوع {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:108
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr "لم نتمكن من الاتصال بالخادم، لكن سنواصل المحاولة، رمز آخر خطأ: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:160
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "استغرقت الطلب فترة طويلة، الرجاء المحاولة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:201
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"لا يمكننا الوصول إلى الخادم حاليا، حاول مرة أخرى من فضلك. رمز الخطأ : {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:215
|
||||
#: pretix/static/pretixbase/js/asynctask.js:230
|
||||
msgid "We are processing your request …"
|
||||
msgstr "جاري معالجة طلبك …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:223
|
||||
#: pretix/static/pretixbase/js/asynctask.js:238
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -392,7 +392,7 @@ msgstr ""
|
||||
"نعمل الآن على ارسال طلبك إلى الخادم، إذا أستغرقت العملية أكثر من دقيقة، يرجى "
|
||||
"التحقق من اتصالك بالإنترنت ثم أعد تحميل الصفحة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:285
|
||||
#: pretix/static/pretixbase/js/asynctask.js:301
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "أغلق الرسالة"
|
||||
@@ -454,47 +454,47 @@ msgstr ""
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:112
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr "جميع الشروط في الأسفل"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:113
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr "خيار واحد على الأقل"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:114
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||
msgid "Event start"
|
||||
msgstr "بداية الفعالية"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:115
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||
msgid "Event end"
|
||||
msgstr "نهاية الفعالية"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:116
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||
msgid "Event admission"
|
||||
msgstr "تسجيل الفعالية"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:117
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||
msgid "custom date and time"
|
||||
msgstr "تحديد التاريخ والوقت"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:124
|
||||
msgid "custom time"
|
||||
msgstr "الوقت المحدد"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:125
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr "القدرة على التحمل (الدقائق)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:126
|
||||
msgid "Add condition"
|
||||
msgstr "أضف شرطا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:127
|
||||
msgid "minutes"
|
||||
msgstr "الدقائق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:128
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
@@ -660,32 +660,32 @@ msgstr[3] "سيتم حجز العناصر لك في سلة التسوق لعدة
|
||||
msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقائق {num}."
|
||||
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:161
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:171
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "يحصل المنظم على %(currency) %(amount)"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:169
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:179
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "ستسترد %(currency)%(amount)"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:185
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:195
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:426
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:436
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:462
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:472
|
||||
msgid "required"
|
||||
msgstr "مطلوب"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:565
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:584
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:575
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:594
|
||||
msgid "Time zone:"
|
||||
msgstr "المنطقة الزمنية:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:575
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:585
|
||||
msgid "Your local time:"
|
||||
msgstr "التوقيت المحلي:"
|
||||
|
||||
|
||||
+2847
-2491
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-31 09:02+0000\n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -316,62 +316,62 @@ msgstr ""
|
||||
msgid "close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:58
|
||||
#: pretix/static/pretixbase/js/asynctask.js:135
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:63
|
||||
#: pretix/static/pretixbase/js/asynctask.js:140
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:69
|
||||
#: pretix/static/pretixbase/js/asynctask.js:146
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
"browser and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:105
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixbase/js/asynctask.js:198
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:108
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:160
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:201
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:215
|
||||
#: pretix/static/pretixbase/js/asynctask.js:230
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Estem processant la vostra sol·licitud …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:223
|
||||
#: pretix/static/pretixbase/js/asynctask.js:238
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
"page and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:285
|
||||
#: pretix/static/pretixbase/js/asynctask.js:301
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr ""
|
||||
@@ -433,47 +433,47 @@ msgstr ""
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:112
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:113
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:114
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||
msgid "Event start"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:115
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||
msgid "Event end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:116
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||
msgid "Event admission"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:117
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||
msgid "custom date and time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:124
|
||||
msgid "custom time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:125
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:126
|
||||
msgid "Add condition"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:127
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:128
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
@@ -629,34 +629,34 @@ msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||
msgstr[0] "El contingut de la cistella ja no el teniu reservat."
|
||||
msgstr[1] "El contingut de la cistella ja no el teniu reservat."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:161
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:171
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:169
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:179
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:185
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:195
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:426
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:436
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:462
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:472
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Cistella expirada"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:565
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:584
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:575
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:594
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:575
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:585
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2857
-2443
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-05-31 09:02+0000\n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-03-19 06:00+0000\n"
|
||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -316,8 +316,8 @@ msgstr "Aktuálně uvnitř"
|
||||
msgid "close"
|
||||
msgstr "zavřít"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:58
|
||||
#: pretix/static/pretixbase/js/asynctask.js:135
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -325,13 +325,13 @@ msgstr ""
|
||||
"Váš požadavek je právě zpracováván. V závislosti na velikosti vaší udalosti, "
|
||||
"to může trvat několik minut."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:63
|
||||
#: pretix/static/pretixbase/js/asynctask.js:140
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr "Váš požadavek byl vložem do fronty serveru a brzy bude zpracován."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:69
|
||||
#: pretix/static/pretixbase/js/asynctask.js:146
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -341,14 +341,14 @@ msgstr ""
|
||||
"Pokud to trvá více jak dvě minuty, prosím kontaktuje nás nebo se vraťte do "
|
||||
"vašeho prohlížeče a zkuste to znovu."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:105
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixbase/js/asynctask.js:198
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Vyskytla se chyba {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:108
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -356,12 +356,12 @@ msgstr ""
|
||||
"Momentálně nemůžeme kontaktovat server, ale stále se o to pokoušíme. "
|
||||
"Poslední chybový kód: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:160
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "Zpracování požadavku trvá příliš dlouho. Prosím zkuste to znovu."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:201
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -369,11 +369,11 @@ msgstr ""
|
||||
"Momentálně nemůžeme kontaktovat server. Prosím zkuste to znovu. Chybový kód: "
|
||||
"{code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:215
|
||||
#: pretix/static/pretixbase/js/asynctask.js:230
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Zpracováváme váš požadavek …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:223
|
||||
#: pretix/static/pretixbase/js/asynctask.js:238
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -383,7 +383,7 @@ msgstr ""
|
||||
"prosím zkontrolujte své internetové připojení a znovu načtěte stránku a "
|
||||
"zkuste to znovu."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:285
|
||||
#: pretix/static/pretixbase/js/asynctask.js:301
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Zavřít zprávu"
|
||||
@@ -445,47 +445,47 @@ msgstr "Minuty od předchozího vstupu (-1 pro první vstup)"
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr "Minuty od prvního vstupu (-1 pro první vstup)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:112
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr "Všechny následující podmínky (AND)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:113
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr "Alespoň jedna z následujících podmínek (OR)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:114
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||
msgid "Event start"
|
||||
msgstr "Začátek akce"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:115
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||
msgid "Event end"
|
||||
msgstr "Konec akce"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:116
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||
msgid "Event admission"
|
||||
msgstr "Vstup na akci"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:117
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||
msgid "custom date and time"
|
||||
msgstr "Pevný termín"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:118
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:124
|
||||
msgid "custom time"
|
||||
msgstr "Pevná doba"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:125
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr "Tolerance (v minutách)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:120
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:126
|
||||
msgid "Add condition"
|
||||
msgstr "Přidat podmínku"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:121
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:127
|
||||
msgid "minutes"
|
||||
msgstr "minuty"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:122
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:128
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplikát"
|
||||
|
||||
@@ -647,32 +647,32 @@ msgstr[1] ""
|
||||
msgstr[2] ""
|
||||
"Produkty v nákupním košíku jsou pro vás rezervovány na dalších {num} minut."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:161
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:171
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "Organizátor si ponechává %(currency)s %(amount)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:169
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:179
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "Dostanete %(currency)s %(amount)s zpět"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:185
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:195
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Zadejte částku, kterou si organizátor může ponechat."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:426
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:436
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:462
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:472
|
||||
msgid "required"
|
||||
msgstr "povinný"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:565
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:584
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:575
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:594
|
||||
msgid "Time zone:"
|
||||
msgstr "Časové pásmo:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:575
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:585
|
||||
msgid "Your local time:"
|
||||
msgstr "Místní čas:"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user