forked from CGM_Public/pretix_original
Compare commits
255
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57957626a6 | ||
|
|
ae5464d486 | ||
|
|
67fec8d1f6 | ||
|
|
95a081676b | ||
|
|
7228a6304d | ||
|
|
04b9134e36 | ||
|
|
2e0769bc41 | ||
|
|
4d2f854710 | ||
|
|
b9ac9496d2 | ||
|
|
a975f5dc50 | ||
|
|
4ea1f6284a | ||
|
|
a01d105829 | ||
|
|
b1bfa1acee | ||
|
|
0b4e99c2d8 | ||
|
|
0cdce7a9cd | ||
|
|
464f625301 | ||
|
|
0c1072503c | ||
|
|
9ead82839a | ||
|
|
c346e3a7f4 | ||
|
|
a26f219faf | ||
|
|
74fb8e7d0c | ||
|
|
b9dbeef1ef | ||
|
|
54079797d2 | ||
|
|
02a4ed4be2 | ||
|
|
7f7c95aedb | ||
|
|
47af20d417 | ||
|
|
91e69f793d | ||
|
|
43e24ff88c | ||
|
|
fa3f6def82 | ||
|
|
34469bc222 | ||
|
|
d0364300b5 | ||
|
|
55bc55cc53 | ||
|
|
0ee5511cca | ||
|
|
192699a2c2 | ||
|
|
b8255bc7a0 | ||
|
|
d7f0c14fdc | ||
|
|
3f9ba2f223 | ||
|
|
3f811cc020 | ||
|
|
03f3203a82 | ||
|
|
59901603c6 | ||
|
|
aefb38cdd7 | ||
|
|
aed3ccd2dd | ||
|
|
893d115948 | ||
|
|
8e87cf67c7 | ||
|
|
8972715252 | ||
|
|
1879e440a7 | ||
|
|
f819f0c316 | ||
|
|
a1db13b75e | ||
|
|
6087665775 | ||
|
|
a6f93b6cf0 | ||
|
|
b96374fcf6 | ||
|
|
eb2ad48089 | ||
|
|
64dac504ca | ||
|
|
cf15a08712 | ||
|
|
9197274528 | ||
|
|
d19176ab41 | ||
|
|
8d8abbd941 | ||
|
|
5142c62e6e | ||
|
|
7f7223fcdc | ||
|
|
cdde688964 | ||
|
|
233bcaf00e | ||
|
|
0a5f3e6dd5 | ||
|
|
446d24553e | ||
|
|
45c32bcb05 | ||
|
|
5a5090604a | ||
|
|
2b370bde6d | ||
|
|
024a223ec7 | ||
|
|
022f44ad00 | ||
|
|
a682eab18e | ||
|
|
6721762a3f | ||
|
|
ad443d0eb6 | ||
|
|
ececd3e572 | ||
|
|
ffc4a76b11 | ||
|
|
4beb0c2e30 | ||
|
|
48e161d2d4 | ||
|
|
dc1973f4ff | ||
|
|
a0b046d204 | ||
|
|
0032f83d93 | ||
|
|
f312200881 | ||
|
|
9946da57c2 | ||
|
|
11e04ea3f2 | ||
|
|
9cef63d641 | ||
|
|
cb833cc6da | ||
|
|
5320a69c27 | ||
|
|
510ca67107 | ||
|
|
13720e731e | ||
|
|
78cfbd6460 | ||
|
|
a65f94fa85 | ||
|
|
288f73b735 | ||
|
|
ad33785f4c | ||
|
|
bbc175d3d6 | ||
|
|
2876ff5549 | ||
|
|
ed9caa04fc | ||
|
|
83a8fcaa47 | ||
|
|
858a448db5 | ||
|
|
58b803539b | ||
|
|
6c92c5bacf | ||
|
|
f0089f20fb | ||
|
|
cb2d056afd | ||
|
|
afb115c9a2 | ||
|
|
bb92ffe4eb | ||
|
|
8da8e2f43d | ||
|
|
cab360bdb6 | ||
|
|
c6a2ae3783 | ||
|
|
26ec9dcf6c | ||
|
|
c0832098ef | ||
|
|
fa3ac69b8e | ||
|
|
17f1d571b0 | ||
|
|
a692940397 | ||
|
|
7f2ec51c64 | ||
|
|
aba59a391c | ||
|
|
a819b8bb71 | ||
|
|
8a3b18fbd2 | ||
|
|
dd444299f0 | ||
|
|
3ee5e9cfbc | ||
|
|
f660f35766 | ||
|
|
42e26738e5 | ||
|
|
7c43f115b2 | ||
|
|
f055a598ce | ||
|
|
9138464896 | ||
|
|
479f51a84c | ||
|
|
a3ac54d419 | ||
|
|
b2841e5c61 | ||
|
|
3009f50d51 | ||
|
|
ff3a49ab2a | ||
|
|
19f3fbc7e8 | ||
|
|
bb9b9ac9aa | ||
|
|
d7f6befb5b | ||
|
|
2287be2009 | ||
|
|
0480b6873d | ||
|
|
711f08c9e8 | ||
|
|
d18914fcca | ||
|
|
2411144262 | ||
|
|
2f02d35a52 | ||
|
|
71e82fda81 | ||
|
|
ca3802da90 | ||
|
|
2c68b9e895 | ||
|
|
01092498f4 | ||
|
|
fd841ed66d | ||
|
|
04cbccb536 | ||
|
|
b8ea93de1e | ||
|
|
c49f42301c | ||
|
|
2ae0a16e67 | ||
|
|
6b06fdf822 | ||
|
|
ea3f4e5f62 | ||
|
|
d71c23f7e0 | ||
|
|
5ed7b0032b | ||
|
|
a77f2d01a7 | ||
|
|
ca4f511cde | ||
|
|
83b1c2ea7e | ||
|
|
c91eb2e20d | ||
|
|
bfb480a288 | ||
|
|
22e2143623 | ||
|
|
9e61f7f978 | ||
|
|
092de9e3c4 | ||
|
|
f0822d3c27 | ||
|
|
6fc47ca3b6 | ||
|
|
3716a686f5 | ||
|
|
9c0c77958e | ||
|
|
6154a44274 | ||
|
|
d5aff10297 | ||
|
|
846e39a652 | ||
|
|
75d37b2a37 | ||
|
|
2a0c3da8c4 | ||
|
|
fb7f4d1160 | ||
|
|
5c8817f0c3 | ||
|
|
7663bf7994 | ||
|
|
ea8f74f8aa | ||
|
|
7b34701449 | ||
|
|
06f4bfea24 | ||
|
|
e2862a98a0 | ||
|
|
82f4feadc3 | ||
|
|
906222c7d3 | ||
|
|
7ce2089ca8 | ||
|
|
2133584ed2 | ||
|
|
c3c50d7205 | ||
|
|
4f7a41a4b2 | ||
|
|
93fd79ab33 | ||
|
|
59595c2f10 | ||
|
|
91c6d09f0b | ||
|
|
570a818129 | ||
|
|
d63e2ebe2d | ||
|
|
884c97d62a | ||
|
|
032e958a00 | ||
|
|
7b16dfefbc | ||
|
|
8a5b13dee9 | ||
|
|
d7dde8c23e | ||
|
|
1e2f93fbc5 | ||
|
|
493fc03686 | ||
|
|
8d6d885f6e | ||
|
|
2aa989c293 | ||
|
|
06b226f40f | ||
|
|
73038b0d97 | ||
|
|
4513e31f0d | ||
|
|
d34175114b | ||
|
|
b2c71b47ce | ||
|
|
a889abc52b | ||
|
|
8c01b2a469 | ||
|
|
720c7fd7bb | ||
|
|
6ae6eba4de | ||
|
|
a173e347ea | ||
|
|
94d13e4cdd | ||
|
|
e618441231 | ||
|
|
cd57f1f024 | ||
|
|
075b9c187f | ||
|
|
d9f46cb817 | ||
|
|
2892d16861 | ||
|
|
9128624d68 | ||
|
|
d2cf8f801d | ||
|
|
682d0f886d | ||
|
|
d2cbd41a19 | ||
|
|
828f4e3168 | ||
|
|
e691afdd34 | ||
|
|
add90b08ec | ||
|
|
4539c6523b | ||
|
|
3453818c16 | ||
|
|
e5725d6d33 | ||
|
|
0cda1aeaaf | ||
|
|
769c451bcb | ||
|
|
5b60928205 | ||
|
|
b05f3a449d | ||
|
|
3262f2ba84 | ||
|
|
fe9d8e58a1 | ||
|
|
6706dbb4db | ||
|
|
87632ff8c5 | ||
|
|
2278dbdc4a | ||
|
|
e4e6cc0fcc | ||
|
|
ea73977c37 | ||
|
|
4fb5c6bef0 | ||
|
|
95511b0330 | ||
|
|
3340599aec | ||
|
|
e9a52d07d1 | ||
|
|
0a00a35ab1 | ||
|
|
24d8dc6c76 | ||
|
|
a9d48eaafe | ||
|
|
bb959fa494 | ||
|
|
6126309429 | ||
|
|
29b49ca82f | ||
|
|
7b7b83cb3e | ||
|
|
6bb2b3425d | ||
|
|
182d30ffb7 | ||
|
|
844c291575 | ||
|
|
ecdb1a8e09 | ||
|
|
6c6e5d5af6 | ||
|
|
c33853173b | ||
|
|
5f90bf80b8 | ||
|
|
0ba246846b | ||
|
|
88ea04551e | ||
|
|
2bfd8e17b0 | ||
|
|
1379e5f723 | ||
|
|
9fcaa3d730 | ||
|
|
22d92025c9 | ||
|
|
2b84c8d7a7 | ||
|
|
25ba2f1145 | ||
|
|
ae8ff60964 |
@@ -5,7 +5,6 @@ on:
|
|||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'doc/**'
|
- 'doc/**'
|
||||||
- 'src/pretix/locale/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
17
|
||||||
@@ -10,6 +10,8 @@ recursive-include src/pretix/helpers/locale *
|
|||||||
recursive-include src/pretix/base/templates *
|
recursive-include src/pretix/base/templates *
|
||||||
recursive-include src/pretix/control/templates *
|
recursive-include src/pretix/control/templates *
|
||||||
recursive-include src/pretix/presale/templates *
|
recursive-include src/pretix/presale/templates *
|
||||||
|
recursive-include src/pretix/plugins/autocheckin/templates *
|
||||||
|
recursive-include src/pretix/plugins/autocheckin/static *
|
||||||
recursive-include src/pretix/plugins/banktransfer/templates *
|
recursive-include src/pretix/plugins/banktransfer/templates *
|
||||||
recursive-include src/pretix/plugins/banktransfer/static *
|
recursive-include src/pretix/plugins/banktransfer/static *
|
||||||
recursive-include src/pretix/plugins/manualpayment/templates *
|
recursive-include src/pretix/plugins/manualpayment/templates *
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Package dependencies
|
|||||||
To build and run pretix, you will need the following debian packages::
|
To build and run pretix, you will need the following debian packages::
|
||||||
|
|
||||||
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
||||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||||
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
||||||
|
|
||||||
Config file
|
Config file
|
||||||
|
|||||||
@@ -73,4 +73,11 @@ This release includes a migration that changes retroactively fills an `organizer
|
|||||||
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
||||||
longer than usual, so plan the update accordingly.
|
longer than usual, so plan the update accordingly.
|
||||||
|
|
||||||
|
Upgrade to 2024.7.0 or newer
|
||||||
|
"""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
This release includes a migration that changes how sales channels are referred on orders.
|
||||||
|
If you have a large database, the migration step of the upgrade might take significantly longer than usual, so plan
|
||||||
|
the update accordingly.
|
||||||
|
|
||||||
.. _blog: https://pretix.eu/about/en/blog/
|
.. _blog: https://pretix.eu/about/en/blog/
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
.. _rest-autocheckinrules:
|
||||||
|
|
||||||
|
Auto check-in rules
|
||||||
|
===================
|
||||||
|
|
||||||
|
This feature requires the bundled ``pretix.plugins.autocheckin`` plugin to be active for the event in order to work properly.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Auto check-in rules specify that tickets should under specific conditions automatically be considered checked in after
|
||||||
|
they have been purchased.
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the rule
|
||||||
|
list integer ID of the check-in list to check the ticket in on. If
|
||||||
|
``None``, the system will select all matching check-in lists.
|
||||||
|
mode string ``"placed"`` if the rule should be evaluated right after
|
||||||
|
an order has been created, ``"paid"`` if the rule should
|
||||||
|
be evaluated after the order has been fully paid.
|
||||||
|
all_sales_channels boolean If ``true`` (default), the rule applies to tickets sold on all sales channels.
|
||||||
|
limit_sales_channels list of strings List of sales channel identifiers the rule should apply to
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
all_products boolean If ``true`` (default), the rule affects all products and variations.
|
||||||
|
limit_products list of integers List of item IDs, if ``all_products`` is not set. If the
|
||||||
|
product listed here has variations, all variations will be matched.
|
||||||
|
limit_variations list of integers List of product variation IDs, if ``all_products`` is not set.
|
||||||
|
The parent product does not need to be part of ``limit_products``.
|
||||||
|
all_payment_methods boolean If ``true`` (default), the rule applies to tickets paid with all payment methods.
|
||||||
|
limit_payment_methods list of strings List of payment method identifiers the rule should apply to
|
||||||
|
if ``all_payment_methods`` is ``false``.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionadded:: 2024.7
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
|
||||||
|
|
||||||
|
Returns a list of all rules configured for an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Returns information on one rule, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param id: The ``id`` field of the rule to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
|
||||||
|
|
||||||
|
Create a new rule.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 166
|
||||||
|
|
||||||
|
{
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a rule for
|
||||||
|
:param event: The ``slug`` field of the event to create a rule for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The rule could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create rules.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Update a rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 34
|
||||||
|
|
||||||
|
{
|
||||||
|
"mode": "paid",
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: text/javascript
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"list": 12345,
|
||||||
|
"mode": "placed",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
|
"all_products": False,
|
||||||
|
"limit_products": [2, 3],
|
||||||
|
"limit_variations": [456],
|
||||||
|
"all_payment_methods": true,
|
||||||
|
"limit_payment_methods": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the rule to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The rule could not be modified due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
|
||||||
|
|
||||||
|
Delete a rule.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param id: The ``id`` field of the rule to delete
|
||||||
|
: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 rule cannot be deleted since it is currently in use.
|
||||||
@@ -40,6 +40,11 @@ answers list of objects Answers to user
|
|||||||
seat objects The assigned seat (or ``null``)
|
seat objects The assigned seat (or ``null``)
|
||||||
├ id integer Internal ID of the seat instance
|
├ id integer Internal ID of the seat instance
|
||||||
├ name string Human-readable seat name
|
├ name string Human-readable seat name
|
||||||
|
├ zone_name string Name of the zone the seat is in
|
||||||
|
├ row_name string Name/number of the row the seat is in
|
||||||
|
├ row_label string Additional label of the row (or ``null``)
|
||||||
|
├ seat_number string Number of the seat within the row
|
||||||
|
├ seat_label string Additional label of the seat (or ``null``)
|
||||||
└ seat_guid string Identifier of the seat within the seating plan
|
└ seat_guid string Identifier of the seat within the seating plan
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ position_count integer Number of ticke
|
|||||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
||||||
|
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
|
||||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ id integer Internal ID
|
|||||||
active boolean The discount will be ignored if this is ``false``
|
active boolean The discount will be ignored if this is ``false``
|
||||||
internal_name string A name for the rule used in the backend
|
internal_name string A name for the rule used in the backend
|
||||||
position integer An integer, used for sorting the rules which are applied in order
|
position integer An integer, used for sorting the rules which are applied in order
|
||||||
sales_channels list of strings Sales channels this discount is available on, such as
|
all_sales_channels boolean If ``true`` (default), the discount is available on all sales channels
|
||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
that support discounts.
|
||||||
|
limit_sales_channels list of strings List of sales channel identifiers the discount is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
available_from datetime The first date time at which this discount can be applied
|
available_from datetime The first date time at which this discount can be applied
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_until datetime The last date time at which this discount can be applied
|
available_until datetime The last date time at which this discount can be applied
|
||||||
@@ -95,6 +99,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -151,6 +157,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -193,6 +201,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -224,6 +234,8 @@ Endpoints
|
|||||||
"active": true,
|
"active": true,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -284,6 +296,8 @@ Endpoints
|
|||||||
"active": false,
|
"active": false,
|
||||||
"internal_name": "3 for 2",
|
"internal_name": "3 for 2",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
|||||||
@@ -49,8 +49,11 @@ item_meta_properties object Item-specific m
|
|||||||
valid_keys object Cryptographic keys for non-default signature schemes.
|
valid_keys object Cryptographic keys for non-default signature schemes.
|
||||||
For performance reason, value is omitted in lists and
|
For performance reason, value is omitted in lists and
|
||||||
only contained in detail views. Value can be cached.
|
only contained in detail views. Value can be cached.
|
||||||
sales_channels list A list of sales channels this event is available for
|
all_sales_channels boolean If ``true`` (default), the event is available on all sales channels.
|
||||||
sale on.
|
limit_sales_channels list of strings List of sales channel identifiers the event is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
public_url string The public, customer-facing URL of the event (read-only).
|
public_url string The public, customer-facing URL of the event (read-only).
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
@@ -131,11 +134,13 @@ Endpoints
|
|||||||
"pretix.plugins.paypal",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.ticketoutputpdf"
|
"pretix.plugins.ticketoutputpdf"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
"resellers"
|
"resellers"
|
||||||
],
|
],
|
||||||
|
"sales_channels": [],
|
||||||
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -225,6 +230,8 @@ Endpoints
|
|||||||
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -282,11 +289,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": true,
|
||||||
"web",
|
"limit_sales_channels": []
|
||||||
"pretixpos",
|
|
||||||
"resellers"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -322,6 +326,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -387,11 +393,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
"sales_channels": [
|
"all_sales_channels": true,
|
||||||
"web",
|
"limit_sales_channels": []
|
||||||
"pretixpos",
|
|
||||||
"resellers"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -427,6 +430,8 @@ Endpoints
|
|||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
"pretix.plugins.paypal"
|
"pretix.plugins.paypal"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
@@ -502,6 +507,8 @@ Endpoints
|
|||||||
"pretix.plugins.paypal",
|
"pretix.plugins.paypal",
|
||||||
"pretix.plugins.pretixdroid"
|
"pretix.plugins.pretixdroid"
|
||||||
],
|
],
|
||||||
|
"all_sales_channels": true,
|
||||||
|
"limit_sales_channels": [],
|
||||||
"sales_channels": [
|
"sales_channels": [
|
||||||
"web",
|
"web",
|
||||||
"pretixpos",
|
"pretixpos",
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ Endpoints
|
|||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string secret: Only show gift cards with the given secret.
|
:query string secret: Only show gift cards with the given secret.
|
||||||
|
:query string value: Only show gift cards with the given value.
|
||||||
|
:query boolean expired: Filter for gift cards that are (not) expired.
|
||||||
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ at :ref:`plugin-docs`.
|
|||||||
checkinlists
|
checkinlists
|
||||||
waitinglist
|
waitinglist
|
||||||
customers
|
customers
|
||||||
|
saleschannels
|
||||||
membershiptypes
|
membershiptypes
|
||||||
memberships
|
memberships
|
||||||
giftcards
|
giftcards
|
||||||
@@ -43,5 +44,7 @@ at :ref:`plugin-docs`.
|
|||||||
scheduled_exports
|
scheduled_exports
|
||||||
shredders
|
shredders
|
||||||
sendmail_rules
|
sendmail_rules
|
||||||
|
auto_checkin_rules
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
|
seats
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ List of all invoices
|
|||||||
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
|
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
|
||||||
``is_cancellation`` will be returned.
|
``is_cancellation`` will be returned.
|
||||||
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
|
:query string order: If set, only invoices belonging to the order with the given order code will be returned.
|
||||||
|
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
|
||||||
|
:query string number: If set, only invoices with the given invoice number will be returned.
|
||||||
|
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
|
||||||
:query string refers: If set, only invoices referring to the given invoice will be returned.
|
:query string refers: If set, only invoices referring to the given invoice will be returned.
|
||||||
:query string locale: If set, only invoices with the given locale will be returned.
|
:query string locale: If set, only invoices with the given locale will be returned.
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
|
||||||
|
|||||||
@@ -38,11 +38,14 @@ require_membership boolean If ``true``, bo
|
|||||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||||
be hidden from users without a valid membership.
|
be hidden from users without a valid membership.
|
||||||
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||||
sales_channels list of strings Sales channels this variation is available on, such as
|
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
The item-level list takes precedence, i.e. a sales
|
The item-level list takes precedence, i.e. a sales
|
||||||
channel needs to be on both lists for the item to be
|
channel needs to be on both lists for the variation to be
|
||||||
available.
|
available (unless ``all_sales_channels`` is used).
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
available_from datetime The first date time at which this variation can be bought
|
available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
@@ -111,6 +114,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -139,6 +144,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -157,6 +164,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string search: Filter the list by the value of the variation (substring search).
|
||||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||||
returned.
|
returned.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -202,6 +210,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -244,7 +254,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -277,6 +288,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -341,6 +354,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_hidden": false,
|
"require_membership_hidden": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
|
|||||||
@@ -46,8 +46,11 @@ personalized boolean ``true`` for
|
|||||||
position integer An integer, used for sorting
|
position integer An integer, used for sorting
|
||||||
picture file A product picture to be displayed in the shop
|
picture file A product picture to be displayed in the shop
|
||||||
(can be ``null``).
|
(can be ``null``).
|
||||||
sales_channels list of strings Sales channels this product is available on, such as
|
all_sales_channels boolean If ``true`` (default), the item is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
limit_sales_channels list of strings List of sales channel identifiers the item is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
|
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
available_from datetime The first date time at which this item can be bought
|
available_from datetime The first date time at which this item can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||||
@@ -157,11 +160,14 @@ variations list of objects A list with o
|
|||||||
be hidden from users without a valid membership.
|
be hidden from users without a valid membership.
|
||||||
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||||
Markdown syntax or can be ``null``.
|
Markdown syntax or can be ``null``.
|
||||||
├ sales_channels list of strings Sales channels this variation is available on, such as
|
├ all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
├ limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||||
|
if ``all_sales_channels`` is ``false``.
|
||||||
The item-level list takes precedence, i.e. a sales
|
The item-level list takes precedence, i.e. a sales
|
||||||
channel needs to be on both lists for the item to be
|
channel needs to be on both lists for the variation to be
|
||||||
available.
|
available (unless ``all_sales_channels`` is used).
|
||||||
|
├ sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||||
|
and ``limit_sales_channels`` instead.
|
||||||
├ available_from datetime The first date time at which this variation can be bought
|
├ available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
@@ -276,6 +282,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -340,6 +348,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -362,6 +372,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -380,6 +392,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string search: Filter the list by internal name or name of the item (substring search).
|
||||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||||
returned.
|
returned.
|
||||||
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
||||||
@@ -420,6 +433,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -485,6 +500,8 @@ Endpoints
|
|||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"description": null,
|
"description": null,
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -506,6 +523,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -545,7 +564,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
"category": null,
|
"category": null,
|
||||||
@@ -608,7 +628,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -630,7 +651,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
@@ -657,6 +679,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Standard ticket"},
|
"name": {"en": "Standard ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "23.00",
|
"default_price": "23.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -721,6 +745,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -743,6 +769,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -801,6 +829,8 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "Ticket"},
|
"name": {"en": "Ticket"},
|
||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"default_price": "25.00",
|
"default_price": "25.00",
|
||||||
"original_price": null,
|
"original_price": null,
|
||||||
@@ -865,6 +895,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
@@ -887,6 +919,8 @@ Endpoints
|
|||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_membership": false,
|
"require_membership": false,
|
||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
|
"all_sales_channels": false,
|
||||||
|
"limit_sales_channels": ["web"],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_from_mode": "hide",
|
"available_from_mode": "hide",
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ payment_date date **DEPRECATED AN
|
|||||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||||
total money (string) Total value of this order
|
total money (string) Total value of this order
|
||||||
comment string Internal comment on this order
|
comment string Internal comment on this order
|
||||||
|
api_meta object Meta data for that order. Only available through API, no guarantees
|
||||||
|
on the content structure. You can use this to save references to your system.
|
||||||
custom_followup_at date Internal date for a custom follow-up action
|
custom_followup_at date Internal date for a custom follow-up action
|
||||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||||
that this ticket requires special attention if a ticket
|
that this ticket requires special attention if a ticket
|
||||||
@@ -201,7 +203,8 @@ checkins list of objects List of **succe
|
|||||||
├ datetime datetime Time of check-in
|
├ datetime datetime Time of check-in
|
||||||
├ type string Type of scan (defaults to ``entry``)
|
├ type string Type of scan (defaults to ``entry``)
|
||||||
├ gate integer Internal ID of the gate. Can be ``null``.
|
├ gate integer Internal ID of the gate. Can be ``null``.
|
||||||
├ device integer Internal ID of the device. Can be ``null``.
|
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
||||||
|
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
|
||||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||||
downloads list of objects List of ticket download options
|
downloads list of objects List of ticket download options
|
||||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||||
@@ -215,6 +218,11 @@ answers list of objects Answers to user
|
|||||||
seat objects The assigned seat. Can be ``null``.
|
seat objects The assigned seat. Can be ``null``.
|
||||||
├ id integer Internal ID of the seat instance
|
├ id integer Internal ID of the seat instance
|
||||||
├ name string Human-readable seat name
|
├ name string Human-readable seat name
|
||||||
|
├ zone_name string Name of the zone the seat is in
|
||||||
|
├ row_name string Name/number of the row the seat is in
|
||||||
|
├ row_label string Additional label of the row (or ``null``)
|
||||||
|
├ seat_number string Number of the seat within the row
|
||||||
|
├ seat_label string Additional label of the seat (or ``null``)
|
||||||
└ seat_guid string Identifier of the seat within the seating plan
|
└ seat_guid string Identifier of the seat within the seating plan
|
||||||
pdf_data object Data object required for ticket PDF generation. By default,
|
pdf_data object Data object required for ticket PDF generation. By default,
|
||||||
this field is missing. It will be added only if you add the
|
this field is missing. It will be added only if you add the
|
||||||
@@ -455,10 +463,13 @@ List of all orders
|
|||||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||||
you will not notice it using this method.
|
you will not notice it using this method.
|
||||||
:query datetime created_since: Only return orders that have been created since the given date.
|
:query datetime created_since: Only return orders that have been created since the given date (inclusive).
|
||||||
|
:query datetime created_before: Only return orders that have been created before the given date (exclusive).
|
||||||
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
||||||
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
||||||
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
||||||
|
:query string sales_channel: Only return orders with the given sales channel identifier (e.g. ``"web"``).
|
||||||
|
:query string payment_provider: Only return orders that contain a payment using the given payment provider. Note that this also searches for partial incomplete, or failed payments within the order and is not useful to get a sum of payment amounts without further processing.
|
||||||
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
||||||
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -554,6 +565,7 @@ Fetching individual orders
|
|||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
|
"api_meta": {},
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"checkin_text": null,
|
"checkin_text": null,
|
||||||
@@ -734,6 +746,8 @@ Updating order fields
|
|||||||
|
|
||||||
* ``comment``
|
* ``comment``
|
||||||
|
|
||||||
|
* ``api_meta``
|
||||||
|
|
||||||
* ``custom_followup_at``
|
* ``custom_followup_at``
|
||||||
|
|
||||||
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
Sales channels
|
||||||
|
==============
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The sales channel resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
identifier string Internal ID of the sales channel. For sales channel types
|
||||||
|
that allow only one instance, this is the same as ``type``.
|
||||||
|
For sales channel types that allow multiple instances, this
|
||||||
|
is always prefixed with ``type.``.
|
||||||
|
label multi-lingual string Human-readable name of the sales channel
|
||||||
|
type string Type of the sales channel. Only channels with type ``api``
|
||||||
|
can currently be created through the API.
|
||||||
|
position integer Position for sorting lists of sales channels
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/
|
||||||
|
|
||||||
|
Returns a list of all sales channels within a given organizer.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"identifier": "web",
|
||||||
|
"name": {
|
||||||
|
"en": "Online shop"
|
||||||
|
},
|
||||||
|
"type": "web",
|
||||||
|
"position": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||||
|
|
||||||
|
Returns information on one sales channel, identified by its identifier.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "web",
|
||||||
|
"name": {
|
||||||
|
"en": "Online shop"
|
||||||
|
},
|
||||||
|
"type": "web",
|
||||||
|
"position": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param identifier: The ``identifier`` field of the sales channel to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/saleschannels/
|
||||||
|
|
||||||
|
Creates a sales channel
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "api.custom",
|
||||||
|
"name": {
|
||||||
|
"en": "Custom integration"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"position": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "api.custom",
|
||||||
|
"name": {
|
||||||
|
"en": "Custom integration"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"position": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a sales channel for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The sales channel could not be created due to invalid submitted data.
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||||
|
|
||||||
|
Update a sales channel. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||||
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
|
want to change.
|
||||||
|
|
||||||
|
You can change all fields of the resource except the ``identifier`` and ``type`` fields.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"position": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"identifier": "web",
|
||||||
|
"name": {
|
||||||
|
"en": "Online shop"
|
||||||
|
},
|
||||||
|
"type": "web",
|
||||||
|
"position": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param identifier: The ``identifier`` field of the sales channel to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The sales channel could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||||
|
|
||||||
|
Delete a sales channel. You can not delete sales channels which have already been used or which are integral parts
|
||||||
|
of the system.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/saleschannels/api.custom/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Vary: Accept
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param identifier: The ``identifier`` field of the sales channel to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the sales channel is currently in use.
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
.. _`rest-reusablemedia`:
|
||||||
|
|
||||||
|
Seats
|
||||||
|
=====
|
||||||
|
|
||||||
|
The seat resource represents the seats in a seating plan in a specific event or subevent.
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The seat resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of this seat
|
||||||
|
subevent integer Internal ID of the subevent this seat belongs to
|
||||||
|
zone_name string Name of the zone the seat is in
|
||||||
|
row_name string Name/number of the row the seat is in
|
||||||
|
row_label string Additional label of the row (or ``null``)
|
||||||
|
seat_number string Number of the seat within the row
|
||||||
|
seat_label string Additional label of the seat (or ``null``)
|
||||||
|
seat_guid string Identifier of the seat within the seating plan
|
||||||
|
product integer Internal ID of the product that is mapped to this seat
|
||||||
|
blocked boolean Whether this seat is blocked manually.
|
||||||
|
orderposition integer / object Internal ID of an order position reserving this seat.
|
||||||
|
cartposition integer / object Internal ID of a cart position reserving this seat.
|
||||||
|
voucher integer / object Internal ID of a voucher reserving this seat.
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/
|
||||||
|
|
||||||
|
Returns a list of all seats in the specified event or subevent. Depending on whether the event has subevents, the
|
||||||
|
according endpoint has to be used.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/seats/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 500,
|
||||||
|
"next": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/seats/?page=2",
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1633,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "1",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "b9746230-6f31-4f41-bbc9-d6b60bdb3342",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": false,
|
||||||
|
"orderposition": null,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": 51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1634,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "2",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": 4321,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1.
|
||||||
|
:query string zone_name: Only show seats with the given zone_name.
|
||||||
|
:query string row_name: Only show seats with the given row_name.
|
||||||
|
:query string row_label: Only show seats with the given row_label.
|
||||||
|
:query string seat_number: Only show seats with the given seat_number.
|
||||||
|
:query string seat_label: Only show seats with the given seat_label.
|
||||||
|
:query string seat_guid: Only show seats with the given seat_guid.
|
||||||
|
:query boolean blocked: Only show seats with the given blocked status.
|
||||||
|
:query boolean is_available: Only show seats that are (not) currently available.
|
||||||
|
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
|
||||||
|
shown as a nested value instead of just an ID. This requires permission to access that object.
|
||||||
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier, and won't include the `seat` attribute, as that would be redundant.
|
||||||
|
The parameter can be given multiple times.
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to fetch
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: Endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(subevent_id)/seats/(id)/
|
||||||
|
|
||||||
|
Returns information on one seat, identified by its ID.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/seats/1634/?expand=orderposition HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1634,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "2",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "1d29fe20-8e1e-4984-b0ee-2773b0d07e07",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": {
|
||||||
|
"id": 134,
|
||||||
|
"order": {
|
||||||
|
"code": "U0HW7",
|
||||||
|
"event": "sampleconf"
|
||||||
|
},
|
||||||
|
"positionid": 1,
|
||||||
|
"item": 104,
|
||||||
|
"variation": 59,
|
||||||
|
"price": "60.00",
|
||||||
|
"attendee_name": "",
|
||||||
|
"attendee_name_parts": {
|
||||||
|
"_scheme": "given_family"
|
||||||
|
},
|
||||||
|
"company": null,
|
||||||
|
"street": null,
|
||||||
|
"zipcode": null,
|
||||||
|
"city": null,
|
||||||
|
"country": null,
|
||||||
|
"state": null,
|
||||||
|
"discount": null,
|
||||||
|
"attendee_email": null,
|
||||||
|
"voucher": null,
|
||||||
|
"tax_rate": "0.00",
|
||||||
|
"tax_value": "0.00",
|
||||||
|
"secret": "4rfgp263jduratnsvwvy6cc6r6wnptbj",
|
||||||
|
"addon_to": null,
|
||||||
|
"subevent": null,
|
||||||
|
"checkins": [],
|
||||||
|
"downloads": [],
|
||||||
|
"answers": [],
|
||||||
|
"tax_rule": null,
|
||||||
|
"pseudonymization_id": "ZSNYSG3URZ",
|
||||||
|
"canceled": false,
|
||||||
|
"valid_from": null,
|
||||||
|
"valid_until": null,
|
||||||
|
"blocked": null,
|
||||||
|
"voucher_budget_use": null
|
||||||
|
},
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to fetch
|
||||||
|
:param id: The ``id`` field of the seat to fetch
|
||||||
|
:query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be
|
||||||
|
shown as a nested value instead of just an ID. This requires permission to access that object.
|
||||||
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier, and won't include the `seat` attribute, as that would be redundant.
|
||||||
|
The parameter can be given multiple times.
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/seats/(id)/
|
||||||
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/(id)/
|
||||||
|
|
||||||
|
Update a seat.
|
||||||
|
|
||||||
|
You can only change the ``blocked`` field.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/1636/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"blocked": true
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1636,
|
||||||
|
"subevent": null,
|
||||||
|
"zone_name": "Ground floor",
|
||||||
|
"row_name": "1",
|
||||||
|
"row_label": null,
|
||||||
|
"seat_number": "4",
|
||||||
|
"seat_label": null,
|
||||||
|
"seat_guid": "6c0e29e5-05d6-421f-99f3-afd01478ecad",
|
||||||
|
"product": 104,
|
||||||
|
"blocked": true,
|
||||||
|
"orderposition": null,
|
||||||
|
"cartposition": null,
|
||||||
|
"voucher": null
|
||||||
|
},
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to modify
|
||||||
|
:param id: The ``id`` field of the seat to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
Scheduled email rules
|
Scheduled email rules
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
This feature requires the bundled ``pretix.plugins.sendmail`` plugin to be active for the event in order to work properly.
|
||||||
|
|
||||||
Resource description
|
Resource description
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ send_to string Can be ``"order
|
|||||||
or ``"both"``.
|
or ``"both"``.
|
||||||
date. Otherwise it is relative to the event start date.
|
date. Otherwise it is relative to the event start date.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2023.7
|
.. versionchanged:: 2023.7
|
||||||
|
|
||||||
The ``include_pending`` field has been deprecated.
|
The ``include_pending`` field has been deprecated.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.order.modified``
|
* ``pretix.event.order.modified``
|
||||||
* ``pretix.event.order.contact.changed``
|
* ``pretix.event.order.contact.changed``
|
||||||
* ``pretix.event.order.changed.*``
|
* ``pretix.event.order.changed.*``
|
||||||
|
* ``pretix.event.order.deleted`` (can only occur for test mode orders)
|
||||||
* ``pretix.event.order.refund.created``
|
* ``pretix.event.order.refund.created``
|
||||||
* ``pretix.event.order.refund.created.externally``
|
* ``pretix.event.order.refund.created.externally``
|
||||||
* ``pretix.event.order.refund.requested``
|
* ``pretix.event.order.refund.requested``
|
||||||
@@ -115,6 +116,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query boolean enabled: Only show webhooks that are or are not enabled
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Core
|
|||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators, gift_card_transaction_display,
|
register_ticket_secret_generators, gift_card_transaction_display,
|
||||||
register_text_placeholders, register_mail_placeholders
|
register_text_placeholders, register_mail_placeholders
|
||||||
|
|
||||||
@@ -35,11 +35,11 @@ Frontend
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: order_info, order_info_top, order_meta_from_request
|
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ The project pretix is split into several components. The main components are:
|
|||||||
create and manage their events, items, orders and tickets.
|
create and manage their events, items, orders and tickets.
|
||||||
|
|
||||||
**presale**
|
**presale**
|
||||||
This is the ticket-shop itself, containing all of the parts visible to the
|
This is the ticket shop itself, containing all of the parts visible to the
|
||||||
end user. Also called "frontend" in parts of this documentation.
|
end user. Also called "frontend" in parts of this documentation.
|
||||||
|
|
||||||
**api**
|
**api**
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ It is a good idea to put this command into your git hook ``.git/hooks/pre-commit
|
|||||||
for example, to check for any errors in any staged files when committing::
|
for example, to check for any errors in any staged files when committing::
|
||||||
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cd $GIT_DIR/../src
|
|
||||||
export GIT_WORK_TREE=../
|
|
||||||
export GIT_DIR=../.git
|
|
||||||
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
source ../env/bin/activate # Adjust to however you activate your virtual environment
|
||||||
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
|
for file in $(git diff --cached --name-only | grep -E '\.py$' | grep -Ev "migrations|mt940\.py|pretix/settings\.py|make_testdata\.py|testutils/settings\.py|tests/settings\.py|pretix/base/models/__init__\.py|.*_pb2\.py")
|
||||||
do
|
do
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
GetYourGuide
|
||||||
|
============
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The GetYourGuide integration is currently in Beta. Please contact support@pretix.eu to enable the integration
|
||||||
|
for your pretix.eu organizer account.
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
Using third party aggregators, such als GetYourGuide, event organizers can sell tickets to their events not only on
|
||||||
|
their own ticket-shop but also on the aggregator's portal. While this service is not for free, it allows event
|
||||||
|
organizers to reacher a larger audience that would otherwise not have found their way into the organizers webshop.
|
||||||
|
|
||||||
|
Using pretix' integration with GetYourGuide, event organizers can profit from an additional sales and revenue channel,
|
||||||
|
while keeping the effort for setting up and maintaining multiple ticket shops to a minimum.
|
||||||
|
|
||||||
|
Preparing your organizer account
|
||||||
|
--------------------------------
|
||||||
|
The first step in enabling the GetYourGuide integration, is to setup a corresponding Sales Channel, which will be used
|
||||||
|
to properly attribute the sales generated. This needs to be done only once per organizer account.
|
||||||
|
|
||||||
|
To do so, log into the pretix backend, select ``Organizers`` from the navigation and then the organizer in question.
|
||||||
|
Extending the ``Settings``-menu, find the ``Sales channels`` configuration and click the ``Add a new channel`` button.
|
||||||
|
|
||||||
|
On the following page, you will be able to select ``GetYourGuide`` as the sales channel type and give it a custom name.
|
||||||
|
|
||||||
|
Preparing your event
|
||||||
|
--------------------
|
||||||
|
In order to now sell your events on GetYourGuide, you will need to configure each event in question.
|
||||||
|
|
||||||
|
1. Enabling the plugin
|
||||||
|
Within your event, extend the ``Settings`` menu and navigate to ``Plugins``. Activate the plugin in the
|
||||||
|
``Integrations`` tab.
|
||||||
|
|
||||||
|
2. Sell the event on the sales channel
|
||||||
|
Pick the sales channel or channels, on which you would like to sell your event by navigating to the event's general
|
||||||
|
settings page using the ``Sell on all sales channels`` or ``Restrict to specific sales channels`` checkboxes.
|
||||||
|
|
||||||
|
3. Configure one or more products to be sold on GetYourGuide
|
||||||
|
Either create a new or edit an existing product, that you would like to sell on GetYourGuide. To do so, you will
|
||||||
|
need to have checked the ``Sell on all sales channels`` or appropriate ``Restrict to specific sales channels``
|
||||||
|
checkbox of the product within it's ``Availability`` tab.
|
||||||
|
In addition, you will also need to set the GetYourGuide equivalent ticket category in the product's accordingly
|
||||||
|
named settings tab. Within your event, there can be only one product per ticket category. Depending on your further
|
||||||
|
configuration, you must at least select one product to be in the ``Adult`` or ``Group`` category.
|
||||||
|
|
||||||
|
4. Configuring the GetYourGuide-plugin
|
||||||
|
Once you have configured one or more products to be eligible to be sold on GetYourGuide, you'll need to configure a
|
||||||
|
few basic settings within the event (``Settings`` --> ``GetYourGuide``). The most important settings can be found
|
||||||
|
the in the ``Configuration`` tab, such as the location of the event on sale.
|
||||||
|
|
||||||
|
Ticket Categories
|
||||||
|
-----------------
|
||||||
|
While pretix only uses the ticket category term loosely to group together multiple products for nicer display,
|
||||||
|
GetYourGuide is relying on the ticket categories to price the tickets.
|
||||||
|
|
||||||
|
First of all, you need to make the decision on how you are planning on selling your tickets on GetYourGuide - in most
|
||||||
|
cases, this will reflect your current sales strategy within your pretix shop.
|
||||||
|
|
||||||
|
- Individual tickets
|
||||||
|
Every single person attending will need to purchase their own ticket. A family of two adults and two
|
||||||
|
children will have to purchase and pay for a total of 4 tickets.
|
||||||
|
In this case, you will need to offer *at least* a ticket of the ``Adult`` type, but may offer any other ticket
|
||||||
|
category type (Child, Youth, Senior, ...) in addition. But you cannot offer a ``Group`` ticket.
|
||||||
|
|
||||||
|
- Group tickets
|
||||||
|
Two groups, consisting of 10 and 20 participants respectively, won't need to purchase a total of 30 tickets, but
|
||||||
|
rather two group tickets. It is up to you to configure the group size limits within the GetYourGuide-settings of your
|
||||||
|
product.
|
||||||
|
Choosing this option, you cannot offer any other ticket categories besides ``Group``.
|
||||||
|
|
||||||
|
Setting up event dates and quotas
|
||||||
|
---------------------------------
|
||||||
|
Of course, in addition to creating products, you will also need to add them to a quota for them to be available for
|
||||||
|
sale. The process for doing this is the very same as for any regular event or event series.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When selling individual tickets through GetYourGuide, you will not be able to offer differing quantities for
|
||||||
|
individual ticket categories.
|
||||||
|
|
||||||
|
For this reason, we recommend to place all GetYourGuide-eligible products into the same quota. Should you however opt
|
||||||
|
to create multiple quotas which create an imbalance, pretix will report only the available number of tickets for the
|
||||||
|
lowest relevant quota.
|
||||||
|
|
||||||
|
Connecting your event to GetYourGuide
|
||||||
|
-------------------------------------
|
||||||
|
Once you have set up your event and products and performed all necessary configuration, you may want to use the
|
||||||
|
Analyzer-feature of our GetYourGuide-plugin (``Settings`` -> ``GetYourGuide`` -> tab ``Analyzer``).
|
||||||
|
|
||||||
|
The Analyzer should not display any blocking error messages and at least one event date that is ready for publishing on
|
||||||
|
the GetYourGuide platform.
|
||||||
|
|
||||||
|
At this point, you will need to setup your event (called ``product`` in the GetYourGuide universe) on their
|
||||||
|
`Supplier Portal`_ and connect it with your pretix shop. To do so, please follow the
|
||||||
|
`Connecting a new product to your Reservation System`_ on the GetYourGuide Supply Partner Help Center.
|
||||||
|
|
||||||
|
Select ``pretix.eu`` as your reservation system; the required ``product ID`` can be found in the ``Configuration`` tab
|
||||||
|
of the GetYourGuide plugin settings page.
|
||||||
|
|
||||||
|
From this point on, GetYourGuide will automatically import the availabilities and products and offer them for sale.
|
||||||
|
|
||||||
|
.. _Supplier Portal: https://suppliers.getyourguide.com/
|
||||||
|
.. _Connecting a new product to your Reservation System: https://supply.getyourguide.support/hc/en-us/articles/18008029689373-Connecting-a-new-product-to-your-Reservation-system
|
||||||
@@ -25,3 +25,4 @@ If you want to **create** a plugin, please go to the
|
|||||||
webinar
|
webinar
|
||||||
presale-saml
|
presale-saml
|
||||||
kulturpass
|
kulturpass
|
||||||
|
getyourguide
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
sphinx==7.3.*
|
sphinx==7.4.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
@@ -6,5 +6,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pygments-markdown-lexer
|
|
||||||
pyenchant==3.2.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==7.3.*
|
sphinx==7.4.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
@@ -7,5 +7,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pygments-markdown-lexer
|
|
||||||
pyenchant==3.2.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ Android 9 Support planned until at least 12/2025.
|
|||||||
Android 8 Support planned until at least 12/2025.
|
Android 8 Support planned until at least 12/2025.
|
||||||
Android 7 Support planned until at least 06/2025.
|
Android 7 Support planned until at least 06/2025.
|
||||||
Android 6 Support planned until at least 06/2025.
|
Android 6 Support planned until at least 06/2025.
|
||||||
Android 5 | Support planned until at least 06/2025.
|
Android 5 Support planned until at least 06/2025.
|
||||||
| No support for COVID certificate verification.
|
|
||||||
Android 4 Support dropped.
|
Android 4 Support dropped.
|
||||||
=========================== ==========================================================
|
=========================== ==========================================================
|
||||||
|
|
||||||
@@ -57,16 +56,17 @@ Android 8 | Support planned until at least 12/2025.
|
|||||||
Android 7 | Support planned until at least 12/2024.
|
Android 7 | Support planned until at least 12/2024.
|
||||||
| Support for Stripe Terminal to be dropped 05/2024.
|
| Support for Stripe Terminal to be dropped 05/2024.
|
||||||
| No support for Cryptovision TSE.
|
| No support for Cryptovision TSE.
|
||||||
|
| No support for SumUp.
|
||||||
Android 6 | Support planned until at least 12/2024.
|
Android 6 | Support planned until at least 12/2024.
|
||||||
| No support for Cryptovision TSE.
|
| No support for Cryptovision TSE.
|
||||||
| No support for Fiskal Cloud.
|
| No support for Fiskal Cloud.
|
||||||
| No support for Stripe Terminal.
|
| No support for Stripe Terminal.
|
||||||
|
| No support for SumUp.
|
||||||
Android 5 | Support planned until at least 12/2024.
|
Android 5 | Support planned until at least 12/2024.
|
||||||
| No support for Cryptovision TSE.
|
| No support for Cryptovision TSE.
|
||||||
| No support for Fiskal Cloud.
|
| No support for Fiskal Cloud.
|
||||||
| No support for Stripe Terminal.
|
| No support for Stripe Terminal.
|
||||||
| No support for SumUp.
|
| No support for SumUp.
|
||||||
| No support for COVID certificate verification.
|
|
||||||
Android 4 Support dropped.
|
Android 4 Support dropped.
|
||||||
=========================== ==========================================================
|
=========================== ==========================================================
|
||||||
|
|
||||||
@@ -87,9 +87,6 @@ Android 7 Support planned until at least 06/2025.
|
|||||||
Android 6 Support planned until at least 06/2025.
|
Android 6 Support planned until at least 06/2025.
|
||||||
Android 5 | Support planned until at least 06/2025.
|
Android 5 | Support planned until at least 06/2025.
|
||||||
| No support for Evolis printers on some devices.
|
| No support for Evolis printers on some devices.
|
||||||
Android 4.4 | Support planned until at least 06/2024.
|
|
||||||
| No support for USB printers.
|
|
||||||
| No support for Evolis printers.
|
|
||||||
Android 4 Support dropped.
|
Android 4 Support dropped.
|
||||||
=========================== ==========================================================
|
=========================== ==========================================================
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ without any special behavior.
|
|||||||
Connecting SSO providers (pretix as the SSO client)
|
Connecting SSO providers (pretix as the SSO client)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
To connect an external application as a SSO provider, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
||||||
in your organizer account.
|
in your organizer account.
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
||||||
|
|||||||
@@ -449,6 +449,29 @@ Further reading:
|
|||||||
|
|
||||||
* `Stripe Payment Method Domain registration`_
|
* `Stripe Payment Method Domain registration`_
|
||||||
|
|
||||||
|
|
||||||
|
Content Security Policy
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
When using a Content Security Policy (CSP) on your website, you may need to make some adjustments. If your pretix
|
||||||
|
shop is running under a custom domain, you need to add the following rules:
|
||||||
|
|
||||||
|
* ``script-src``: ``'unsafe-eval' https://pretix.eu`` (adjust to your domain for self-hosted pretix)
|
||||||
|
* ``style-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``connect-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``frame-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted)
|
||||||
|
* ``img-src``: ``https://pretix.eu`` (adjust to your domain for self-hosted pretix **and** for custom domain on pretix Hosted) and for pretix Hosted additionally add ``https://cdn.pretix.space``
|
||||||
|
|
||||||
|
|
||||||
|
External payment providers and Cross-Origin-Opener-Policy
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
If you use a payment provider that opens a new window during checkout (such as PayPal), be aware that setting
|
||||||
|
``Cross-Origin-Opener-Policy: same-origin`` results in an empty popup-window being opened in the foreground. This is
|
||||||
|
due to JavaScript not having access to the opened window. To mitigate this, you either need to always open the widget’s
|
||||||
|
checkout in a new tab (see :ref:`Always open a new tab`) or set ``Cross-Origin-Opener-Policy: same-origin-allow-popups``
|
||||||
|
|
||||||
|
|
||||||
Working with Cross-Origin-Embedder-Policy
|
Working with Cross-Origin-Embedder-Policy
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
+16
-18
@@ -22,7 +22,7 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Framework :: Django :: 4.1",
|
"Framework :: Django :: 4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -35,16 +35,15 @@ dependencies = [
|
|||||||
"cryptography>=3.4.2",
|
"cryptography>=3.4.2",
|
||||||
"css-inline==0.14.*",
|
"css-inline==0.14.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dj-static",
|
"Django[argon2]==4.2.*,>=4.2.15",
|
||||||
"Django[argon2]==4.2.*",
|
|
||||||
"django-bootstrap3==24.2",
|
"django-bootstrap3==24.2",
|
||||||
"django-compressor==4.5",
|
"django-compressor==4.5.1",
|
||||||
"django-countries==7.6.*",
|
"django-countries==7.6.*",
|
||||||
"django-filter==24.2",
|
"django-filter==24.3",
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.2.*",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.5.*",
|
"django-hijack==3.6.*",
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
"django-i18nfield==1.9.*,>=1.9.4",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==4.0",
|
"django-localflavor==4.0",
|
||||||
@@ -62,10 +61,10 @@ dependencies = [
|
|||||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.3.*",
|
"kombu==5.4.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.7", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.30.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.2.*",
|
"oauthlib==3.2.*",
|
||||||
@@ -73,17 +72,17 @@ dependencies = [
|
|||||||
"packaging",
|
"packaging",
|
||||||
"paypalrestsdk==1.13.*",
|
"paypalrestsdk==1.13.*",
|
||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.8.*",
|
"PyJWT==2.9.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==10.3.*",
|
"Pillow==10.4.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==5.27.*",
|
"protobuf==5.28.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.20.*",
|
"pycryptodome==3.20.*",
|
||||||
"pypdf==4.2.*",
|
"pypdf==4.3.*",
|
||||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
@@ -92,10 +91,9 @@ dependencies = [
|
|||||||
"redis==5.0.*",
|
"redis==5.0.*",
|
||||||
"reportlab==4.2.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.5.*",
|
"sentry-sdk==2.13.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
"slimit",
|
||||||
"static3==0.7.*",
|
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
@@ -110,10 +108,10 @@ dependencies = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.9.*",
|
"aiohttp==3.10.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.23.*",
|
"fakeredis==2.24.*",
|
||||||
"flake8==7.1.*",
|
"flake8==7.1.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==5.13.*",
|
"isort==5.13.*",
|
||||||
@@ -127,7 +125,7 @@ dev = [
|
|||||||
"pytest-rerunfailures==14.*",
|
"pytest-rerunfailures==14.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.6.*",
|
"pytest-xdist==3.6.*",
|
||||||
"pytest==8.2.*",
|
"pytest==8.3.*",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.7.0.dev0"
|
__version__ = "2024.9.0.dev0"
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
|||||||
'pretix.plugins.badges',
|
'pretix.plugins.badges',
|
||||||
'pretix.plugins.manualpayment',
|
'pretix.plugins.manualpayment',
|
||||||
'pretix.plugins.returnurl',
|
'pretix.plugins.returnurl',
|
||||||
|
'pretix.plugins.autocheckin',
|
||||||
'pretix.plugins.webcheckin',
|
'pretix.plugins.webcheckin',
|
||||||
'django_countries',
|
'django_countries',
|
||||||
'oauth2_provider',
|
'oauth2_provider',
|
||||||
@@ -79,6 +80,7 @@ ALL_LANGUAGES = [
|
|||||||
('de', _('German')),
|
('de', _('German')),
|
||||||
('de-informal', _('German (informal)')),
|
('de-informal', _('German (informal)')),
|
||||||
('ar', _('Arabic')),
|
('ar', _('Arabic')),
|
||||||
|
('eu', _('Basque')),
|
||||||
('ca', _('Catalan')),
|
('ca', _('Catalan')),
|
||||||
('zh-hans', _('Chinese (simplified)')),
|
('zh-hans', _('Chinese (simplified)')),
|
||||||
('zh-hant', _('Chinese (traditional)')),
|
('zh-hant', _('Chinese (traditional)')),
|
||||||
@@ -100,6 +102,7 @@ ALL_LANGUAGES = [
|
|||||||
('ro', _('Romanian')),
|
('ro', _('Romanian')),
|
||||||
('ru', _('Russian')),
|
('ru', _('Russian')),
|
||||||
('sk', _('Slovak')),
|
('sk', _('Slovak')),
|
||||||
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
|
from django.forms import MultipleChoiceField
|
||||||
|
from django_filters import Filter
|
||||||
|
from django_filters.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleCharField(forms.CharField):
|
||||||
|
widget = forms.MultipleHiddenInput
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
elif not isinstance(value, (list, tuple)):
|
||||||
|
raise ValidationError(
|
||||||
|
MultipleChoiceField.default_error_messages["invalid_list"], code="invalid_list"
|
||||||
|
)
|
||||||
|
return [str(val) for val in value]
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleCharFilter(Filter):
|
||||||
|
"""
|
||||||
|
This filter performs OR(by default) or AND(using conjoined=True) query
|
||||||
|
on the selected inputs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field_class = MultipleCharField
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.conjoined = kwargs.pop("conjoined", False)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def filter(self, qs, value):
|
||||||
|
if not value:
|
||||||
|
# Even though not a noop, no point filtering if empty.
|
||||||
|
return qs
|
||||||
|
|
||||||
|
if not self.conjoined:
|
||||||
|
q = Q()
|
||||||
|
for v in set(value):
|
||||||
|
predicate = self.get_filter_predicate(v)
|
||||||
|
if self.conjoined:
|
||||||
|
qs = self.get_method(qs)(**predicate)
|
||||||
|
else:
|
||||||
|
q |= Q(**predicate)
|
||||||
|
|
||||||
|
if not self.conjoined:
|
||||||
|
qs = self.get_method(qs)(q)
|
||||||
|
|
||||||
|
return qs.distinct() if self.distinct else qs
|
||||||
|
|
||||||
|
def get_filter_predicate(self, v):
|
||||||
|
name = self.field_name
|
||||||
|
if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
|
||||||
|
name = LOOKUP_SEP.join([name, self.lookup_expr])
|
||||||
|
try:
|
||||||
|
return {name: getattr(v, self.field.to_field_name)}
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
return {name: v}
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.db.models import prefetch_related_objects
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class AsymmetricField(serializers.Field):
|
class AsymmetricField(serializers.Field):
|
||||||
@@ -61,3 +63,59 @@ class CompatibleJSONField(serializers.JSONField):
|
|||||||
if value:
|
if value:
|
||||||
return json.loads(value)
|
return json.loads(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelMigrationMixin:
|
||||||
|
"""
|
||||||
|
Translates between the old field "sales_channels" and the new field combo "all_sales_channels"/"limit_sales_channels".
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def organizer(self):
|
||||||
|
if "organizer" in self.context:
|
||||||
|
return self.context["organizer"]
|
||||||
|
elif "event" in self.context:
|
||||||
|
return self.context["event"].organizer
|
||||||
|
else:
|
||||||
|
raise ValueError("organizer not in context")
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if "sales_channels" in data:
|
||||||
|
prefetch_related_objects([self.organizer], "sales_channels")
|
||||||
|
all_channels = {
|
||||||
|
s.identifier for s in
|
||||||
|
self.organizer.sales_channels.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||||
|
raise ValidationError(
|
||||||
|
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
|
"the list of all sales channels."
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||||
|
raise ValidationError(
|
||||||
|
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
|
"the same list."
|
||||||
|
)
|
||||||
|
|
||||||
|
if data["sales_channels"] == all_channels:
|
||||||
|
data["all_sales_channels"] = True
|
||||||
|
data["limit_sales_channels"] = []
|
||||||
|
else:
|
||||||
|
data["all_sales_channels"] = False
|
||||||
|
data["limit_sales_channels"] = data["sales_channels"]
|
||||||
|
del data["sales_channels"]
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
value = super().to_representation(value)
|
||||||
|
if value.get("all_sales_channels"):
|
||||||
|
prefetch_related_objects([self.organizer], "sales_channels")
|
||||||
|
value["sales_channels"] = sorted([
|
||||||
|
s.identifier for s in
|
||||||
|
self.organizer.sales_channels.all()
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
value["sales_channels"] = value["limit_sales_channels"]
|
||||||
|
return value
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Seat, Voucher
|
from pretix.base.models import SalesChannel, Seat, Voucher
|
||||||
from pretix.base.models.orders import CartPosition
|
from pretix.base.models.orders import CartPosition
|
||||||
|
|
||||||
|
|
||||||
@@ -212,7 +212,11 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||||
seat = serializers.CharField(required=False, allow_null=True)
|
seat = serializers.CharField(required=False, allow_null=True)
|
||||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
sales_channel = serializers.SlugRelatedField(
|
||||||
|
slug_field='identifier',
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
voucher = serializers.CharField(required=False, allow_null=True)
|
voucher = serializers.CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -221,6 +225,10 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
|
|
||||||
def validate_cart_id(self, cid):
|
def validate_cart_id(self, cid):
|
||||||
if cid and not cid.endswith('@api'):
|
if cid and not cid.endswith('@api'):
|
||||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||||
|
|||||||
@@ -25,14 +25,20 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList
|
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||||
|
|
||||||
|
|
||||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||||
checkin_count = serializers.IntegerField(read_only=True)
|
checkin_count = serializers.IntegerField(read_only=True)
|
||||||
position_count = serializers.IntegerField(read_only=True)
|
position_count = serializers.IntegerField(read_only=True)
|
||||||
|
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
@@ -43,6 +49,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||||
|
|
||||||
@@ -72,10 +80,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('subevent'):
|
if full_data.get('subevent'):
|
||||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||||
|
|
||||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
|
||||||
if channel not in get_all_sales_channels():
|
|
||||||
raise ValidationError(_('Unknown sales channel.'))
|
|
||||||
|
|
||||||
CheckinList.validate_rules(data.get('rules'))
|
CheckinList.validate_rules(data.get('rules'))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -19,18 +19,27 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import Discount
|
from pretix.base.models import Discount, SalesChannel
|
||||||
|
|
||||||
|
|
||||||
class DiscountSerializer(I18nAwareModelSerializer):
|
class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Discount
|
model = Discount
|
||||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
|
||||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||||
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||||
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
||||||
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
||||||
@@ -39,6 +48,7 @@ class DiscountSerializer(I18nAwareModelSerializer):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||||
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -46,10 +46,15 @@ from rest_framework import serializers
|
|||||||
from rest_framework.fields import ChoiceField, Field
|
from rest_framework.fields import ChoiceField, Field
|
||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import (
|
||||||
|
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
|
)
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
from pretix.base.models import (
|
||||||
|
CartPosition, Device, Event, OrderPosition, SalesChannel, Seat, TaxRule,
|
||||||
|
TeamAPIToken, Voucher,
|
||||||
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
@@ -161,7 +166,7 @@ class ValidKeysField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(I18nAwareModelSerializer):
|
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||||
plugins = PluginsField(required=False, source='*')
|
plugins = PluginsField(required=False, source='*')
|
||||||
@@ -170,6 +175,13 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||||
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
def get_event_url(self, event):
|
def get_event_url(self, event):
|
||||||
return build_absolute_uri(event, 'presale:event.index')
|
return build_absolute_uri(event, 'presale:event.index')
|
||||||
@@ -180,7 +192,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
||||||
'sales_channels', 'best_availability_state', 'public_url')
|
'all_sales_channels', 'limit_sales_channels', 'best_availability_state', 'public_url')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -188,6 +200,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
self.fields.pop('valid_keys')
|
self.fields.pop('valid_keys')
|
||||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||||
self.fields.pop('best_availability_state')
|
self.fields.pop('best_availability_state')
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -269,13 +282,17 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
plugins_available = {
|
plugins_available = {
|
||||||
p.module for p in get_all_plugins(self.instance)
|
p.module: p for p in get_all_plugins(self.instance)
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
}
|
}
|
||||||
|
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||||
|
|
||||||
for plugin in value.get('plugins'):
|
for plugin in value.get('plugins'):
|
||||||
if plugin not in plugins_available:
|
if plugin not in plugins_available:
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
if getattr(plugins_available[plugin], 'restricted', False):
|
||||||
|
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||||
|
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -828,6 +845,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||||
|
'seating_allow_blocked_seats_for_channel',
|
||||||
]
|
]
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
# These are read-only since they are currently only settable on organizers, not events
|
# These are read-only since they are currently only settable on organizers, not events
|
||||||
@@ -878,6 +896,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'locale',
|
'locale',
|
||||||
'last_order_modification_date',
|
'last_order_modification_date',
|
||||||
'show_quota_left',
|
'show_quota_left',
|
||||||
|
'show_dates_on_frontpage',
|
||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'attendee_names_asked',
|
'attendee_names_asked',
|
||||||
'attendee_names_required',
|
'attendee_names_required',
|
||||||
@@ -953,3 +972,77 @@ class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ItemMetaProperty
|
model = ItemMetaProperty
|
||||||
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
||||||
|
|
||||||
|
|
||||||
|
def prefetch_by_id(items, qs, id_attr, target_attr):
|
||||||
|
"""
|
||||||
|
Prefetches a related object on each item in the given list of items by searching by id or another
|
||||||
|
unique field. The id value is read from the attribute on item specified in `id_attr`, searched on queryset `qs` by
|
||||||
|
the primary key, and the resulting prefetched model object is stored into `target_attr` on the item.
|
||||||
|
"""
|
||||||
|
ids = [getattr(item, id_attr) for item in items if getattr(item, id_attr)]
|
||||||
|
if ids:
|
||||||
|
result = qs.in_bulk(id_list=ids)
|
||||||
|
for item in items:
|
||||||
|
setattr(item, target_attr, result.get(getattr(item, id_attr)))
|
||||||
|
|
||||||
|
|
||||||
|
class SeatSerializer(I18nAwareModelSerializer):
|
||||||
|
orderposition = serializers.IntegerField(source='orderposition_id')
|
||||||
|
cartposition = serializers.IntegerField(source='cartposition_id')
|
||||||
|
voucher = serializers.IntegerField(source='voucher_id')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Seat
|
||||||
|
read_only_fields = (
|
||||||
|
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||||
|
'seat_number', 'seat_label', 'seat_guid', 'product',
|
||||||
|
'orderposition', 'cartposition', 'voucher',
|
||||||
|
)
|
||||||
|
fields = (
|
||||||
|
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||||
|
'seat_number', 'seat_label', 'seat_guid', 'product', 'blocked',
|
||||||
|
'orderposition', 'cartposition', 'voucher',
|
||||||
|
)
|
||||||
|
|
||||||
|
def prefetch_expanded_data(self, items, request, expand_fields):
|
||||||
|
if 'orderposition' in expand_fields:
|
||||||
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
|
||||||
|
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
|
||||||
|
if 'cartposition' in expand_fields:
|
||||||
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
|
||||||
|
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
|
||||||
|
if 'voucher' in expand_fields:
|
||||||
|
if 'can_view_vouchers' not in request.eventpermset:
|
||||||
|
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
|
||||||
|
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
|
||||||
|
|
||||||
|
def __init__(self, instance, *args, **kwargs):
|
||||||
|
if not kwargs.get('data'):
|
||||||
|
self.prefetch_expanded_data(instance if hasattr(instance, '__iter__') else [instance],
|
||||||
|
kwargs['context']['request'],
|
||||||
|
kwargs['context']['expand_fields'])
|
||||||
|
|
||||||
|
super().__init__(instance, *args, **kwargs)
|
||||||
|
|
||||||
|
if 'orderposition' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.media import (
|
||||||
|
NestedOrderPositionSerializer,
|
||||||
|
)
|
||||||
|
self.fields['orderposition'] = NestedOrderPositionSerializer(read_only=True, context=self.context['order_context'])
|
||||||
|
try:
|
||||||
|
del self.fields['orderposition'].fields['seat']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'cartposition' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.cart import CartPositionSerializer
|
||||||
|
self.fields['cartposition'] = CartPositionSerializer(read_only=True)
|
||||||
|
del self.fields['cartposition'].fields['seat']
|
||||||
|
|
||||||
|
if 'voucher' in self.context['expand_fields']:
|
||||||
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
|
self.fields['voucher'] = VoucherSerializer(read_only=True)
|
||||||
|
del self.fields['voucher'].fields['seat']
|
||||||
|
|||||||
@@ -42,19 +42,27 @@ from django.utils.functional import cached_property, lazy
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||||
from pretix.api.serializers.event import MetaDataField
|
from pretix.api.serializers.event import MetaDataField
|
||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||||
ItemVariationMetaValue, Question, QuestionOption, Quota,
|
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||||
coerce_to_string=True)
|
coerce_to_string=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemVariation
|
model = ItemVariation
|
||||||
@@ -63,11 +71,14 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text',
|
'checkin_attention', 'checkin_text',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = (
|
||||||
|
self.context['event'].organizer.sales_channels.all() if 'event' in self.context else SalesChannel.objects.none()
|
||||||
|
)
|
||||||
|
|
||||||
def validate_meta_data(self, value):
|
def validate_meta_data(self, value):
|
||||||
for key in value['meta_data'].keys():
|
for key in value['meta_data'].keys():
|
||||||
@@ -76,10 +87,17 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||||
coerce_to_string=True)
|
coerce_to_string=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemVariation
|
model = ItemVariation
|
||||||
@@ -88,21 +106,26 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text',
|
'checkin_attention', 'checkin_text',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||||
variation = ItemVariation.objects.create(**validated_data)
|
variation = ItemVariation.objects.create(**validated_data)
|
||||||
|
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
variation.require_membership_types.add(*require_membership_types)
|
variation.require_membership_types.add(*require_membership_types)
|
||||||
|
|
||||||
|
if limit_sales_channels:
|
||||||
|
variation.limit_sales_channels.add(*limit_sales_channels)
|
||||||
|
|
||||||
# Meta data
|
# Meta data
|
||||||
if meta_data is not None:
|
if meta_data is not None:
|
||||||
for key, value in meta_data.items():
|
for key, value in meta_data.items():
|
||||||
@@ -223,7 +246,7 @@ class ItemTaxRateField(serializers.Field):
|
|||||||
return str(Decimal('0.00'))
|
return str(Decimal('0.00'))
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(I18nAwareModelSerializer):
|
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
@@ -232,11 +255,18 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
'image/png', 'image/jpeg', 'image/gif'
|
'image/png', 'image/jpeg', 'image/gif'
|
||||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||||
|
limit_sales_channels = serializers.SlugRelatedField(
|
||||||
|
slug_field="identifier",
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
allow_empty=True,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
|
||||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
'description', 'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||||
'personalized', 'position', 'picture',
|
'personalized', 'position', 'picture',
|
||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
@@ -259,6 +289,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
if not self.read_only:
|
if not self.read_only:
|
||||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||||
|
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -335,7 +367,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
picture = validated_data.pop('picture', None)
|
picture = validated_data.pop('picture', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||||
item = Item.objects.create(**validated_data)
|
item = Item.objects.create(**validated_data)
|
||||||
|
if limit_sales_channels:
|
||||||
|
item.limit_sales_channels.add(*limit_sales_channels)
|
||||||
if picture:
|
if picture:
|
||||||
item.picture.save(os.path.basename(picture.name), picture)
|
item.picture.save(os.path.basename(picture.name), picture)
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
@@ -343,10 +378,13 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
for variation_data in variations_data:
|
for variation_data in variations_data:
|
||||||
require_membership_types = variation_data.pop('require_membership_types', [])
|
require_membership_types = variation_data.pop('require_membership_types', [])
|
||||||
|
limit_sales_channels = variation_data.pop('limit_sales_channels', [])
|
||||||
var_meta_data = variation_data.pop('meta_data', {})
|
var_meta_data = variation_data.pop('meta_data', {})
|
||||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||||
if require_membership_types:
|
if require_membership_types:
|
||||||
v.require_membership_types.add(*require_membership_types)
|
v.require_membership_types.add(*require_membership_types)
|
||||||
|
if limit_sales_channels:
|
||||||
|
v.limit_sales_channels.add(*limit_sales_channels)
|
||||||
|
|
||||||
if var_meta_data is not None:
|
if var_meta_data is not None:
|
||||||
for key, value in var_meta_data.items():
|
for key, value in var_meta_data.items():
|
||||||
|
|||||||
@@ -46,13 +46,12 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||||
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
|
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
@@ -166,7 +165,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Seat
|
model = Seat
|
||||||
fields = ('id', 'name', 'seat_guid')
|
fields = ('id', 'name', 'seat_guid', 'zone_name', 'row_name', 'row_label', 'seat_label', 'seat_number')
|
||||||
|
|
||||||
|
|
||||||
class AnswerSerializer(I18nAwareModelSerializer):
|
class AnswerSerializer(I18nAwareModelSerializer):
|
||||||
@@ -274,9 +273,15 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CheckinSerializer(I18nAwareModelSerializer):
|
class CheckinSerializer(I18nAwareModelSerializer):
|
||||||
|
device_id = serializers.SlugRelatedField(
|
||||||
|
source='device',
|
||||||
|
slug_field='device_id',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Checkin
|
model = Checkin
|
||||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'type')
|
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||||
|
|
||||||
|
|
||||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||||
@@ -586,7 +591,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||||
|
|
||||||
if 'variation' in self.context['expand']:
|
if 'variation' in self.context['expand']:
|
||||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
|
||||||
|
|
||||||
if 'answers.question' in self.context['expand']:
|
if 'answers.question' in self.context['expand']:
|
||||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||||
@@ -714,6 +719,11 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||||
url = OrderURLField(source='*', read_only=True)
|
url = OrderURLField(source='*', read_only=True)
|
||||||
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
||||||
|
sales_channel = serializers.SlugRelatedField(
|
||||||
|
slug_field='identifier',
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@@ -722,7 +732,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||||
'url', 'customer', 'valid_if_pending'
|
'url', 'customer', 'valid_if_pending', 'api_meta'
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
@@ -732,6 +742,10 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
if "organizer" in self.context:
|
||||||
|
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||||
|
else:
|
||||||
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
if not self.context['pdf_data']:
|
if not self.context['pdf_data']:
|
||||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||||
|
|
||||||
@@ -778,7 +792,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
||||||
'phone', 'valid_if_pending']
|
'phone', 'valid_if_pending', 'api_meta']
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -1033,19 +1047,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
require_approval = serializers.BooleanField(default=False, required=False)
|
require_approval = serializers.BooleanField(default=False, required=False)
|
||||||
simulate = serializers.BooleanField(default=False, required=False)
|
simulate = serializers.BooleanField(default=False, required=False)
|
||||||
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
||||||
|
sales_channel = serializers.SlugRelatedField(
|
||||||
|
slug_field='identifier',
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||||
self.fields['expires'].required = False
|
self.fields['expires'].required = False
|
||||||
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||||
'require_approval', 'valid_if_pending', 'expires')
|
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1059,11 +1079,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError('Expiration date must be in the future.')
|
raise ValidationError('Expiration date must be in the future.')
|
||||||
return expires
|
return expires
|
||||||
|
|
||||||
def validate_sales_channel(self, channel):
|
|
||||||
if channel not in get_all_sales_channels():
|
|
||||||
raise ValidationError('Unknown sales channel.')
|
|
||||||
return channel
|
|
||||||
|
|
||||||
def validate_code(self, code):
|
def validate_code(self, code):
|
||||||
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -1125,20 +1140,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(errs)
|
raise ValidationError(errs)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_testmode(self, testmode):
|
|
||||||
if 'sales_channel' in self.initial_data:
|
|
||||||
try:
|
|
||||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
|
||||||
|
|
||||||
if testmode and not sales_channel.testmode_supported:
|
|
||||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
|
||||||
except KeyError:
|
|
||||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
|
||||||
# sales_channel
|
|
||||||
pass
|
|
||||||
|
|
||||||
return testmode
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||||
@@ -1147,9 +1148,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
simulate = validated_data.pop('simulate', False)
|
simulate = validated_data.pop('simulate', False)
|
||||||
|
|
||||||
|
if not validated_data.get("sales_channel"):
|
||||||
|
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||||
|
|
||||||
|
if validated_data.get("testmode") and not validated_data["sales_channel"].type_instance.testmode_supported:
|
||||||
|
raise ValidationError({"testmode": ["This sales channel does not provide support for test mode."]})
|
||||||
|
|
||||||
self._send_mail = validated_data.pop('send_email', False)
|
self._send_mail = validated_data.pop('send_email', False)
|
||||||
if self._send_mail is None:
|
if self._send_mail is None:
|
||||||
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
|
self._send_mail = validated_data["sales_channel"].identifier in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -1309,7 +1317,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||||
else:
|
else:
|
||||||
seat_usage[seat] += 1
|
seat_usage[seat] += 1
|
||||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
|
sales_channel_id = validated_data['sales_channel'].identifier
|
||||||
|
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=sales_channel_id)) or seat_usage[seat] > 1:
|
||||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||||
elif seated:
|
elif seated:
|
||||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||||
@@ -1368,6 +1377,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
if validated_data.get('locale', None) is None:
|
if validated_data.get('locale', None) is None:
|
||||||
validated_data['locale'] = self.context['event'].settings.locale
|
validated_data['locale'] = self.context['event'].settings.locale
|
||||||
|
|
||||||
order = Order(event=self.context['event'], **validated_data)
|
order = Order(event=self.context['event'], **validated_data)
|
||||||
if not validated_data.get('expires'):
|
if not validated_data.get('expires'):
|
||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from pretix.base.i18n import get_language_without_region
|
|||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
@@ -165,6 +165,36 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
|||||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||||
|
type = serializers.CharField(default="api")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SalesChannel
|
||||||
|
fields = ('identifier', 'type', 'label', 'position')
|
||||||
|
|
||||||
|
def validate_type(self, value):
|
||||||
|
if (not self.instance or not self.instance.pk) and value != "api":
|
||||||
|
raise ValidationError(
|
||||||
|
"You can currently only create channels of type 'api' through the API."
|
||||||
|
)
|
||||||
|
if value and self.instance and self.instance.pk and self.instance.type != value:
|
||||||
|
raise ValidationError(
|
||||||
|
"You cannot change the type of a sales channel."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_identifier(self, value):
|
||||||
|
if (not self.instance or not self.instance.pk) and not value.startswith("api."):
|
||||||
|
raise ValidationError(
|
||||||
|
"Your identifier needs to start with 'api.'."
|
||||||
|
)
|
||||||
|
if value and self.instance and self.instance.pk and self.instance.identifier != value:
|
||||||
|
raise ValidationError(
|
||||||
|
"You cannot change the identifier of a sales channel."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||||
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
|||||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||||
orga_router.register(r'customers', organizer.CustomerViewSet)
|
orga_router.register(r'customers', organizer.CustomerViewSet)
|
||||||
|
orga_router.register(r'saleschannels', organizer.SalesChannelViewSet)
|
||||||
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
||||||
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
||||||
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
||||||
@@ -86,6 +87,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
|||||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||||
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
||||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||||
|
event_router.register(r'seats', event.SeatViewSet)
|
||||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
@@ -94,6 +96,9 @@ event_router.register(r'exporters', exporters.EventExportersViewSet, basename='e
|
|||||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||||
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||||
|
|
||||||
|
subevent_router = routers.DefaultRouter()
|
||||||
|
subevent_router.register(r'seats', event.SeatViewSet)
|
||||||
|
|
||||||
checkinlist_router = routers.DefaultRouter()
|
checkinlist_router = routers.DefaultRouter()
|
||||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||||
|
|
||||||
@@ -131,6 +136,7 @@ urlpatterns = [
|
|||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||||
name="event.settings"),
|
name="event.settings"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||||
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/subevents/(?P<subevent>\d+)/', include(subevent_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||||
|
|||||||
@@ -211,8 +211,12 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
|
|
||||||
if validated_data.get('seat'):
|
if validated_data.get('seat'):
|
||||||
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
||||||
|
if validated_data.get('sales_channel'):
|
||||||
|
sales_channel_id = validated_data.get('sales_channel').identifier
|
||||||
|
else:
|
||||||
|
sales_channel_id = "web"
|
||||||
if not validated_data['seat'].is_available(
|
if not validated_data['seat'].is_available(
|
||||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
sales_channel=sales_channel_id,
|
||||||
distance_ignore_cart_id=validated_data['cart_id'],
|
distance_ignore_cart_id=validated_data['cart_id'],
|
||||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
OrderPosition.objects.prefetch_related(
|
OrderPosition.objects.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -406,7 +406,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
'item__variations').select_related('item__tax_rule')
|
'item__variations').select_related('item__tax_rule')
|
||||||
|
|
||||||
if expand and 'variation' in expand:
|
if expand and 'variation' in expand:
|
||||||
qs = qs.prefetch_related('variation')
|
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
write_permission = 'can_change_items'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.discounts.all()
|
return self.request.event.discounts.prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
|
)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
|
|||||||
+115
-11
@@ -40,19 +40,22 @@ from django.utils.timezone import now
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import serializers, views, viewsets
|
from rest_framework import serializers, views, viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import (
|
||||||
|
NotFound, PermissionDenied, ValidationError,
|
||||||
|
)
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.auth.permission import EventCRUDPermission
|
from pretix.api.auth.permission import EventCRUDPermission
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.event import (
|
from pretix.api.serializers.event import (
|
||||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
|
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
|
||||||
TaxRuleSerializer,
|
SubEventSerializer, TaxRuleSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
|
CartPosition, Device, Event, ItemMetaProperty, Seat, SeatCategoryMapping,
|
||||||
TaxRule, TeamAPIToken,
|
TaxRule, TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
@@ -113,7 +116,10 @@ with scopes_disabled():
|
|||||||
return queryset.exclude(expr)
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
def sales_channel_qs(self, queryset, name, value):
|
def sales_channel_qs(self, queryset, name, value):
|
||||||
return queryset.filter(sales_channels__contains=value)
|
return queryset.filter(
|
||||||
|
Q(all_sales_channels=True) |
|
||||||
|
Q(limit_sales_channels__identifier=value)
|
||||||
|
)
|
||||||
|
|
||||||
def search_qs(self, queryset, name, value):
|
def search_qs(self, queryset, name, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@@ -135,6 +141,12 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('date_from', 'slug')
|
ordering_fields = ('date_from', 'slug')
|
||||||
filterset_class = EventFilter
|
filterset_class = EventFilter
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
**super().get_serializer_context(),
|
||||||
|
"organizer": self.request.organizer,
|
||||||
|
}
|
||||||
|
|
||||||
def get_copy_from_queryset(self):
|
def get_copy_from_queryset(self):
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
return self.request.auth.get_events_with_any_permission()
|
return self.request.auth.get_events_with_any_permission()
|
||||||
@@ -153,13 +165,20 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
qs = filter_qs_by_attr(qs, self.request)
|
qs = filter_qs_by_attr(qs, self.request)
|
||||||
|
|
||||||
if 'with_availability_for' in self.request.GET:
|
if 'with_availability_for' in self.request.GET:
|
||||||
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
qs = Event.annotated(
|
||||||
|
qs,
|
||||||
|
channel=get_object_or_404(
|
||||||
|
self.request.organizer.sales_channels,
|
||||||
|
identifier=self.request.GET.get('with_availability_for')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return qs.prefetch_related(
|
return qs.prefetch_related(
|
||||||
'organizer',
|
'organizer',
|
||||||
'meta_values',
|
'meta_values',
|
||||||
'meta_values__property',
|
'meta_values__property',
|
||||||
'item_meta_properties',
|
'item_meta_properties',
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'seat_category_mappings',
|
'seat_category_mappings',
|
||||||
to_attr='_seat_category_mappings',
|
to_attr='_seat_category_mappings',
|
||||||
@@ -268,8 +287,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event.is_public = serializer.validated_data['is_public']
|
new_event.is_public = serializer.validated_data['is_public']
|
||||||
if 'testmode' in serializer.validated_data:
|
if 'testmode' in serializer.validated_data:
|
||||||
new_event.testmode = serializer.validated_data['testmode']
|
new_event.testmode = serializer.validated_data['testmode']
|
||||||
if 'sales_channels' in serializer.validated_data:
|
|
||||||
new_event.sales_channels = serializer.validated_data['sales_channels']
|
|
||||||
if 'has_subevents' in serializer.validated_data:
|
if 'has_subevents' in serializer.validated_data:
|
||||||
new_event.has_subevents = serializer.validated_data['has_subevents']
|
new_event.has_subevents = serializer.validated_data['has_subevents']
|
||||||
if 'date_admission' in serializer.validated_data:
|
if 'date_admission' in serializer.validated_data:
|
||||||
@@ -277,6 +294,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event.save()
|
new_event.save()
|
||||||
if 'timezone' in serializer.validated_data:
|
if 'timezone' in serializer.validated_data:
|
||||||
new_event.settings.timezone = serializer.validated_data['timezone']
|
new_event.settings.timezone = serializer.validated_data['timezone']
|
||||||
|
|
||||||
|
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||||
|
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||||
|
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||||
else:
|
else:
|
||||||
serializer.instance.set_defaults()
|
serializer.instance.set_defaults()
|
||||||
|
|
||||||
@@ -379,7 +400,10 @@ with scopes_disabled():
|
|||||||
return queryset.exclude(expr)
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
def sales_channel_qs(self, queryset, name, value):
|
def sales_channel_qs(self, queryset, name, value):
|
||||||
return queryset.filter(event__sales_channels__contains=value)
|
return queryset.filter(
|
||||||
|
Q(event__all_sales_channels=True) |
|
||||||
|
Q(event__limit_sales_channels__identifier=value)
|
||||||
|
)
|
||||||
|
|
||||||
def search_qs(self, queryset, name, value):
|
def search_qs(self, queryset, name, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@@ -421,13 +445,19 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
qs = SubEvent.objects.filter(
|
qs = SubEvent.objects.filter(
|
||||||
event__organizer=self.request.organizer,
|
event__organizer=self.request.organizer,
|
||||||
event__in=self.request.user.get_events_with_any_permission()
|
event__in=self.request.user.get_events_with_any_permission(request=self.request)
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = filter_qs_by_attr(qs, self.request)
|
qs = filter_qs_by_attr(qs, self.request)
|
||||||
|
|
||||||
if 'with_availability_for' in self.request.GET:
|
if 'with_availability_for' in self.request.GET:
|
||||||
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
qs = SubEvent.annotated(
|
||||||
|
qs,
|
||||||
|
channel=get_object_or_404(
|
||||||
|
self.request.organizer.sales_channels,
|
||||||
|
identifier=self.request.GET.get('with_availability_for')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return qs.prefetch_related(
|
return qs.prefetch_related(
|
||||||
'event',
|
'event',
|
||||||
@@ -639,3 +669,77 @@ class EventSettingsView(views.APIView):
|
|||||||
'request': request
|
'request': request
|
||||||
})
|
})
|
||||||
return Response(s.data)
|
return Response(s.data)
|
||||||
|
|
||||||
|
|
||||||
|
class SeatFilter(FilterSet):
|
||||||
|
is_available = django_filters.BooleanFilter(method="is_available_qs")
|
||||||
|
|
||||||
|
def is_available_qs(self, queryset, name, value):
|
||||||
|
expr = (
|
||||||
|
Q(orderposition_id__isnull=True, cartposition_id__isnull=True, voucher_id__isnull=True)
|
||||||
|
)
|
||||||
|
if self.request.event.settings.seating_minimal_distance:
|
||||||
|
expr = expr & Q(has_closeby_taken=False)
|
||||||
|
if value:
|
||||||
|
return queryset.filter(expr)
|
||||||
|
else:
|
||||||
|
return queryset.exclude(expr)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Seat
|
||||||
|
fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
|
||||||
|
|
||||||
|
|
||||||
|
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
|
serializer_class = SeatSerializer
|
||||||
|
queryset = Seat.objects.none()
|
||||||
|
write_permission = 'can_change_event_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend, )
|
||||||
|
filterset_class = SeatFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
if self.request.event.has_subevents and 'subevent' in self.request.resolver_match.kwargs:
|
||||||
|
try:
|
||||||
|
subevent = self.request.event.subevents.get(pk=self.request.resolver_match.kwargs['subevent'])
|
||||||
|
except SubEvent.DoesNotExist:
|
||||||
|
raise NotFound('Subevent not found')
|
||||||
|
qs = Seat.annotated(
|
||||||
|
event_id=self.request.event.id,
|
||||||
|
subevent=subevent,
|
||||||
|
qs=subevent.seats.all(),
|
||||||
|
annotate_ids=True,
|
||||||
|
minimal_distance=self.request.event.settings.seating_minimal_distance,
|
||||||
|
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
|
||||||
|
)
|
||||||
|
elif not self.request.event.has_subevents and 'subevent' not in self.request.resolver_match.kwargs:
|
||||||
|
qs = Seat.annotated(
|
||||||
|
event_id=self.request.event.id,
|
||||||
|
subevent=None,
|
||||||
|
qs=self.request.event.seats.all(),
|
||||||
|
annotate_ids=True,
|
||||||
|
minimal_distance=self.request.event.settings.seating_minimal_distance,
|
||||||
|
distance_only_within_row=self.request.event.settings.seating_distance_only_within_row,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotFound('Please use the subevent-specific endpoint' if self.request.event.has_subevents
|
||||||
|
else 'This event has no subevents')
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['expand_fields'] = self.request.query_params.getlist('expand')
|
||||||
|
ctx['order_context'] = {
|
||||||
|
'event': self.request.event,
|
||||||
|
'pdf_data': None,
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
super().perform_update(serializer)
|
||||||
|
serializer.instance.event.log_action(
|
||||||
|
"pretix.event.seats.blocks.changed",
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={"seats": [serializer.instance.pk]},
|
||||||
|
)
|
||||||
|
|||||||
@@ -56,10 +56,17 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
from pretix.helpers.i18n import i18ncomp
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class ItemFilter(FilterSet):
|
class ItemFilter(FilterSet):
|
||||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||||
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
|
|
||||||
|
def search_qs(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(internal_name__icontains=value) | Q(name__icontains=i18ncomp(value))
|
||||||
|
)
|
||||||
|
|
||||||
def tax_rate_qs(self, queryset, name, value):
|
def tax_rate_qs(self, queryset, name, value):
|
||||||
if value in ("0", "None", "0.00"):
|
if value in ("0", "None", "0.00"):
|
||||||
@@ -71,6 +78,18 @@ with scopes_disabled():
|
|||||||
model = Item
|
model = Item
|
||||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||||
|
|
||||||
|
class ItemVariationFilter(FilterSet):
|
||||||
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
|
|
||||||
|
def search_qs(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(value__icontains=i18ncomp(value))
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ItemVariation
|
||||||
|
fields = ['active']
|
||||||
|
|
||||||
|
|
||||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
@@ -87,6 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||||
'variations__meta_values', 'variations__meta_values__property',
|
'variations__meta_values', 'variations__meta_values__property',
|
||||||
'require_membership_types', 'variations__require_membership_types',
|
'require_membership_types', 'variations__require_membership_types',
|
||||||
|
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
@@ -139,6 +159,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = ItemVariationSerializer
|
serializer_class = ItemVariationSerializer
|
||||||
queryset = ItemVariation.objects.none()
|
queryset = ItemVariation.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
||||||
|
filterset_class = ItemVariationFilter
|
||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
@@ -152,7 +173,8 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
return self.item.variations.all().prefetch_related(
|
return self.item.variations.all().prefetch_related(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
'meta_values__property',
|
'meta_values__property',
|
||||||
'require_membership_types'
|
'require_membership_types',
|
||||||
|
'limit_sales_channels',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
queryset=OrderPosition.objects.select_related(
|
queryset=OrderPosition.objects.select_related(
|
||||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from rest_framework.mixins import CreateModelMixin
|
|||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from pretix.api.filters import MultipleCharFilter
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
@@ -108,6 +109,7 @@ with scopes_disabled():
|
|||||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||||
|
created_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
||||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||||
search = django_filters.CharFilter(method='search_qs')
|
search = django_filters.CharFilter(method='search_qs')
|
||||||
@@ -115,6 +117,8 @@ with scopes_disabled():
|
|||||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
||||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
||||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
customer = django_filters.CharFilter(field_name='customer__identifier')
|
||||||
|
sales_channel = django_filters.CharFilter(field_name='sales_channel__identifier')
|
||||||
|
payment_provider = django_filters.CharFilter(method='provider_qs')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@@ -138,6 +142,11 @@ with scopes_disabled():
|
|||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def provider_qs(self, qs, name, value):
|
||||||
|
return qs.filter(Exists(
|
||||||
|
OrderPayment.objects.filter(order=OuterRef('pk'), provider=value)
|
||||||
|
))
|
||||||
|
|
||||||
def subevent_before_qs(self, qs, name, value):
|
def subevent_before_qs(self, qs, name, value):
|
||||||
if getattr(self.request, 'event', None):
|
if getattr(self.request, 'event', None):
|
||||||
subevents = self.request.event.subevents
|
subevents = self.request.event.subevents
|
||||||
@@ -229,7 +238,7 @@ class OrderViewSetMixin:
|
|||||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||||
qs = qs.select_related('customer')
|
qs = qs.select_related('customer')
|
||||||
|
|
||||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
qs = qs.select_related('sales_channel').prefetch_related(self._positions_prefetch(self.request))
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _positions_prefetch(self, request):
|
def _positions_prefetch(self, request):
|
||||||
@@ -249,7 +258,7 @@ class OrderViewSetMixin:
|
|||||||
return Prefetch(
|
return Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
||||||
)),
|
)),
|
||||||
@@ -270,7 +279,7 @@ class OrderViewSetMixin:
|
|||||||
return Prefetch(
|
return Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
'item', 'variation',
|
'item', 'variation',
|
||||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||||
'seat',
|
'seat',
|
||||||
@@ -316,6 +325,11 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
else:
|
else:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
@@ -1078,7 +1092,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
'item_meta_properties',
|
'item_meta_properties',
|
||||||
)
|
)
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1097,7 +1111,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
qs.prefetch_related(
|
qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1121,7 +1135,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||||
@@ -1812,17 +1826,14 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class InvoiceFilter(FilterSet):
|
class InvoiceFilter(FilterSet):
|
||||||
refers = django_filters.CharFilter(method='refers_qs')
|
refers = django_filters.CharFilter(method='refers_qs')
|
||||||
number = django_filters.CharFilter(method='nr_qs')
|
number = MultipleCharFilter(field_name='nr', lookup_expr='iexact')
|
||||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
order = MultipleCharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
|
|
||||||
def refers_qs(self, queryset, name, value):
|
def refers_qs(self, queryset, name, value):
|
||||||
return queryset.annotate(
|
return queryset.annotate(
|
||||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||||
).filter(refers_nr__iexact=value)
|
).filter(refers_nr__iexact=value)
|
||||||
|
|
||||||
def nr_qs(self, queryset, name, value):
|
|
||||||
return queryset.filter(nr__iexact=value)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Invoice
|
model = Invoice
|
||||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ from decimal import Decimal
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import OuterRef, Subquery, Sum
|
from django.db.models import OuterRef, Q, Subquery, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import mixins, serializers, status, views, viewsets
|
from rest_framework import mixins, serializers, status, views, viewsets
|
||||||
@@ -43,13 +44,13 @@ from pretix.api.serializers.organizer import (
|
|||||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||||
TeamMemberSerializer, TeamSerializer,
|
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||||
User,
|
TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
@@ -136,11 +137,19 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class GiftCardFilter(FilterSet):
|
class GiftCardFilter(FilterSet):
|
||||||
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||||
|
expired = django_filters.BooleanFilter(method='expired_qs')
|
||||||
|
value = django_filters.NumberFilter(field_name='cached_value')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
fields = ['secret', 'testmode']
|
fields = ['secret', 'testmode']
|
||||||
|
|
||||||
|
def expired_qs(self, qs, name, value):
|
||||||
|
if value:
|
||||||
|
return qs.filter(expires__isnull=False, expires__lt=now())
|
||||||
|
else:
|
||||||
|
return qs.filter(Q(expires__isnull=True) | Q(expires__gte=now()))
|
||||||
|
|
||||||
|
|
||||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = GiftCardSerializer
|
serializer_class = GiftCardSerializer
|
||||||
@@ -675,3 +684,68 @@ class MembershipViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data,
|
data=self.request.data,
|
||||||
)
|
)
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
class SalesChannelFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = SalesChannel
|
||||||
|
fields = ['type', 'identifier']
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = SalesChannelSerializer
|
||||||
|
queryset = SalesChannel.objects.none()
|
||||||
|
permission = 'can_change_organizer_settings'
|
||||||
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
filterset_class = SalesChannelFilter
|
||||||
|
lookup_field = 'identifier'
|
||||||
|
lookup_url_kwarg = 'identifier'
|
||||||
|
lookup_value_regex = r"[a-zA-Z0-9.\-_]+"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.request.organizer.sales_channels.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
inst = serializer.save(
|
||||||
|
organizer=self.request.organizer,
|
||||||
|
type="api"
|
||||||
|
)
|
||||||
|
inst.log_action(
|
||||||
|
'pretix.saleschannel.created',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
inst = serializer.save(
|
||||||
|
type=serializer.instance.type,
|
||||||
|
identifier=serializer.instance.identifier,
|
||||||
|
)
|
||||||
|
inst.log_action(
|
||||||
|
'pretix.sales_channel.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data,
|
||||||
|
)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
if not instance.allow_delete():
|
||||||
|
raise PermissionDenied("Can only be deleted if unused.")
|
||||||
|
instance.log_action(
|
||||||
|
'pretix.saleschannel.deleted',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={'id': instance.pk}
|
||||||
|
)
|
||||||
|
instance.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
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import django_filters
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
from pretix.api.models import WebHook
|
from pretix.api.models import WebHook
|
||||||
@@ -26,11 +28,17 @@ from pretix.api.serializers.webhooks import WebHookSerializer
|
|||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookFilter(FilterSet):
|
||||||
|
enabled = django_filters.rest_framework.BooleanFilter()
|
||||||
|
|
||||||
|
|
||||||
class WebHookViewSet(viewsets.ModelViewSet):
|
class WebHookViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = WebHookSerializer
|
serializer_class = WebHookSerializer
|
||||||
queryset = WebHook.objects.none()
|
queryset = WebHook.objects.none()
|
||||||
permission = 'can_change_organizer_settings'
|
permission = 'can_change_organizer_settings'
|
||||||
write_permission = 'can_change_organizer_settings'
|
write_permission = 'can_change_organizer_settings'
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
filterset_class = WebhookFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.organizer.webhooks.prefetch_related('listeners')
|
return self.request.organizer.webhooks.prefetch_related('listeners')
|
||||||
|
|||||||
@@ -126,6 +126,17 @@ class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeletedOrderWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
def build_payload(self, logentry: LogEntry):
|
||||||
|
return {
|
||||||
|
'notification_id': logentry.pk,
|
||||||
|
'organizer': logentry.organizer.slug,
|
||||||
|
'event': logentry.event.slug,
|
||||||
|
'code': logentry.parsed_data.get("code"),
|
||||||
|
'action': logentry.action_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
@@ -297,6 +308,10 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.order.denied',
|
'pretix.event.order.denied',
|
||||||
_('Order denied'),
|
_('Order denied'),
|
||||||
),
|
),
|
||||||
|
DeletedOrderWebhookEvent(
|
||||||
|
'pretix.event.order.deleted',
|
||||||
|
_('Order deleted'),
|
||||||
|
),
|
||||||
ParametrizedOrderPositionCheckinWebhookEvent(
|
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||||
'pretix.event.checkin',
|
'pretix.event.checkin',
|
||||||
_('Ticket checked in'),
|
_('Ticket checked in'),
|
||||||
|
|||||||
+85
-25
@@ -20,56 +20,83 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.signals import register_sales_channels
|
from pretix.base.signals import (
|
||||||
|
register_sales_channel_types, register_sales_channels,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_ALL_CHANNELS = None
|
_ALL_CHANNEL_TYPES = None
|
||||||
|
|
||||||
|
|
||||||
class SalesChannel:
|
class SalesChannelType:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<SalesChannel: {}>'.format(self.identifier)
|
return '<SalesChannelType: {}>'.format(self.identifier)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
"""
|
"""
|
||||||
The internal identifier of this sales channel.
|
The internal identifier of this sales channel type.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def verbose_name(self) -> str:
|
def verbose_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
A human-readable name of this sales channel.
|
A human-readable name of this sales channel type.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""
|
||||||
|
A human-readable description of this sales channel type.
|
||||||
|
"""
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str:
|
def icon(self) -> str:
|
||||||
"""
|
"""
|
||||||
The name of a Font Awesome icon to represent this channel
|
This can be:
|
||||||
|
|
||||||
|
- The name of a Font Awesome icon to represent this channel type.
|
||||||
|
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||||
"""
|
"""
|
||||||
return "circle"
|
return "circle"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_created(self) -> bool:
|
||||||
|
"""
|
||||||
|
Indication, if a sales channel of this type should automatically be created for every organizer
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def multiple_allowed(self) -> bool:
|
||||||
|
"""
|
||||||
|
Indication, if multiple sales channels of this type may exist in the same organizer
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def testmode_supported(self) -> bool:
|
def testmode_supported(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Indication, if a saleschannels supports test mode orders
|
Indication, if a sales channel of this type supports test mode orders
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def payment_restrictions_supported(self) -> bool:
|
def payment_restrictions_supported(self) -> bool:
|
||||||
"""
|
"""
|
||||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
|
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel type.
|
||||||
|
|
||||||
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
|
Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
|
||||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
handles payments locally. Therefore, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||||
the event organizer cannot restrict the usage of any payment provider through the backend.
|
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
@@ -77,8 +104,8 @@ class SalesChannel:
|
|||||||
@property
|
@property
|
||||||
def unlimited_items_per_order(self) -> bool:
|
def unlimited_items_per_order(self) -> bool:
|
||||||
"""
|
"""
|
||||||
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
|
If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
|
||||||
items defined in the event settings.
|
amount of items defined in the event settings.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -96,34 +123,67 @@ class SalesChannel:
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_event_plugin(self) -> str:
|
||||||
|
"""
|
||||||
|
Name of an event plugin that is required for this sales channel to be useful. Defaults to ``None``.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
def get_all_sales_channels():
|
|
||||||
global _ALL_CHANNELS
|
|
||||||
|
|
||||||
if _ALL_CHANNELS:
|
def get_all_sales_channel_types():
|
||||||
return _ALL_CHANNELS
|
from pretix.base.signals import register_sales_channel_types
|
||||||
|
global _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
|
if _ALL_CHANNEL_TYPES:
|
||||||
|
return _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
channels = []
|
channels = []
|
||||||
for recv, ret in register_sales_channels.send(None):
|
for recv, ret in register_sales_channel_types.send(None):
|
||||||
|
if isinstance(ret, (list, tuple)):
|
||||||
|
channels += ret
|
||||||
|
else:
|
||||||
|
channels.append(ret)
|
||||||
|
for recv, ret in register_sales_channels.send(None): # todo: remove me
|
||||||
if isinstance(ret, (list, tuple)):
|
if isinstance(ret, (list, tuple)):
|
||||||
channels += ret
|
channels += ret
|
||||||
else:
|
else:
|
||||||
channels.append(ret)
|
channels.append(ret)
|
||||||
channels.sort(key=lambda c: c.identifier)
|
channels.sort(key=lambda c: c.identifier)
|
||||||
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
|
_ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
|
||||||
if 'web' in _ALL_CHANNELS:
|
if 'web' in _ALL_CHANNEL_TYPES:
|
||||||
_ALL_CHANNELS.move_to_end('web', last=False)
|
_ALL_CHANNEL_TYPES.move_to_end('web', last=False)
|
||||||
return _ALL_CHANNELS
|
return _ALL_CHANNEL_TYPES
|
||||||
|
|
||||||
|
|
||||||
class WebshopSalesChannel(SalesChannel):
|
def get_all_sales_channels():
|
||||||
|
# TODO: remove me
|
||||||
|
warnings.warn('Using get_all_sales_channels() is no longer appropriate, use get_al_sales_channel_types() instead.',
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
return get_all_sales_channel_types()
|
||||||
|
|
||||||
|
|
||||||
|
class WebshopSalesChannelType(SalesChannelType):
|
||||||
identifier = "web"
|
identifier = "web"
|
||||||
verbose_name = _('Online shop')
|
verbose_name = _('Online shop')
|
||||||
icon = "globe"
|
icon = "globe"
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
|
class ApiSalesChannelType(SalesChannelType):
|
||||||
|
identifier = "api"
|
||||||
|
verbose_name = _('API')
|
||||||
|
description = _('API sales channels come with no built-in functionality, but may be used for custom integrations.')
|
||||||
|
icon = "exchange"
|
||||||
|
default_created = False
|
||||||
|
multiple_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
SalesChannel = SalesChannelType # TODO: remove me
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_sales_channel_types, dispatch_uid="base_register_default_sales_channel_types")
|
||||||
def base_sales_channels(sender, **kwargs):
|
def base_sales_channels(sender, **kwargs):
|
||||||
return (
|
return (
|
||||||
WebshopSalesChannel(),
|
WebshopSalesChannelType(),
|
||||||
|
ApiSalesChannelType(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -207,10 +207,13 @@ class ListExporter(BaseExporter):
|
|||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
return 'export'
|
return 'export'
|
||||||
|
|
||||||
|
def get_csv_encoding(self):
|
||||||
|
return 'utf-8'
|
||||||
|
|
||||||
def _render_csv(self, form_data, output_file=None, **kwargs):
|
def _render_csv(self, form_data, output_file=None, **kwargs):
|
||||||
if output_file:
|
if output_file:
|
||||||
if 'b' in output_file.mode:
|
if 'b' in output_file.mode:
|
||||||
output_file = io.TextIOWrapper(output_file, encoding='utf-8', newline='')
|
output_file = io.TextIOWrapper(output_file, encoding=self.get_csv_encoding(), errors='replace', newline='')
|
||||||
writer = csv.writer(output_file, **kwargs)
|
writer = csv.writer(output_file, **kwargs)
|
||||||
total = 0
|
total = 0
|
||||||
counter = 0
|
counter = 0
|
||||||
@@ -246,7 +249,7 @@ class ListExporter(BaseExporter):
|
|||||||
if counter % max(10, total // 100) == 0:
|
if counter % max(10, total // 100) == 0:
|
||||||
self.progress_callback(counter / total * 100)
|
self.progress_callback(counter / total * 100)
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode(self.get_csv_encoding(), errors='replace')
|
||||||
|
|
||||||
def prepare_xlsx_sheet(self, ws):
|
def prepare_xlsx_sheet(self, ws):
|
||||||
pass
|
pass
|
||||||
@@ -256,7 +259,7 @@ class ListExporter(BaseExporter):
|
|||||||
ws = wb.create_sheet()
|
ws = wb.create_sheet()
|
||||||
self.prepare_xlsx_sheet(ws)
|
self.prepare_xlsx_sheet(ws)
|
||||||
try:
|
try:
|
||||||
ws.title = str(self.verbose_name)
|
ws.title = str(self.verbose_name)[:30]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
total = 0
|
total = 0
|
||||||
@@ -374,7 +377,7 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
wb = SafeWorkbook(write_only=True)
|
wb = SafeWorkbook(write_only=True)
|
||||||
n_sheets = len(self.sheets)
|
n_sheets = len(self.sheets)
|
||||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||||
ws = wb.create_sheet(str(l))
|
ws = wb.create_sheet(str(l)[:30])
|
||||||
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
||||||
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ from openpyxl.styles import Alignment
|
|||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
from ...helpers.safe_openpyxl import SafeCell
|
from ...helpers.safe_openpyxl import SafeCell
|
||||||
from ..channels import get_all_sales_channels
|
|
||||||
from ..exporter import ListExporter
|
from ..exporter import ListExporter
|
||||||
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
||||||
from ..signals import register_data_exporters
|
from ..signals import register_data_exporters
|
||||||
@@ -53,7 +52,7 @@ class ItemDataExporter(ListExporter):
|
|||||||
|
|
||||||
def iterate_list(self, form_data):
|
def iterate_list(self, form_data):
|
||||||
locales = self.event.settings.locales
|
locales = self.event.settings.locales
|
||||||
scs = get_all_sales_channels()
|
scs = self.organizer.sales_channels.all()
|
||||||
header = [
|
header = [
|
||||||
_("Product ID"),
|
_("Product ID"),
|
||||||
_("Variation ID"),
|
_("Variation ID"),
|
||||||
@@ -141,9 +140,15 @@ class ItemDataExporter(ListExporter):
|
|||||||
row.append(i.name.localize(l))
|
row.append(i.name.localize(l))
|
||||||
for l in locales:
|
for l in locales:
|
||||||
row.append(v.value.localize(l))
|
row.append(v.value.localize(l))
|
||||||
|
|
||||||
|
sales_channels = list(scs)
|
||||||
|
if not i.all_sales_channels:
|
||||||
|
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||||
|
if not v.all_sales_channels:
|
||||||
|
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in v.limit_sales_channels.all())]
|
||||||
row += [
|
row += [
|
||||||
_("Yes") if i.active and v.active else "",
|
_("Yes") if i.active and v.active else "",
|
||||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
|
", ".join([str(sn.label) for sn in sales_channels]),
|
||||||
v.default_price or i.default_price,
|
v.default_price or i.default_price,
|
||||||
_("Yes") if i.free_price else "",
|
_("Yes") if i.free_price else "",
|
||||||
str(i.tax_rule) if i.tax_rule else "",
|
str(i.tax_rule) if i.tax_rule else "",
|
||||||
@@ -186,9 +191,12 @@ class ItemDataExporter(ListExporter):
|
|||||||
row.append(i.name.localize(l))
|
row.append(i.name.localize(l))
|
||||||
for l in locales:
|
for l in locales:
|
||||||
row.append("")
|
row.append("")
|
||||||
|
sales_channels = list(scs)
|
||||||
|
if not i.all_sales_channels:
|
||||||
|
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||||
row += [
|
row += [
|
||||||
_("Yes") if i.active else "",
|
_("Yes") if i.active else "",
|
||||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
|
", ".join([str(sn.label) for sn in sales_channels]),
|
||||||
i.default_price,
|
i.default_price,
|
||||||
_("Yes") if i.free_price else "",
|
_("Yes") if i.free_price else "",
|
||||||
str(i.tax_rule) if i.tax_rule else "",
|
str(i.tax_rule) if i.tax_rule else "",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'import in third-party systems.')
|
'import in third-party systems.')
|
||||||
|
|
||||||
def render(self, form_data):
|
def render(self, form_data):
|
||||||
|
all_sales_channels = self.event.organizer.sales_channels.all()
|
||||||
jo = {
|
jo = {
|
||||||
'event': {
|
'event': {
|
||||||
'name': str(self.event.name),
|
'name': str(self.event.name),
|
||||||
@@ -85,7 +86,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'admission': item.admission,
|
'admission': item.admission,
|
||||||
'personalized': item.personalized,
|
'personalized': item.personalized,
|
||||||
'active': item.active,
|
'active': item.active,
|
||||||
'sales_channels': item.sales_channels,
|
'sales_channels': [c.identifier for c in (all_sales_channels if item.all_sales_channels else item.limit_sales_channels.all())],
|
||||||
'description': str(item.description),
|
'description': str(item.description),
|
||||||
'available_from': item.available_from,
|
'available_from': item.available_from,
|
||||||
'available_until': item.available_until,
|
'available_until': item.available_until,
|
||||||
@@ -114,7 +115,9 @@ class JSONExporter(BaseExporter):
|
|||||||
'checkin_text': variation.checkin_text,
|
'checkin_text': variation.checkin_text,
|
||||||
'require_approval': variation.require_approval,
|
'require_approval': variation.require_approval,
|
||||||
'require_membership': variation.require_membership,
|
'require_membership': variation.require_membership,
|
||||||
'sales_channels': variation.sales_channels,
|
'sales_channels': [
|
||||||
|
c.identifier for c in (all_sales_channels if variation.all_sales_channels else variation.limit_sales_channels.all())
|
||||||
|
],
|
||||||
'available_from': variation.available_from,
|
'available_from': variation.available_from,
|
||||||
'available_until': variation.available_until,
|
'available_until': variation.available_until,
|
||||||
'hide_without_voucher': variation.hide_without_voucher,
|
'hide_without_voucher': variation.hide_without_voucher,
|
||||||
@@ -122,6 +125,7 @@ class JSONExporter(BaseExporter):
|
|||||||
} for variation in item.variations.all()
|
} for variation in item.variations.all()
|
||||||
]
|
]
|
||||||
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
ItemMetaValue.objects.select_related('property'),
|
ItemMetaValue.objects.select_related('property'),
|
||||||
@@ -130,6 +134,7 @@ class JSONExporter(BaseExporter):
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'variations',
|
'variations',
|
||||||
queryset=ItemVariation.objects.prefetch_related(
|
queryset=ItemVariation.objects.prefetch_related(
|
||||||
|
'limit_sales_channels',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'meta_values',
|
'meta_values',
|
||||||
ItemVariationMetaValue.objects.select_related('property'),
|
ItemVariationMetaValue.objects.select_related('property'),
|
||||||
@@ -167,7 +172,7 @@ class JSONExporter(BaseExporter):
|
|||||||
'require_approval': order.require_approval,
|
'require_approval': order.require_approval,
|
||||||
'checkin_attention': order.checkin_attention,
|
'checkin_attention': order.checkin_attention,
|
||||||
'checkin_text': order.checkin_text,
|
'checkin_text': order.checkin_text,
|
||||||
'sales_channel': order.sales_channel,
|
'sales_channel': order.sales_channel.identifier,
|
||||||
'expires': order.expires,
|
'expires': order.expires,
|
||||||
'datetime': order.datetime,
|
'datetime': order.datetime,
|
||||||
'fees': [
|
'fees': [
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
),
|
),
|
||||||
).select_related(
|
).select_related(
|
||||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||||
'voucher', 'tax_rule'
|
'voucher', 'tax_rule', 'addon_to',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'subevent', 'subevent__meta_values',
|
'subevent', 'subevent__meta_values',
|
||||||
'answers', 'answers__question', 'answers__options'
|
'answers', 'answers__question', 'answers__options'
|
||||||
@@ -619,6 +619,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('Valid until'),
|
_('Valid until'),
|
||||||
_('Order comment'),
|
_('Order comment'),
|
||||||
_('Follow-up date'),
|
_('Follow-up date'),
|
||||||
|
_('Add-on to position ID'),
|
||||||
]
|
]
|
||||||
|
|
||||||
questions = list(Question.objects.filter(event__in=self.events))
|
questions = list(Question.objects.filter(event__in=self.events))
|
||||||
@@ -652,7 +653,8 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('VAT ID'),
|
_('VAT ID'),
|
||||||
]
|
]
|
||||||
headers += [
|
headers += [
|
||||||
_('Sales channel'), _('Order locale'),
|
_('Sales channel'),
|
||||||
|
_('Order locale'),
|
||||||
_('E-mail address verified'),
|
_('E-mail address verified'),
|
||||||
_('External customer ID'),
|
_('External customer ID'),
|
||||||
_('Check-in lists'),
|
_('Check-in lists'),
|
||||||
@@ -743,6 +745,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]
|
]
|
||||||
row.append(order.comment)
|
row.append(order.comment)
|
||||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||||
|
row.append(op.addon_to.positionid if op.addon_to_id else "")
|
||||||
acache = {}
|
acache = {}
|
||||||
for a in op.answers.all():
|
for a in op.answers.all():
|
||||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from datetime import datetime
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.formats import get_format
|
from django.utils.formats import get_format
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import get_current_timezone, now
|
from django.utils.timezone import get_current_timezone, now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ def format_placeholders_help_text(placeholders, event=None):
|
|||||||
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
||||||
placeholders.sort(key=lambda x: x[0])
|
placeholders.sort(key=lambda x: x[0])
|
||||||
phs = [
|
phs = [
|
||||||
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (_("Sample: %s") % v if v else "", k)
|
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
|
||||||
for k, v in placeholders
|
for k, v in placeholders
|
||||||
]
|
]
|
||||||
return _('Available placeholders: {list}').format(
|
return _('Available placeholders: {list}').format(
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from typing import Tuple
|
|||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
import vat_moss.exchange_rates
|
import vat_moss.exchange_rates
|
||||||
from bidi.algorithm import get_display
|
from bidi import get_display
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|||||||
@@ -256,8 +256,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
# pages
|
# pages
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
resp['X-XSS-Protection'] = '1'
|
|
||||||
|
|
||||||
# We just need to have a P3P, not matter whats in there
|
# We just need to have a P3P, not matter whats in there
|
||||||
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
||||||
# https://github.com/pretix/pretix/issues/765
|
# https://github.com/pretix/pretix/issues/765
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channel_types
|
||||||
|
|
||||||
|
|
||||||
def set_sales_channels(apps, schema_editor):
|
def set_sales_channels(apps, schema_editor):
|
||||||
@@ -11,7 +11,7 @@ def set_sales_channels(apps, schema_editor):
|
|||||||
# Therefore, for existing events, we enable all sales channels
|
# Therefore, for existing events, we enable all sales channels
|
||||||
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||||
Event = apps.get_model('pretixbase', 'Event')
|
Event = apps.get_model('pretixbase', 'Event')
|
||||||
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
|
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channel_types()) + "]"
|
||||||
batch_size = 1000
|
batch_size = 1000
|
||||||
Event_SettingsStore.objects.bulk_create([
|
Event_SettingsStore.objects.bulk_create([
|
||||||
Event_SettingsStore(
|
Event_SettingsStore(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
import pretix.base.models.fields
|
import pretix.base.models.fields
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channel_types
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
name='sales_channels',
|
name='sales_channels',
|
||||||
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
|
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channel_types().keys())),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-03-24 17:43
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import i18nfield.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0264_order_internal_secret"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SalesChannel",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("label", i18nfield.fields.I18nCharField(max_length=200)),
|
||||||
|
("identifier", models.CharField(max_length=200)),
|
||||||
|
("type", models.CharField(max_length=200)),
|
||||||
|
("position", models.PositiveIntegerField(default=0)),
|
||||||
|
("configuration", models.JSONField(default=dict)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="checkinlist",
|
||||||
|
old_name="auto_checkin_sales_channels",
|
||||||
|
new_name="auto_checkin_sales_channel_types",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="discount",
|
||||||
|
name="all_sales_channels",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="event",
|
||||||
|
name="all_sales_channels",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="item",
|
||||||
|
name="all_sales_channels",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="itemvariation",
|
||||||
|
name="all_sales_channels",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="order",
|
||||||
|
old_name="sales_channel",
|
||||||
|
new_name="sales_channel_type",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="saleschannel",
|
||||||
|
name="organizer",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="sales_channels",
|
||||||
|
to="pretixbase.organizer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="discount",
|
||||||
|
name="limit_sales_channels",
|
||||||
|
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="event",
|
||||||
|
name="limit_sales_channels",
|
||||||
|
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="item",
|
||||||
|
name="limit_sales_channels",
|
||||||
|
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="itemvariation",
|
||||||
|
name="limit_sales_channels",
|
||||||
|
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="checkinlist",
|
||||||
|
name="auto_checkin_sales_channels",
|
||||||
|
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="order",
|
||||||
|
name="sales_channel",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="pretixbase.saleschannel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="saleschannel",
|
||||||
|
unique_together={("organizer", "identifier")},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-03-24 17:55
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.base.channels import get_all_sales_channel_types
|
||||||
|
|
||||||
|
|
||||||
|
def create_sales_channels(apps, schema_editor):
|
||||||
|
channel_types = get_all_sales_channel_types()
|
||||||
|
type_to_channel = dict()
|
||||||
|
full_discount_set = set()
|
||||||
|
full_set = set()
|
||||||
|
|
||||||
|
Organizer = apps.get_model("pretixbase", "Organizer")
|
||||||
|
for o in Organizer.objects.all().iterator():
|
||||||
|
for i, t in enumerate(channel_types.values()):
|
||||||
|
if not t.default_created:
|
||||||
|
continue
|
||||||
|
type_to_channel[t.identifier, o.pk] = o.sales_channels.get_or_create(
|
||||||
|
type=t.identifier,
|
||||||
|
defaults=dict(
|
||||||
|
position=i,
|
||||||
|
identifier=t.identifier,
|
||||||
|
label=LazyI18nString.from_gettext(t.verbose_name),
|
||||||
|
),
|
||||||
|
)[0]
|
||||||
|
full_set.add(t.identifier)
|
||||||
|
if t.discounts_supported:
|
||||||
|
full_discount_set.add(t.identifier)
|
||||||
|
|
||||||
|
Event = apps.get_model("pretixbase", "Event")
|
||||||
|
for d in Event.objects.all().iterator():
|
||||||
|
if set(d.sales_channels) != full_set:
|
||||||
|
d.all_sales_channels = False
|
||||||
|
d.save()
|
||||||
|
for s in d.sales_channels:
|
||||||
|
d.limit_sales_channels.add(type_to_channel[s, d.organizer_id])
|
||||||
|
|
||||||
|
Item = apps.get_model("pretixbase", "Item")
|
||||||
|
for d in Item.objects.select_related("event").iterator():
|
||||||
|
if set(d.sales_channels) != full_set:
|
||||||
|
d.all_sales_channels = False
|
||||||
|
d.save()
|
||||||
|
for s in d.sales_channels:
|
||||||
|
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
|
||||||
|
|
||||||
|
ItemVariation = apps.get_model("pretixbase", "ItemVariation")
|
||||||
|
for d in ItemVariation.objects.select_related("item__event").iterator():
|
||||||
|
if set(d.sales_channels) != full_set:
|
||||||
|
d.all_sales_channels = False
|
||||||
|
d.save()
|
||||||
|
for s in d.sales_channels:
|
||||||
|
d.limit_sales_channels.add(type_to_channel[s, d.item.event.organizer_id])
|
||||||
|
|
||||||
|
Discount = apps.get_model("pretixbase", "Discount")
|
||||||
|
for d in Discount.objects.select_related("event").iterator():
|
||||||
|
if set(d.sales_channels) != full_discount_set:
|
||||||
|
d.all_sales_channels = False
|
||||||
|
d.save()
|
||||||
|
for s in d.sales_channels:
|
||||||
|
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
|
||||||
|
|
||||||
|
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||||
|
for c in CheckinList.objects.select_related("event").iterator():
|
||||||
|
for s in c.auto_checkin_sales_channel_types:
|
||||||
|
c.auto_checkin_sales_channels.add(type_to_channel[s, c.event.organizer_id])
|
||||||
|
|
||||||
|
Order = apps.get_model("pretixbase", "Order")
|
||||||
|
for (k, orgid), v in type_to_channel.items():
|
||||||
|
Order.objects.filter(sales_channel_type=k, event__organizer_id=orgid, sales_channel__isnull=True).update(
|
||||||
|
sales_channel=v
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0265_saleschannel_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_sales_channels, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-03-25 13:34
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0266_saleschannel_migrate_data"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="checkinlist",
|
||||||
|
name="auto_checkin_sales_channel_types",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="discount",
|
||||||
|
name="sales_channels",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="event",
|
||||||
|
name="sales_channels",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="item",
|
||||||
|
name="sales_channels",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="itemvariation",
|
||||||
|
name="sales_channels",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="order",
|
||||||
|
name="sales_channel_type",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="order",
|
||||||
|
name="sales_channel",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="pretixbase.saleschannel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-07-01 09:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.orders
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0267_remove_old_sales_channels"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="subevent",
|
||||||
|
name="items",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="subevent",
|
||||||
|
name="variations",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="order",
|
||||||
|
name="internal_secret",
|
||||||
|
field=models.CharField(
|
||||||
|
default=pretix.base.models.orders.generate_secret,
|
||||||
|
max_length=32,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.13 on 2024-07-17 14:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0268_remove_subevent_items_remove_subevent_variations_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='api_meta',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -38,7 +38,6 @@ from i18nfield.strings import LazyI18nString
|
|||||||
from phonenumber_field.phonenumber import to_python
|
from phonenumber_field.phonenumber import to_python
|
||||||
from phonenumbers import SUPPORTED_REGIONS
|
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.forms.questions import guess_country
|
||||||
from pretix.base.modelimport import (
|
from pretix.base.modelimport import (
|
||||||
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
|
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
|
||||||
@@ -538,18 +537,28 @@ class Expires(DatetimeColumnMixin, ImportColumn):
|
|||||||
class Saleschannel(ImportColumn):
|
class Saleschannel(ImportColumn):
|
||||||
identifier = 'sales_channel'
|
identifier = 'sales_channel'
|
||||||
verbose_name = gettext_lazy('Sales channel')
|
verbose_name = gettext_lazy('Sales channel')
|
||||||
|
default_value = None
|
||||||
|
initial = 'static:web'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def channels(self):
|
||||||
|
return list(self.event.organizer.sales_channels.all())
|
||||||
|
|
||||||
def static_choices(self):
|
def static_choices(self):
|
||||||
return [
|
return [
|
||||||
(sc.identifier, sc.verbose_name) for sc in get_all_sales_channels().values()
|
(c.identifier, str(c.label)) for c in self.channels
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if not value:
|
matches = [
|
||||||
value = 'web'
|
p for p in self.channels
|
||||||
if value not in get_all_sales_channels():
|
if p.identifier == value or any((v and v == value) for v in i18n_flat(p.label))
|
||||||
|
]
|
||||||
|
if len(matches) == 0:
|
||||||
raise ValidationError(_("Please enter a valid sales channel."))
|
raise ValidationError(_("Please enter a valid sales channel."))
|
||||||
return value
|
if len(matches) > 1:
|
||||||
|
raise ValidationError(_("Please enter a valid sales channel."))
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
order.sales_channel = value
|
order.sales_channel = value
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
|
|||||||
|
|
||||||
from pretix.base.modelimport import (
|
from pretix.base.modelimport import (
|
||||||
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
|
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
|
||||||
IntegerColumnMixin, i18n_flat,
|
IntegerColumnMixin, SubeventColumnMixin, i18n_flat,
|
||||||
)
|
)
|
||||||
from pretix.base.models import ItemVariation, Quota, Seat, Voucher
|
from pretix.base.models import ItemVariation, Quota, Seat, SubEvent, Voucher
|
||||||
from pretix.base.signals import voucher_import_columns
|
from pretix.base.signals import voucher_import_columns
|
||||||
|
|
||||||
|
|
||||||
@@ -55,11 +55,11 @@ class CodeColumn(ImportColumn):
|
|||||||
obj.code = value
|
obj.code = value
|
||||||
|
|
||||||
|
|
||||||
class SubeventColumn(ImportColumn):
|
class SubeventColumn(SubeventColumnMixin, ImportColumn):
|
||||||
identifier = 'subevent'
|
identifier = 'subevent'
|
||||||
verbose_name = pgettext_lazy('subevents', 'Date')
|
verbose_name = pgettext_lazy('subevents', 'Date')
|
||||||
|
|
||||||
def assign(self, value, obj: Voucher, **kwargs):
|
def assign(self, value, obj: SubEvent, **kwargs):
|
||||||
obj.subevent = value
|
obj.subevent = value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ from .orders import (
|
|||||||
generate_secret,
|
generate_secret,
|
||||||
)
|
)
|
||||||
from .organizer import (
|
from .organizer import (
|
||||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
Organizer, Organizer_SettingsStore, SalesChannel, Team, TeamAPIToken,
|
||||||
|
TeamInvite,
|
||||||
)
|
)
|
||||||
from .seating import Seat, SeatCategoryMapping, SeatingPlan
|
from .seating import Seat, SeatCategoryMapping, SeatingPlan
|
||||||
from .tax import TaxRule
|
from .tax import TaxRule
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ from django_scopes import ScopedManager, scopes_disabled
|
|||||||
|
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
from pretix.base.models.fields import MultiStringField
|
|
||||||
from pretix.helpers import PostgresWindowFrame
|
from pretix.helpers import PostgresWindowFrame
|
||||||
|
|
||||||
|
|
||||||
@@ -100,13 +99,13 @@ class CheckinList(LoggedModel):
|
|||||||
verbose_name=_('Automatically check out everyone at'),
|
verbose_name=_('Automatically check out everyone at'),
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
auto_checkin_sales_channels = MultiStringField(
|
auto_checkin_sales_channels = models.ManyToManyField(
|
||||||
default=[],
|
"SalesChannel",
|
||||||
blank=True,
|
|
||||||
verbose_name=_('Sales channels to automatically check in'),
|
verbose_name=_('Sales channels to automatically check in'),
|
||||||
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
|
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
|
||||||
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
|
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
|
||||||
'are not checked again before entry and should be considered validated directly upon purchase.')
|
'to use the new plugin.'),
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
rules = models.JSONField(default=dict, blank=True)
|
rules = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models import fields
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
|
|
||||||
|
|
||||||
@@ -65,10 +64,14 @@ class Discount(LoggedModel):
|
|||||||
default=0,
|
default=0,
|
||||||
verbose_name=_("Position")
|
verbose_name=_("Position")
|
||||||
)
|
)
|
||||||
sales_channels = fields.MultiStringField(
|
all_sales_channels = models.BooleanField(
|
||||||
verbose_name=_('Sales channels'),
|
verbose_name=_("All supported sales channels"),
|
||||||
default=['web'],
|
default=True,
|
||||||
blank=False,
|
)
|
||||||
|
limit_sales_channels = models.ManyToManyField(
|
||||||
|
"SalesChannel",
|
||||||
|
verbose_name=_("Sales channels"),
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
available_from = models.DateTimeField(
|
available_from = models.DateTimeField(
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
from collections import Counter, OrderedDict, defaultdict
|
from collections import Counter, OrderedDict, defaultdict
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
@@ -59,14 +60,12 @@ from django.urls import reverse
|
|||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.fields import MultiStringField
|
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
from pretix.base.timemachine import time_machine_now
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.base.validators import EventSlugBanlistValidator
|
from pretix.base.validators import EventSlugBanlistValidator
|
||||||
@@ -180,14 +179,10 @@ class EventMixin:
|
|||||||
"""
|
"""
|
||||||
tz = tz or self.timezone
|
tz = tz or self.timezone
|
||||||
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
|
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
|
||||||
if as_html:
|
df, dt = self.date_from, self.date_from
|
||||||
return format_html(
|
else:
|
||||||
"<time datetime=\"{}\">{}</time>",
|
df, dt = self.date_from, self.date_to
|
||||||
_date(self.date_from.astimezone(tz), "Y-m-d"),
|
return daterange(df.astimezone(tz), dt.astimezone(tz), as_html)
|
||||||
_date(self.date_from.astimezone(tz), "DATE_FORMAT"),
|
|
||||||
)
|
|
||||||
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
|
|
||||||
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz), as_html)
|
|
||||||
|
|
||||||
def get_date_range_display_as_html(self, tz=None, force_show_end=False) -> str:
|
def get_date_range_display_as_html(self, tz=None, force_show_end=False) -> str:
|
||||||
return self.get_date_range_display(tz, force_show_end, as_html=True)
|
return self.get_date_range_display(tz, force_show_end, as_html=True)
|
||||||
@@ -304,8 +299,12 @@ class EventMixin:
|
|||||||
return safe_string(json.dumps(eventdict))
|
return safe_string(json.dumps(eventdict))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def annotated(cls, qs, channel='web', voucher=None):
|
def annotated(cls, qs, channel, voucher=None):
|
||||||
from pretix.base.models import Item, ItemVariation, Quota
|
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||||
|
# makes the query SIGNIFICANTLY faster
|
||||||
|
from pretix.base.models import Item, ItemVariation, Quota, SalesChannel
|
||||||
|
|
||||||
|
assert isinstance(channel, (SalesChannel, str))
|
||||||
|
|
||||||
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
||||||
Q(variations__isnull=True)
|
Q(variations__isnull=True)
|
||||||
@@ -316,18 +315,23 @@ class EventMixin:
|
|||||||
|
|
||||||
q_variation = (
|
q_variation = (
|
||||||
Q(active=True)
|
Q(active=True)
|
||||||
& Q(sales_channels__contains=channel)
|
|
||||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
|
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
|
||||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
|
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
|
||||||
& Q(item__active=True)
|
& Q(item__active=True)
|
||||||
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
|
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
|
||||||
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
|
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
|
||||||
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
||||||
& Q(item__sales_channels__contains=channel)
|
|
||||||
& Q(item__require_bundling=False)
|
& Q(item__require_bundling=False)
|
||||||
& Q(quotas__pk=OuterRef('pk'))
|
& Q(quotas__pk=OuterRef('pk'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if isinstance(channel, str):
|
||||||
|
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
||||||
|
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
|
||||||
|
else:
|
||||||
|
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
|
||||||
|
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels=channel))
|
||||||
|
|
||||||
if voucher:
|
if voucher:
|
||||||
if voucher.variation_id:
|
if voucher.variation_id:
|
||||||
q_variation &= Q(pk=voucher.variation_id)
|
q_variation &= Q(pk=voucher.variation_id)
|
||||||
@@ -467,6 +471,7 @@ class EventMixin:
|
|||||||
return best_state_found, num_tickets_found, num_tickets_possible
|
return best_state_found, num_tickets_found, num_tickets_possible
|
||||||
|
|
||||||
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||||
|
assert isinstance(sales_channel, str) or sales_channel is None
|
||||||
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
|
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
|
||||||
|
|
||||||
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
||||||
@@ -495,10 +500,13 @@ class EventMixin:
|
|||||||
return qs.filter(q)
|
return qs.filter(q)
|
||||||
|
|
||||||
|
|
||||||
def default_sales_channels():
|
def default_sales_channels(): # kept for legacy migration
|
||||||
from ..channels import get_all_sales_channels
|
from ..channels import get_all_sales_channel_types
|
||||||
|
|
||||||
return list(get_all_sales_channels().keys())
|
if "PYTEST_CURRENT_TEST" not in os.environ:
|
||||||
|
warnings.warn('Method should not be used in new code.', DeprecationWarning)
|
||||||
|
|
||||||
|
return list(get_all_sales_channel_types().keys())
|
||||||
|
|
||||||
|
|
||||||
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
|
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
|
||||||
@@ -535,8 +543,10 @@ class Event(EventMixin, LoggedModel):
|
|||||||
:type plugins: str
|
:type plugins: str
|
||||||
:param has_subevents: Enable event series functionality
|
:param has_subevents: Enable event series functionality
|
||||||
:type has_subevents: bool
|
:type has_subevents: bool
|
||||||
:param sales_channels: A list of sales channel identifiers, that this event is available for sale on
|
:param all_sales_channels: A flag indicating that this event is available on all channels and limit_sales_channels will be ignored.
|
||||||
:type sales_channels: list
|
:type all_sales_channels: bool
|
||||||
|
:param limit_sales_channels: A list of sales channel identifiers, that this event is available for sale on
|
||||||
|
:type limit_sales_channels: list
|
||||||
"""
|
"""
|
||||||
|
|
||||||
settings_namespace = 'event'
|
settings_namespace = 'event'
|
||||||
@@ -628,10 +638,14 @@ class Event(EventMixin, LoggedModel):
|
|||||||
auto_now=True, db_index=True
|
auto_now=True, db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
sales_channels = MultiStringField(
|
all_sales_channels = models.BooleanField(
|
||||||
verbose_name=_('Restrict to specific sales channels'),
|
verbose_name=_("Sell on all sales channels"),
|
||||||
help_text=_('Only sell tickets for this event on the following sales channels.'),
|
default=True,
|
||||||
default=default_sales_channels,
|
)
|
||||||
|
limit_sales_channels = models.ManyToManyField(
|
||||||
|
"SalesChannel",
|
||||||
|
verbose_name=_("Restrict to specific sales channels"),
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='organizer')
|
objects = ScopedManager(organizer='organizer')
|
||||||
@@ -805,10 +819,17 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if other.date_admission:
|
if other.date_admission:
|
||||||
self.date_admission = self.date_from + (other.date_admission - other.date_from)
|
self.date_admission = self.date_from + (other.date_admission - other.date_from)
|
||||||
self.testmode = other.testmode
|
self.testmode = other.testmode
|
||||||
self.sales_channels = other.sales_channels
|
self.all_sales_channels = other.all_sales_channels
|
||||||
self.save()
|
self.save()
|
||||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||||
|
|
||||||
|
if not self.all_sales_channels:
|
||||||
|
self.limit_sales_channels.set(
|
||||||
|
self.organizer.sales_channels.filter(
|
||||||
|
identifier__in=other.limit_sales_channels.values_list("identifier", flat=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not skip_meta_data:
|
if not skip_meta_data:
|
||||||
for emv in EventMetaValue.objects.filter(event=other):
|
for emv in EventMetaValue.objects.filter(event=other):
|
||||||
emv.pk = None
|
emv.pk = None
|
||||||
@@ -846,12 +867,17 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
item_map = {}
|
item_map = {}
|
||||||
variation_map = {}
|
variation_map = {}
|
||||||
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
for i in Item.objects.filter(event=other).prefetch_related(
|
||||||
|
'variations', 'limit_sales_channels', 'require_membership_types',
|
||||||
|
'variations__limit_sales_channels', 'variations__require_membership_types',
|
||||||
|
):
|
||||||
vars = list(i.variations.all())
|
vars = list(i.variations.all())
|
||||||
require_membership_types = list(i.require_membership_types.all())
|
require_membership_types = list(i.require_membership_types.all())
|
||||||
|
limit_sales_channels = list(i.limit_sales_channels.all())
|
||||||
item_map[i.pk] = i
|
item_map[i.pk] = i
|
||||||
i.pk = None
|
i.pk = None
|
||||||
i.event = self
|
i.event = self
|
||||||
|
i._prefetched_objects_cache = {}
|
||||||
if i.picture:
|
if i.picture:
|
||||||
i.picture.save(os.path.basename(i.picture.name), i.picture)
|
i.picture.save(os.path.basename(i.picture.name), i.picture)
|
||||||
if i.category_id:
|
if i.category_id:
|
||||||
@@ -868,12 +894,23 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||||
i.require_membership_types.set(require_membership_types)
|
i.require_membership_types.set(require_membership_types)
|
||||||
|
|
||||||
|
if not i.all_sales_channels:
|
||||||
|
i.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||||
|
|
||||||
for v in vars:
|
for v in vars:
|
||||||
|
require_membership_types = list(v.require_membership_types.all())
|
||||||
|
limit_sales_channels = list(v.limit_sales_channels.all())
|
||||||
variation_map[v.pk] = v
|
variation_map[v.pk] = v
|
||||||
v.pk = None
|
v.pk = None
|
||||||
v.item = i
|
v.item = i
|
||||||
|
v._prefetched_objects_cache = {}
|
||||||
v.save(force_insert=True)
|
v.save(force_insert=True)
|
||||||
|
|
||||||
|
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||||
|
v.require_membership_types.set(require_membership_types)
|
||||||
|
if not v.all_sales_channels:
|
||||||
|
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||||
|
|
||||||
for i in self.items.filter(hidden_if_item_available__isnull=False):
|
for i in self.items.filter(hidden_if_item_available__isnull=False):
|
||||||
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
|
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
|
||||||
i.save()
|
i.save()
|
||||||
@@ -911,6 +948,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
vars = list(q.variations.all())
|
vars = list(q.variations.all())
|
||||||
oldid = q.pk
|
oldid = q.pk
|
||||||
q.pk = None
|
q.pk = None
|
||||||
|
q._prefetched_objects_cache = {}
|
||||||
q.event = self
|
q.event = self
|
||||||
q.closed = False
|
q.closed = False
|
||||||
q.save(force_insert=True)
|
q.save(force_insert=True)
|
||||||
@@ -922,11 +960,15 @@ class Event(EventMixin, LoggedModel):
|
|||||||
q.variations.add(variation_map[v.pk])
|
q.variations.add(variation_map[v.pk])
|
||||||
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
|
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
|
||||||
|
|
||||||
for d in Discount.objects.filter(event=other).prefetch_related('condition_limit_products'):
|
for d in Discount.objects.filter(event=other).prefetch_related(
|
||||||
|
'condition_limit_products', 'benefit_limit_products', 'limit_sales_channels'
|
||||||
|
):
|
||||||
c_items = list(d.condition_limit_products.all())
|
c_items = list(d.condition_limit_products.all())
|
||||||
b_items = list(d.benefit_limit_products.all())
|
b_items = list(d.benefit_limit_products.all())
|
||||||
|
limit_sales_channels = list(d.limit_sales_channels.all())
|
||||||
d.pk = None
|
d.pk = None
|
||||||
d.event = self
|
d.event = self
|
||||||
|
d._prefetched_objects_cache = {}
|
||||||
d.save(force_insert=True)
|
d.save(force_insert=True)
|
||||||
d.log_action('pretix.object.cloned')
|
d.log_action('pretix.object.cloned')
|
||||||
for i in c_items:
|
for i in c_items:
|
||||||
@@ -936,12 +978,16 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if i.pk in item_map:
|
if i.pk in item_map:
|
||||||
d.benefit_limit_products.add(item_map[i.pk])
|
d.benefit_limit_products.add(item_map[i.pk])
|
||||||
|
|
||||||
|
if not d.all_sales_channels:
|
||||||
|
d.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||||
|
|
||||||
question_map = {}
|
question_map = {}
|
||||||
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
|
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
|
||||||
items = list(q.items.all())
|
items = list(q.items.all())
|
||||||
opts = list(q.options.all())
|
opts = list(q.options.all())
|
||||||
question_map[q.pk] = q
|
question_map[q.pk] = q
|
||||||
q.pk = None
|
q.pk = None
|
||||||
|
q._prefetched_objects_cache = {}
|
||||||
q.event = self
|
q.event = self
|
||||||
q.save(force_insert=True)
|
q.save(force_insert=True)
|
||||||
q.log_action('pretix.object.cloned')
|
q.log_action('pretix.object.cloned')
|
||||||
@@ -972,10 +1018,14 @@ class Event(EventMixin, LoggedModel):
|
|||||||
_walk_rules(i)
|
_walk_rules(i)
|
||||||
|
|
||||||
checkin_list_map = {}
|
checkin_list_map = {}
|
||||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
||||||
|
'limit_products', 'auto_checkin_sales_channels'
|
||||||
|
):
|
||||||
items = list(cl.limit_products.all())
|
items = list(cl.limit_products.all())
|
||||||
|
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
|
||||||
checkin_list_map[cl.pk] = cl
|
checkin_list_map[cl.pk] = cl
|
||||||
cl.pk = None
|
cl.pk = None
|
||||||
|
cl._prefetched_objects_cache = {}
|
||||||
cl.event = self
|
cl.event = self
|
||||||
rules = cl.rules
|
rules = cl.rules
|
||||||
_walk_rules(rules)
|
_walk_rules(rules)
|
||||||
@@ -984,6 +1034,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
cl.log_action('pretix.object.cloned')
|
cl.log_action('pretix.object.cloned')
|
||||||
for i in items:
|
for i in items:
|
||||||
cl.limit_products.add(item_map[i.pk])
|
cl.limit_products.add(item_map[i.pk])
|
||||||
|
if auto_checkin_sales_channels:
|
||||||
|
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
|
||||||
|
|
||||||
if other.seating_plan:
|
if other.seating_plan:
|
||||||
if other.seating_plan.organizer_id == self.organizer_id:
|
if other.seating_plan.organizer_id == self.organizer_id:
|
||||||
@@ -1487,8 +1539,11 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
return qs_annotated
|
return qs_annotated
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def annotated(cls, qs, channel='web', voucher=None):
|
def annotated(cls, qs, channel, voucher=None):
|
||||||
from .items import SubEventItem, SubEventItemVariation
|
from .items import SubEventItem, SubEventItemVariation
|
||||||
|
from .organizer import SalesChannel
|
||||||
|
|
||||||
|
assert isinstance(channel, (str, SalesChannel))
|
||||||
|
|
||||||
qs = super().annotated(qs, channel, voucher=voucher)
|
qs = super().annotated(qs, channel, voucher=voucher)
|
||||||
qs = qs.annotate(
|
qs = qs.annotate(
|
||||||
|
|||||||
@@ -34,8 +34,10 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
from collections import Counter, OrderedDict
|
from collections import Counter, OrderedDict
|
||||||
from datetime import date, datetime, time, timedelta
|
from datetime import date, datetime, time, timedelta
|
||||||
from decimal import Decimal, DecimalException
|
from decimal import Decimal, DecimalException
|
||||||
@@ -61,7 +63,6 @@ from django_countries.fields import Country
|
|||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
|
||||||
from pretix.base.models import fields
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.fields import MultiStringField
|
from pretix.base.models.fields import MultiStringField
|
||||||
from pretix.base.models.tax import TaxedPrice
|
from pretix.base.models.tax import TaxedPrice
|
||||||
@@ -270,14 +271,24 @@ class SubEventItemVariation(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||||
|
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||||
|
# makes the query SIGNIFICANTLY faster
|
||||||
|
from .organizer import SalesChannel
|
||||||
|
|
||||||
|
assert isinstance(channel, (SalesChannel, str))
|
||||||
q = (
|
q = (
|
||||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||||
# in models/event.py: EventMixin.annotated()
|
# in models/event.py: EventMixin.annotated()
|
||||||
Q(active=True)
|
Q(active=True)
|
||||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
|
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
|
||||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
& Q(require_bundling=False)
|
||||||
)
|
)
|
||||||
|
if isinstance(channel, str):
|
||||||
|
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
||||||
|
else:
|
||||||
|
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
|
||||||
|
|
||||||
if not allow_addons:
|
if not allow_addons:
|
||||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||||
|
|
||||||
@@ -353,8 +364,10 @@ class Item(LoggedModel):
|
|||||||
:type original_price: decimal.Decimal
|
:type original_price: decimal.Decimal
|
||||||
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
|
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
|
||||||
:type require_approval: bool
|
:type require_approval: bool
|
||||||
:param sales_channels: Sales channels this item is available on.
|
:param all_sales_channels: A flag indicating that this item is available on all channels and limit_sales_channels will be ignored.
|
||||||
:type sales_channels: bool
|
:type all_sales_channels: bool
|
||||||
|
:param limit_sales_channels: A list of sales channel identifiers, that this item is available for sale on.
|
||||||
|
:type limit_sales_channels: list
|
||||||
:param issue_giftcard: If ``True``, buying this product will give you a gift card with the value of the product's price
|
:param issue_giftcard: If ``True``, buying this product will give you a gift card with the value of the product's price
|
||||||
:type issue_giftcard: bool
|
:type issue_giftcard: bool
|
||||||
:param validity_mode: Instruction how to set ``valid_from``/``valid_until`` on tickets, ``null`` is default event validity.
|
:param validity_mode: Instruction how to set ``valid_from``/``valid_until`` on tickets, ``null`` is default event validity.
|
||||||
@@ -609,9 +622,14 @@ class Item(LoggedModel):
|
|||||||
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||||
)
|
)
|
||||||
sales_channels = fields.MultiStringField(
|
all_sales_channels = models.BooleanField(
|
||||||
verbose_name=_('Sales channels'),
|
verbose_name=_("Sell on all sales channels"),
|
||||||
default=['web'],
|
default=True,
|
||||||
|
)
|
||||||
|
limit_sales_channels = models.ManyToManyField(
|
||||||
|
"SalesChannel",
|
||||||
|
verbose_name=_("Restrict to specific sales channels"),
|
||||||
|
help_text=_('Only sell tickets for this product on the selected sales channels.'),
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
issue_giftcard = models.BooleanField(
|
issue_giftcard = models.BooleanField(
|
||||||
@@ -1033,9 +1051,13 @@ class Item(LoggedModel):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def _all_sales_channels_identifiers():
|
def _all_sales_channels_identifiers(): # kept for legacy migrations
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channel_types
|
||||||
return list(get_all_sales_channels().keys())
|
|
||||||
|
if "PYTEST_CURRENT_TEST" not in os.environ:
|
||||||
|
warnings.warn('Method should not be used in new code.', DeprecationWarning)
|
||||||
|
|
||||||
|
return list(get_all_sales_channel_types().keys())
|
||||||
|
|
||||||
|
|
||||||
class ItemVariation(models.Model):
|
class ItemVariation(models.Model):
|
||||||
@@ -1058,6 +1080,10 @@ class ItemVariation(models.Model):
|
|||||||
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
||||||
approval by an administrator
|
approval by an administrator
|
||||||
:type require_approval: bool
|
:type require_approval: bool
|
||||||
|
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
|
||||||
|
:type all_sales_channels: bool
|
||||||
|
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
||||||
|
:type limit_sales_channels: list
|
||||||
|
|
||||||
"""
|
"""
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
@@ -1143,9 +1169,13 @@ class ItemVariation(models.Model):
|
|||||||
default=Item.UNAVAIL_MODE_HIDDEN,
|
default=Item.UNAVAIL_MODE_HIDDEN,
|
||||||
max_length=16,
|
max_length=16,
|
||||||
)
|
)
|
||||||
sales_channels = fields.MultiStringField(
|
all_sales_channels = models.BooleanField(
|
||||||
verbose_name=_('Sales channels'),
|
verbose_name=_("Sell on all sales channels the product is sold on"),
|
||||||
default=_all_sales_channels_identifiers,
|
default=True,
|
||||||
|
)
|
||||||
|
limit_sales_channels = models.ManyToManyField(
|
||||||
|
"SalesChannel",
|
||||||
|
verbose_name=_("Restrict to specific sales channels"),
|
||||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
||||||
'selected here but not on product level, the variation will not be available.'),
|
'selected here but not on product level, the variation will not be available.'),
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
:type require_approval: bool
|
:type require_approval: bool
|
||||||
:param meta_info: Additional meta information on the order, JSON-encoded.
|
:param meta_info: Additional meta information on the order, JSON-encoded.
|
||||||
:type meta_info: str
|
:type meta_info: str
|
||||||
:param sales_channel: Identifier of the sales channel this order was created through.
|
:param sales_channel: Foreign key to the sales channel this order was created through.
|
||||||
:type sales_channel: str
|
:type sales_channel: SalesChannel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
STATUS_PENDING = "n"
|
STATUS_PENDING = "n"
|
||||||
@@ -299,13 +299,21 @@ class Order(LockModel, LoggedModel):
|
|||||||
verbose_name=_("Meta information"),
|
verbose_name=_("Meta information"),
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
api_meta = models.JSONField(
|
||||||
|
verbose_name=_("API meta information"),
|
||||||
|
null=False, blank=True,
|
||||||
|
default=dict
|
||||||
|
)
|
||||||
last_modified = models.DateTimeField(
|
last_modified = models.DateTimeField(
|
||||||
auto_now=True, db_index=False
|
auto_now=True, db_index=False
|
||||||
)
|
)
|
||||||
require_approval = models.BooleanField(
|
require_approval = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
sales_channel = models.CharField(max_length=190, default="web")
|
sales_channel = models.ForeignKey(
|
||||||
|
"SalesChannel",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
)
|
||||||
email_known_to_work = models.BooleanField(
|
email_known_to_work = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('E-mail address verified')
|
verbose_name=_('E-mail address verified')
|
||||||
@@ -1932,7 +1940,7 @@ class OrderPayment(models.Model):
|
|||||||
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
|
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
|
||||||
)
|
)
|
||||||
|
|
||||||
if send_mail and self.order.sales_channel in self.order.event.settings.mail_sales_channel_placed_paid:
|
if send_mail and self.order.sales_channel.identifier in self.order.event.settings.mail_sales_channel_placed_paid:
|
||||||
self._send_paid_mail(invoice, user, mail_text)
|
self._send_paid_mail(invoice, user, mail_text)
|
||||||
if self.order.event.settings.mail_send_order_paid_attendee:
|
if self.order.event.settings.mail_send_order_paid_attendee:
|
||||||
for p in self.order.positions.all():
|
for p in self.order.positions.all():
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ from django.utils.crypto import get_random_string
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_scopes import ScopedManager, scope
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.validators import OrganizerSlugBanlistValidator
|
from pretix.base.validators import OrganizerSlugBanlistValidator
|
||||||
@@ -104,6 +106,8 @@ class Organizer(LoggedModel):
|
|||||||
if is_new:
|
if is_new:
|
||||||
kwargs.pop('update_fields', None) # does not make sense here
|
kwargs.pop('update_fields', None) # does not make sense here
|
||||||
self.set_defaults()
|
self.set_defaults()
|
||||||
|
with scope(organizer=self):
|
||||||
|
self.create_default_sales_channels()
|
||||||
else:
|
else:
|
||||||
self.get_cache().clear()
|
self.get_cache().clear()
|
||||||
return obj
|
return obj
|
||||||
@@ -212,6 +216,24 @@ class Organizer(LoggedModel):
|
|||||||
else:
|
else:
|
||||||
return get_connection(fail_silently=False)
|
return get_connection(fail_silently=False)
|
||||||
|
|
||||||
|
def create_default_sales_channels(self):
|
||||||
|
from pretix.base.channels import get_all_sales_channel_types
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for channel in get_all_sales_channel_types().values():
|
||||||
|
if not channel.default_created:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.sales_channels.get_or_create(
|
||||||
|
identifier=channel.identifier,
|
||||||
|
defaults={
|
||||||
|
'label': LazyI18nString.from_gettext(channel.verbose_name),
|
||||||
|
'type': channel.identifier,
|
||||||
|
},
|
||||||
|
position=i
|
||||||
|
)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
def generate_invite_token():
|
def generate_invite_token():
|
||||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||||
@@ -504,3 +526,58 @@ class OrganizerFooterLink(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
self.organizer.cache.clear()
|
self.organizer.cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannel(LoggedModel):
|
||||||
|
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='sales_channels')
|
||||||
|
label = I18nCharField(
|
||||||
|
max_length=200,
|
||||||
|
verbose_name=_("Name"),
|
||||||
|
)
|
||||||
|
identifier = models.CharField(
|
||||||
|
verbose_name=_("Identifier"),
|
||||||
|
max_length=200,
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
regex=r"^[a-zA-Z0-9.\-_]+$",
|
||||||
|
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
type = models.CharField(
|
||||||
|
verbose_name=_("Type"),
|
||||||
|
max_length=200,
|
||||||
|
)
|
||||||
|
position = models.PositiveIntegerField(
|
||||||
|
default=0,
|
||||||
|
verbose_name=_("Position")
|
||||||
|
)
|
||||||
|
configuration = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer="organizer")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("position", "type", "identifier", "id")
|
||||||
|
unique_together = ("organizer", "identifier")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.label)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def type_instance(self):
|
||||||
|
from ..channels import get_all_sales_channel_types
|
||||||
|
|
||||||
|
types = get_all_sales_channel_types()
|
||||||
|
return types[self.type]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
return self.type_instance.icon
|
||||||
|
|
||||||
|
def allow_delete(self):
|
||||||
|
from . import Order
|
||||||
|
|
||||||
|
if self.type_instance.default_created:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return not Order.objects.filter(sales_channel=self).exists()
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class Seat(models.Model):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def annotated(cls, qs, event_id, subevent, ignore_voucher_id=None, minimal_distance=0,
|
def annotated(cls, qs, event_id, subevent, ignore_voucher_id=None, minimal_distance=0,
|
||||||
ignore_order_id=None, ignore_cart_id=None, distance_only_within_row=False):
|
ignore_order_id=None, ignore_cart_id=None, distance_only_within_row=False, annotate_ids=False):
|
||||||
from . import CartPosition, Order, OrderPosition, Voucher
|
from . import CartPosition, Order, OrderPosition, Voucher
|
||||||
|
|
||||||
vqs = Voucher.objects.filter(
|
vqs = Voucher.objects.filter(
|
||||||
@@ -214,17 +214,24 @@ class Seat(models.Model):
|
|||||||
)
|
)
|
||||||
if ignore_cart_id:
|
if ignore_cart_id:
|
||||||
cqs = cqs.exclude(cart_id=ignore_cart_id)
|
cqs = cqs.exclude(cart_id=ignore_cart_id)
|
||||||
qs_annotated = qs.annotate(
|
if annotate_ids:
|
||||||
has_order=Exists(
|
qs_annotated = qs.annotate(
|
||||||
opqs
|
orderposition_id=Subquery(opqs.values('id')),
|
||||||
),
|
cartposition_id=Subquery(cqs.values('id')),
|
||||||
has_cart=Exists(
|
voucher_id=Subquery(vqs.values('id')),
|
||||||
cqs
|
)
|
||||||
),
|
else:
|
||||||
has_voucher=Exists(
|
qs_annotated = qs.annotate(
|
||||||
vqs
|
has_order=Exists(
|
||||||
|
opqs
|
||||||
|
),
|
||||||
|
has_cart=Exists(
|
||||||
|
cqs
|
||||||
|
),
|
||||||
|
has_voucher=Exists(
|
||||||
|
vqs
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if minimal_distance > 0:
|
if minimal_distance > 0:
|
||||||
# TODO: Is there a more performant implementation on PostgreSQL using
|
# TODO: Is there a more performant implementation on PostgreSQL using
|
||||||
@@ -235,7 +242,11 @@ class Seat(models.Model):
|
|||||||
Power(F('y') - OuterRef('y'), Value(2), output_field=models.FloatField())
|
Power(F('y') - OuterRef('y'), Value(2), output_field=models.FloatField())
|
||||||
)
|
)
|
||||||
).filter(
|
).filter(
|
||||||
Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True),
|
(
|
||||||
|
(Q(orderposition_id__isnull=False) | Q(cartposition_id__isnull=False) | Q(voucher_id__isnull=False))
|
||||||
|
if annotate_ids else
|
||||||
|
(Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True))
|
||||||
|
),
|
||||||
distance__lt=minimal_distance ** 2
|
distance__lt=minimal_distance ** 2
|
||||||
)
|
)
|
||||||
if distance_only_within_row:
|
if distance_only_within_row:
|
||||||
@@ -243,10 +254,14 @@ class Seat(models.Model):
|
|||||||
qs_annotated = qs_annotated.annotate(has_closeby_taken=Exists(sq_closeby))
|
qs_annotated = qs_annotated.annotate(has_closeby_taken=Exists(sq_closeby))
|
||||||
return qs_annotated
|
return qs_annotated
|
||||||
|
|
||||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web',
|
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None,
|
||||||
|
sales_channel='web',
|
||||||
ignore_distancing=False, distance_ignore_cart_id=None):
|
ignore_distancing=False, distance_ignore_cart_id=None):
|
||||||
from .orders import Order
|
from .orders import Order
|
||||||
|
from .organizer import SalesChannel
|
||||||
|
|
||||||
|
if isinstance(sales_channel, SalesChannel):
|
||||||
|
sales_channel = sales_channel.identifier
|
||||||
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
|
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
|
||||||
return False
|
return False
|
||||||
opqs = self.orderposition_set.filter(
|
opqs = self.orderposition_set.filter(
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
|
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
|
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
|
||||||
@@ -417,8 +416,8 @@ class BasePaymentProvider:
|
|||||||
forms.MultipleChoiceField(
|
forms.MultipleChoiceField(
|
||||||
label=_('Restrict to specific sales channels'),
|
label=_('Restrict to specific sales channels'),
|
||||||
choices=(
|
choices=(
|
||||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
(c.identifier, c.label) for c in self.event.organizer.sales_channels.all()
|
||||||
if c.payment_restrictions_supported
|
if c.type_instance.payment_restrictions_supported
|
||||||
),
|
),
|
||||||
initial=['web'],
|
initial=['web'],
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
@@ -588,7 +587,7 @@ class BasePaymentProvider:
|
|||||||
return rel_date.datetime(self.event).date()
|
return rel_date.datetime(self.event).date()
|
||||||
|
|
||||||
def _is_available_by_time(self, now_dt=None, cart_id=None, order=None):
|
def _is_available_by_time(self, now_dt=None, cart_id=None, order=None):
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
tz = ZoneInfo(self.event.settings.timezone)
|
tz = ZoneInfo(self.event.settings.timezone)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -853,7 +852,7 @@ class BasePaymentProvider:
|
|||||||
if str(ia.country) != '' and str(ia.country) not in restricted_countries:
|
if str(ia.country) != '' and str(ia.country) not in restricted_countries:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
|
if order.sales_channel.identifier not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self._is_available_by_time(order=order)
|
return self._is_available_by_time(order=order)
|
||||||
|
|||||||
+48
-9
@@ -49,7 +49,7 @@ from io import BytesIO
|
|||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
import reportlab.rl_config
|
import reportlab.rl_config
|
||||||
from bidi.algorithm import get_display
|
from bidi import get_display
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -956,7 +956,7 @@ class Renderer:
|
|||||||
)
|
)
|
||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
|
||||||
font = o['fontfamily']
|
font = o['fontfamily']
|
||||||
|
|
||||||
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
|
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
|
||||||
@@ -970,12 +970,13 @@ class Renderer:
|
|||||||
if o['italic']:
|
if o['italic']:
|
||||||
font += ' I'
|
font += ' I'
|
||||||
|
|
||||||
|
fontsize = override_fontsize if override_fontsize is not None else float(o['fontsize'])
|
||||||
try:
|
try:
|
||||||
ad = getAscentDescent(font, float(o['fontsize']))
|
ad = getAscentDescent(font, fontsize)
|
||||||
except KeyError: # font not known, fall back
|
except KeyError: # font not known, fall back
|
||||||
logger.warning(f'Use of unknown font "{font}"')
|
logger.warning(f'Use of unknown font "{font}"')
|
||||||
font = 'Open Sans'
|
font = 'Open Sans'
|
||||||
ad = getAscentDescent(font, float(o['fontsize']))
|
ad = getAscentDescent(font, fontsize)
|
||||||
|
|
||||||
align_map = {
|
align_map = {
|
||||||
'left': TA_LEFT,
|
'left': TA_LEFT,
|
||||||
@@ -985,16 +986,17 @@ class Renderer:
|
|||||||
# lineheight display differs from browser canvas. This calc is just empirical values to get
|
# lineheight display differs from browser canvas. This calc is just empirical values to get
|
||||||
# reportlab render similarly to browser canvas.
|
# reportlab render similarly to browser canvas.
|
||||||
# for backwards compatability use „uncorrected“ lineheight of 1.0 instead of 1.15
|
# for backwards compatability use „uncorrected“ lineheight of 1.0 instead of 1.15
|
||||||
lineheight = float(o['lineheight']) * 1.15 if 'lineheight' in o else 1.0
|
lineheight = float(o['lineheight']) * 1.15 if not legacy_lineheight or 'lineheight' in o else 1.0
|
||||||
style = ParagraphStyle(
|
style = ParagraphStyle(
|
||||||
name=uuid.uuid4().hex,
|
name=uuid.uuid4().hex,
|
||||||
fontName=font,
|
fontName=font,
|
||||||
fontSize=float(o['fontsize']),
|
fontSize=fontsize,
|
||||||
leading=lineheight * float(o['fontsize']),
|
leading=lineheight * fontsize,
|
||||||
# for backwards compatability use autoLeading if no lineheight is given
|
# for backwards compatability use autoLeading if no lineheight is given
|
||||||
autoLeading='off' if 'lineheight' in o else 'max',
|
autoLeading='off' if not legacy_lineheight or 'lineheight' in o else 'max',
|
||||||
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
|
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
|
||||||
alignment=align_map[o['align']]
|
alignment=align_map[o['align']],
|
||||||
|
splitLongWords=o.get('splitlongwords', True),
|
||||||
)
|
)
|
||||||
# add an almost-invisible space   after hyphens as word-wrap in ReportLab only works on space chars
|
# add an almost-invisible space   after hyphens as word-wrap in ReportLab only works on space chars
|
||||||
text = conditional_escape(
|
text = conditional_escape(
|
||||||
@@ -1013,6 +1015,41 @@ class Renderer:
|
|||||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
|
||||||
p = Paragraph(text, style=style)
|
p = Paragraph(text, style=style)
|
||||||
|
return p, ad, lineheight
|
||||||
|
|
||||||
|
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||||
|
fontsize = float(o['fontsize'])
|
||||||
|
height = float(o['height']) * mm
|
||||||
|
width = float(o['width']) * mm
|
||||||
|
while True:
|
||||||
|
p, ad, lineheight = self._text_paragraph(op, order, o, override_fontsize=fontsize)
|
||||||
|
w, h = p.wrapOn(canvas, width, 1000 * mm)
|
||||||
|
widths = p.getActualLineWidths0()
|
||||||
|
if not widths:
|
||||||
|
break
|
||||||
|
actual_w = max(widths)
|
||||||
|
if not o.get('autoresize', False) or (h <= height and actual_w <= width) or fontsize <= 1.0:
|
||||||
|
break
|
||||||
|
if h > height: # we can do larger steps for height
|
||||||
|
fontsize -= max(1.0, fontsize * .1)
|
||||||
|
else:
|
||||||
|
fontsize -= max(.25, fontsize * .025)
|
||||||
|
|
||||||
|
canvas.saveState()
|
||||||
|
# The ascent/descent offsets here are not really proven to be correct, they're just empirical values to get
|
||||||
|
# reportlab render similarly to browser canvas.
|
||||||
|
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm + height)
|
||||||
|
canvas.rotate(o.get('rotation', 0) * -1)
|
||||||
|
if o.get('verticalalign', 'top') == 'top':
|
||||||
|
p.drawOn(canvas, 0, - h)
|
||||||
|
elif o.get('verticalalign', 'top') == 'middle':
|
||||||
|
p.drawOn(canvas, 0, (-height - h) / 2)
|
||||||
|
elif o.get('verticalalign', 'top') == 'bottom':
|
||||||
|
p.drawOn(canvas, 0, -height)
|
||||||
|
canvas.restoreState()
|
||||||
|
|
||||||
|
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||||
|
p, ad, lineheight = self._text_paragraph(op, order, o, legacy_lineheight=True)
|
||||||
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
|
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
|
||||||
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
|
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
|
||||||
canvas.saveState()
|
canvas.saveState()
|
||||||
@@ -1051,6 +1088,8 @@ class Renderer:
|
|||||||
self._draw_barcodearea(canvas, op, order, o)
|
self._draw_barcodearea(canvas, op, order, o)
|
||||||
elif o['type'] == "imagearea":
|
elif o['type'] == "imagearea":
|
||||||
self._draw_imagearea(canvas, op, order, o)
|
self._draw_imagearea(canvas, op, order, o)
|
||||||
|
elif o['type'] == "textcontainer":
|
||||||
|
self._draw_textcontainer(canvas, op, order, o)
|
||||||
elif o['type'] == "textarea":
|
elif o['type'] == "textarea":
|
||||||
self._draw_textarea(canvas, op, order, o)
|
self._draw_textarea(canvas, op, order, o)
|
||||||
elif o['type'] == "poweredby":
|
elif o['type'] == "poweredby":
|
||||||
|
|||||||
@@ -52,12 +52,11 @@ from django.utils.translation import (
|
|||||||
)
|
)
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
|
CartPosition, Event, InvoiceAddress, Item, ItemVariation, SalesChannel,
|
||||||
SeatCategoryMapping, Voucher,
|
Seat, SeatCategoryMapping, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.orders import OrderFee
|
from pretix.base.models.orders import OrderFee
|
||||||
@@ -275,8 +274,8 @@ class CartManager:
|
|||||||
AddOperation: 30
|
AddOperation: 30
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None,
|
def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
|
||||||
sales_channel='web'):
|
invoice_address: InvoiceAddress=None, widget_data=None, expiry=None):
|
||||||
self.event = event
|
self.event = event
|
||||||
self.cart_id = cart_id
|
self.cart_id = cart_id
|
||||||
self.real_now_dt = now()
|
self.real_now_dt = now()
|
||||||
@@ -288,6 +287,7 @@ class CartManager:
|
|||||||
self._variations_cache = {}
|
self._variations_cache = {}
|
||||||
self._seated_cache = {}
|
self._seated_cache = {}
|
||||||
self._expiry = None
|
self._expiry = None
|
||||||
|
self._explicit_expiry = expiry
|
||||||
self.invoice_address = invoice_address
|
self.invoice_address = invoice_address
|
||||||
self._widget_data = widget_data or {}
|
self._widget_data = widget_data or {}
|
||||||
self._sales_channel = sales_channel
|
self._sales_channel = sales_channel
|
||||||
@@ -306,7 +306,12 @@ class CartManager:
|
|||||||
return self._seated_cache[item, subevent]
|
return self._seated_cache[item, subevent]
|
||||||
|
|
||||||
def _calculate_expiry(self):
|
def _calculate_expiry(self):
|
||||||
self._expiry = self.real_now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
if self._explicit_expiry:
|
||||||
|
self._expiry = self._explicit_expiry
|
||||||
|
else:
|
||||||
|
self._expiry = self.real_now_dt + timedelta(
|
||||||
|
minutes=self.event.settings.get('reservation_time', as_type=int)
|
||||||
|
)
|
||||||
|
|
||||||
def _check_presale_dates(self):
|
def _check_presale_dates(self):
|
||||||
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
|
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
|
||||||
@@ -384,7 +389,7 @@ class CartManager:
|
|||||||
})
|
})
|
||||||
|
|
||||||
def _check_max_cart_size(self):
|
def _check_max_cart_size(self):
|
||||||
if not get_all_sales_channels()[self._sales_channel].unlimited_items_per_order:
|
if not self._sales_channel.type_instance.unlimited_items_per_order:
|
||||||
cartsize = self.positions.filter(addon_to__isnull=True).count()
|
cartsize = self.positions.filter(addon_to__isnull=True).count()
|
||||||
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
|
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
|
||||||
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
|
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
|
||||||
@@ -422,8 +427,13 @@ class CartManager:
|
|||||||
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
|
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||||
raise CartError(error_messages['media_usage_not_implemented'])
|
raise CartError(error_messages['media_usage_not_implemented'])
|
||||||
|
|
||||||
if self._sales_channel not in op.item.sales_channels or (op.variation and self._sales_channel not in op.variation.sales_channels):
|
if not op.item.all_sales_channels:
|
||||||
raise CartError(error_messages['unavailable'])
|
if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
|
||||||
|
raise CartError(error_messages['unavailable'])
|
||||||
|
|
||||||
|
if op.variation and not op.variation.all_sales_channels:
|
||||||
|
if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
|
||||||
|
raise CartError(error_messages['unavailable'])
|
||||||
|
|
||||||
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
|
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
|
||||||
raise CartError(error_messages['not_for_sale'])
|
raise CartError(error_messages['not_for_sale'])
|
||||||
@@ -457,7 +467,14 @@ class CartManager:
|
|||||||
raise CartError(error_messages['ended'])
|
raise CartError(error_messages['ended'])
|
||||||
|
|
||||||
seated = self._is_seated(op.item, op.subevent)
|
seated = self._is_seated(op.item, op.subevent)
|
||||||
if seated and (not op.seat or (op.seat.blocked and self._sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel)):
|
if (
|
||||||
|
seated and (
|
||||||
|
not op.seat or (
|
||||||
|
op.seat.blocked and
|
||||||
|
self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
raise CartError(error_messages['seat_invalid'])
|
raise CartError(error_messages['seat_invalid'])
|
||||||
elif op.seat and not seated:
|
elif op.seat and not seated:
|
||||||
raise CartError(error_messages['seat_forbidden'])
|
raise CartError(error_messages['seat_forbidden'])
|
||||||
@@ -1371,7 +1388,7 @@ class CartManager:
|
|||||||
|
|
||||||
discount_results = apply_discounts(
|
discount_results = apply_discounts(
|
||||||
self.event,
|
self.event,
|
||||||
self._sales_channel,
|
self._sales_channel.identifier,
|
||||||
[
|
[
|
||||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||||
for cp in positions
|
for cp in positions
|
||||||
@@ -1505,6 +1522,11 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
|||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||||
|
except SalesChannel.DoesNotExist:
|
||||||
|
raise CartError("Invalid sales channel.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
|
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
|
||||||
@@ -1526,6 +1548,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
|
|||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
|
try:
|
||||||
|
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||||
|
except SalesChannel.DoesNotExist:
|
||||||
|
raise CartError("Invalid sales channel.")
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||||
@@ -1546,6 +1572,10 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
|||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
|
try:
|
||||||
|
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||||
|
except SalesChannel.DoesNotExist:
|
||||||
|
raise CartError("Invalid sales channel.")
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||||
@@ -1565,6 +1595,10 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
|||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
|
try:
|
||||||
|
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||||
|
except SalesChannel.DoesNotExist:
|
||||||
|
raise CartError("Invalid sales channel.")
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||||
@@ -1593,6 +1627,10 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
|
|||||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||||
|
except SalesChannel.DoesNotExist:
|
||||||
|
raise CartError("Invalid sales channel.")
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
||||||
|
|||||||
@@ -1154,12 +1154,12 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
|
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
|
||||||
def order_placed(sender, **kwargs):
|
def order_placed(sender, **kwargs):
|
||||||
order = kwargs['order']
|
order = kwargs['order']
|
||||||
event = sender
|
event = sender
|
||||||
|
|
||||||
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
|
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
|
||||||
'limit_products'))
|
'limit_products'))
|
||||||
if not cls:
|
if not cls:
|
||||||
return
|
return
|
||||||
@@ -1171,7 +1171,7 @@ def order_placed(sender, **kwargs):
|
|||||||
checkin_created.send(event, checkin=ci)
|
checkin_created.send(event, checkin=ci)
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task, dispatch_uid="autocheckin_exit_all")
|
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def process_exit_all(sender, **kwargs):
|
def process_exit_all(sender, **kwargs):
|
||||||
qs = CheckinList.objects.filter(
|
qs = CheckinList.objects.filter(
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ def invoice_pdf_task(invoice: int):
|
|||||||
|
|
||||||
def invoice_qualified(order: Order):
|
def invoice_qualified(order: Order):
|
||||||
if order.total == Decimal('0.00') or order.require_approval or \
|
if order.total == Decimal('0.00') or order.require_approval or \
|
||||||
order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
|
order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -443,8 +443,11 @@ def build_preview_invoice_pdf(event):
|
|||||||
locale = event.settings.locale
|
locale = event.settings.locale
|
||||||
|
|
||||||
with rolledback_transaction(), language(locale, event.settings.region):
|
with rolledback_transaction(), language(locale, event.settings.region):
|
||||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
|
order = event.orders.create(
|
||||||
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
|
status=Order.STATUS_PENDING, datetime=timezone.now(),
|
||||||
|
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count(),
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
|
)
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
order=order, event=event, invoice_no="PREVIEW",
|
order=order, event=event, invoice_no="PREVIEW",
|
||||||
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
date=timezone.now().date(), locale=locale, organizer=event.organizer
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ from django.core.mail import (
|
|||||||
from django.core.mail.message import SafeMIMEText
|
from django.core.mail.message import SafeMIMEText
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import now, override
|
from django.utils.timezone import now, override
|
||||||
from django.utils.translation import gettext as _, pgettext
|
from django.utils.translation import gettext as _, pgettext
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
@@ -109,6 +110,22 @@ def clean_sender_name(sender_name: str) -> str:
|
|||||||
return sender_name
|
return sender_name
|
||||||
|
|
||||||
|
|
||||||
|
def prefix_subject(settings_holder, subject, highlight=False):
|
||||||
|
prefix = settings_holder.settings.get('mail_prefix')
|
||||||
|
if prefix and prefix.startswith('[') and prefix.endswith(']'):
|
||||||
|
prefix = prefix[1:-1]
|
||||||
|
if prefix:
|
||||||
|
prefix = f"[{prefix}]"
|
||||||
|
if highlight:
|
||||||
|
prefix = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||||
|
_('This prefix has been set in your event or organizer settings.'),
|
||||||
|
escape(prefix)
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = f"{prefix} {subject}"
|
||||||
|
return subject
|
||||||
|
|
||||||
|
|
||||||
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
|
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
|
||||||
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
|
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
|
||||||
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
|
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
|
||||||
@@ -240,11 +257,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
and settings_holder.settings.contact_mail and not headers.get('Reply-To'):
|
and settings_holder.settings.contact_mail and not headers.get('Reply-To'):
|
||||||
headers['Reply-To'] = settings_holder.settings.contact_mail
|
headers['Reply-To'] = settings_holder.settings.contact_mail
|
||||||
|
|
||||||
prefix = settings_holder.settings.get('mail_prefix')
|
subject = prefix_subject(settings_holder, subject)
|
||||||
if prefix and prefix.startswith('[') and prefix.endswith(']'):
|
|
||||||
prefix = prefix[1:-1]
|
|
||||||
if prefix:
|
|
||||||
subject = "[%s] %s" % (prefix, subject)
|
|
||||||
|
|
||||||
body_plain += "\r\n\r\n-- \r\n"
|
body_plain += "\r\n\r\n-- \r\n"
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import OAuthApplication
|
from pretix.api.models import OAuthApplication
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.email import get_email_context
|
from pretix.base.email import get_email_context
|
||||||
from pretix.base.i18n import get_language_without_region, language
|
from pretix.base.i18n import get_language_without_region, language
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
@@ -76,7 +75,7 @@ from pretix.base.models.orders import (
|
|||||||
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
||||||
generate_secret,
|
generate_secret,
|
||||||
)
|
)
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
|
||||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||||
from pretix.base.payment import GiftCardPayment, PaymentException
|
from pretix.base.payment import GiftCardPayment, PaymentException
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
@@ -650,8 +649,7 @@ def _check_date(event: Event, now_dt: datetime):
|
|||||||
|
|
||||||
|
|
||||||
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
|
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
|
||||||
address: InvoiceAddress = None,
|
sales_channel: SalesChannel, address: InvoiceAddress=None, customer=None):
|
||||||
sales_channel='web', customer=None):
|
|
||||||
err = None
|
err = None
|
||||||
_check_date(event, time_machine_now_dt)
|
_check_date(event, time_machine_now_dt)
|
||||||
|
|
||||||
@@ -775,7 +773,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
if cp.seat:
|
if cp.seat:
|
||||||
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
|
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
|
||||||
# time, since we absolutely can not overbook a seat.
|
# time, since we absolutely can not overbook a seat.
|
||||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel):
|
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
|
||||||
err = err or error_messages['seat_unavailable']
|
err = err or error_messages['seat_unavailable']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
@@ -873,7 +871,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
||||||
discount_results = apply_discounts(
|
discount_results = apply_discounts(
|
||||||
event,
|
event,
|
||||||
sales_channel,
|
sales_channel.identifier,
|
||||||
[
|
[
|
||||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||||
for cp in sorted_positions
|
for cp in sorted_positions
|
||||||
@@ -959,12 +957,11 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
|
|||||||
return fees
|
return fees
|
||||||
|
|
||||||
|
|
||||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||||
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
|
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
|
||||||
meta_info: dict=None, sales_channel: str='web', shown_total=None,
|
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
|
||||||
customer=None, valid_if_pending=False):
|
customer=None, valid_if_pending=False, api_meta: dict=None):
|
||||||
payments = []
|
payments = []
|
||||||
sales_channel = get_all_sales_channels()[sales_channel]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
|
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
|
||||||
@@ -986,10 +983,11 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
datetime=now_dt,
|
datetime=now_dt,
|
||||||
locale=get_language_without_region(locale),
|
locale=get_language_without_region(locale),
|
||||||
total=total,
|
total=total,
|
||||||
testmode=True if sales_channel.testmode_supported and event.testmode else False,
|
testmode=True if sales_channel.type_instance.testmode_supported and event.testmode else False,
|
||||||
meta_info=json.dumps(meta_info or {}),
|
meta_info=json.dumps(meta_info or {}),
|
||||||
|
api_meta=api_meta or {},
|
||||||
require_approval=require_approval,
|
require_approval=require_approval,
|
||||||
sales_channel=sales_channel.identifier,
|
sales_channel=sales_channel,
|
||||||
customer=customer,
|
customer=customer,
|
||||||
valid_if_pending=valid_if_pending,
|
valid_if_pending=valid_if_pending,
|
||||||
)
|
)
|
||||||
@@ -1099,7 +1097,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
|
|||||||
|
|
||||||
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
|
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
|
||||||
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
|
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
|
||||||
shown_total=None, customer=None):
|
shown_total=None, customer=None, api_meta: dict=None):
|
||||||
for p in payment_requests:
|
for p in payment_requests:
|
||||||
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
|
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
|
||||||
if not p['pprov']:
|
if not p['pprov']:
|
||||||
@@ -1108,6 +1106,11 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
if customer:
|
if customer:
|
||||||
customer = event.organizer.customers.get(pk=customer)
|
customer = event.organizer.customers.get(pk=customer)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||||
|
except SalesChannel.DoesNotExist:
|
||||||
|
raise OrderError("Invalid sales channel.")
|
||||||
|
|
||||||
if email == settings.PRETIX_EMAIL_NONE_VALUE:
|
if email == settings.PRETIX_EMAIL_NONE_VALUE:
|
||||||
email = None
|
email = None
|
||||||
|
|
||||||
@@ -1186,9 +1189,21 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
if 'sleep-after-quota-check' in debugflags_var.get():
|
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||||
sleep(2)
|
sleep(2)
|
||||||
|
|
||||||
order, payment_objs = _create_order(event, email, positions, real_now_dt, payment_requests,
|
order, payment_objs = _create_order(
|
||||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
event,
|
||||||
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
email=email,
|
||||||
|
positions=positions,
|
||||||
|
now_dt=real_now_dt,
|
||||||
|
payment_requests=payment_requests,
|
||||||
|
locale=locale,
|
||||||
|
address=addr,
|
||||||
|
meta_info=meta_info,
|
||||||
|
sales_channel=sales_channel,
|
||||||
|
shown_total=shown_total,
|
||||||
|
customer=customer,
|
||||||
|
valid_if_pending=valid_if_pending,
|
||||||
|
api_meta=api_meta,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for p in payment_objs:
|
for p in payment_objs:
|
||||||
@@ -1272,7 +1287,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
email_attendees_template = event.settings.mail_text_order_placed_attendee
|
email_attendees_template = event.settings.mail_text_order_placed_attendee
|
||||||
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
|
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
|
||||||
|
|
||||||
if sales_channel in event.settings.mail_sales_channel_placed_paid:
|
if sales_channel.identifier in event.settings.mail_sales_channel_placed_paid:
|
||||||
_order_placed_email(event, order, email_template, subject_template, log_entry, invoice, payment_objs,
|
_order_placed_email(event, order, email_template, subject_template, log_entry, invoice, payment_objs,
|
||||||
is_free=free_order_flow)
|
is_free=free_order_flow)
|
||||||
if email_attendees:
|
if email_attendees:
|
||||||
@@ -1424,7 +1439,7 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
if days is None:
|
if days is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if o.sales_channel not in event.settings.mail_sales_channel_download_reminder:
|
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
@@ -1926,9 +1941,13 @@ class OrderChangeManager:
|
|||||||
if not item.is_available() or (variation and not variation.is_available()):
|
if not item.is_available() or (variation and not variation.is_available()):
|
||||||
raise OrderError(error_messages['unavailable'])
|
raise OrderError(error_messages['unavailable'])
|
||||||
|
|
||||||
if self.order.sales_channel not in item.sales_channels or (
|
if not item.all_sales_channels:
|
||||||
variation and self.order.sales_channel not in variation.sales_channels):
|
if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
|
||||||
raise OrderError(error_messages['unavailable'])
|
raise OrderError(error_messages['unavailable'])
|
||||||
|
|
||||||
|
if variation and not variation.all_sales_channels:
|
||||||
|
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
|
||||||
|
raise OrderError(error_messages['unavailable'])
|
||||||
|
|
||||||
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
|
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
|
||||||
raise OrderError(error_messages['not_for_sale'])
|
raise OrderError(error_messages['not_for_sale'])
|
||||||
@@ -2025,9 +2044,12 @@ class OrderChangeManager:
|
|||||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||||
# be part of the input.
|
# be part of the input.
|
||||||
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
||||||
or (a.variation and self.order.sales_channel not in a.variation.sales_channels)
|
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
|
||||||
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
||||||
or self.order.sales_channel not in item.sales_channels
|
or (
|
||||||
|
not item.all_sales_channels and
|
||||||
|
not item.limit_sales_channels.contains(self.order.sales_channel)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if is_unavailable:
|
if is_unavailable:
|
||||||
continue
|
continue
|
||||||
@@ -2853,12 +2875,13 @@ class OrderChangeManager:
|
|||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
||||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||||
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None):
|
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None,
|
||||||
|
api_meta: dict=None):
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||||
sales_channel, shown_total, customer)
|
sales_channel, shown_total, customer, api_meta)
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
except (MaxRetriesExceededError, LockTimeoutException):
|
except (MaxRetriesExceededError, LockTimeoutException):
|
||||||
@@ -3129,7 +3152,7 @@ def signal_listener_issue_memberships(sender: Event, order: Order, **kwargs):
|
|||||||
if order.status != Order.STATUS_PAID or not order.customer:
|
if order.status != Order.STATUS_PAID or not order.customer:
|
||||||
return
|
return
|
||||||
for p in order.positions.all():
|
for p in order.positions.all():
|
||||||
if p.item.grant_membership_type_id:
|
if p.item.grant_membership_type_id and not p.granted_memberships.exists():
|
||||||
create_membership(order.customer, p)
|
create_membership(order.customer, p)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
|
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
|
||||||
|
SalesChannel, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import Event, SubEvent
|
from pretix.base.models.event import Event, SubEvent
|
||||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||||
@@ -164,12 +165,14 @@ def apply_discounts(event: Event, sales_channel: str,
|
|||||||
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
||||||
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
|
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
|
||||||
"""
|
"""
|
||||||
|
if isinstance(sales_channel, SalesChannel):
|
||||||
|
sales_channel = sales_channel.identifier
|
||||||
new_prices = {}
|
new_prices = {}
|
||||||
|
|
||||||
discount_qs = event.discounts.filter(
|
discount_qs = event.discounts.filter(
|
||||||
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
|
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
|
||||||
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
|
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
|
||||||
sales_channels__contains=sales_channel,
|
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=sales_channel),
|
||||||
active=True,
|
active=True,
|
||||||
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
||||||
for discount in discount_qs:
|
for discount in discount_qs:
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ def preview(event: int, provider: str):
|
|||||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||||
email='sample@pretix.eu',
|
email='sample@pretix.eu',
|
||||||
locale=event.settings.locale,
|
locale=event.settings.locale,
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
expires=now(), code="PREVIEW1234", total=119)
|
expires=now(), code="PREVIEW1234", total=119)
|
||||||
|
|
||||||
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||||
|
|||||||
@@ -1295,7 +1295,8 @@ DEFAULTS = {
|
|||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Show event times and dates on the ticket shop"),
|
label=_("Show event times and dates on the ticket shop"),
|
||||||
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
|
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
|
||||||
"does however not affect the display in other locations."),
|
"also affects a few other locations, however it should not be expected that the date of the "
|
||||||
|
"event is shown nowhere to users."),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'show_date_to': {
|
'show_date_to': {
|
||||||
@@ -1478,6 +1479,10 @@ DEFAULTS = {
|
|||||||
min_value=1,
|
min_value=1,
|
||||||
required=True,
|
required=True,
|
||||||
widget=forms.NumberInput(),
|
widget=forms.NumberInput(),
|
||||||
|
help_text=_('With an increased limit, a customer may request more than one ticket for a specific product '
|
||||||
|
'using the same, unique email address. However, regardless of this setting, they will need to '
|
||||||
|
'fill the waiting list form multiple times if they want more than one ticket, as every entry only '
|
||||||
|
'grants one single ticket at a time.'),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'show_checkin_number_user': {
|
'show_checkin_number_user': {
|
||||||
@@ -3359,7 +3364,9 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
},
|
},
|
||||||
'seating_allow_blocked_seats_for_channel': {
|
'seating_allow_blocked_seats_for_channel': {
|
||||||
'default': [],
|
'default': [],
|
||||||
'type': list
|
'type': list,
|
||||||
|
'serializer_class': serializers.ListField,
|
||||||
|
'serializer_kwargs': lambda: dict(child=serializers.CharField()),
|
||||||
},
|
},
|
||||||
'seating_distance_within_row': {
|
'seating_distance_within_row': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
@@ -3797,6 +3804,16 @@ def validate_event_settings(event, settings_dict):
|
|||||||
'payment_term_last': _('The last payment date cannot be before the end of presale.')
|
'payment_term_last': _('The last payment date cannot be before the end of presale.')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if settings_dict.get('seating_allow_blocked_seats_for_channel'):
|
||||||
|
allowed_channels = set(event.organizer.sales_channels.values_list("identifier", flat=True))
|
||||||
|
for channel in settings_dict['seating_allow_blocked_seats_for_channel']:
|
||||||
|
if channel not in allowed_channels:
|
||||||
|
raise ValidationError({
|
||||||
|
'seating_allow_blocked_seats_for_channel': _('The value "{identifier}" is not a valid sales channel.').format(
|
||||||
|
identifier=channel
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if isinstance(event, Event):
|
if isinstance(event, Event):
|
||||||
validate_event_settings.send(sender=event, settings_dict=settings_dict)
|
validate_event_settings.send(sender=event, settings_dict=settings_dict)
|
||||||
|
|
||||||
|
|||||||
@@ -294,13 +294,16 @@ This signal is sent out when a notification is sent.
|
|||||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
register_sales_channels = django.dispatch.Signal()
|
register_sales_channel_types = django.dispatch.Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known sales channels types. Receivers should return an
|
This signal is sent out to get all known sales channels types. Receivers should return an
|
||||||
instance of a subclass of ``pretix.base.channels.SalesChannel`` or a list of such
|
instance of a subclass of ``pretix.base.channels.SalesChannelType`` or a list of such
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
register_sales_channels = DeprecatedSignal() # TODO: remove me
|
||||||
|
|
||||||
register_data_exporters = EventPluginSignal()
|
register_data_exporters = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known data exporters. Receivers should return a
|
This signal is sent out to get all known data exporters. Receivers should return a
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{% load rich_text %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}
|
||||||
|
{% include "django/forms/widgets/input.html" %}
|
||||||
|
{% if widget.wrap_label %}
|
||||||
|
{% if "." in widget.value.instance.type_instance.icon %}
|
||||||
|
<img class="fa-like-image" src="{% static widget.value.instance.type_instance.icon %}" alt="">
|
||||||
|
{% else %}
|
||||||
|
<span class="fa fa-fw fa-{{ widget.value.instance.type_instance.icon }} text-muted"></span>
|
||||||
|
{% endif %}
|
||||||
|
{% if widget.plugin_missing %}
|
||||||
|
<del>
|
||||||
|
{% endif %}
|
||||||
|
{{ widget.label }}{% if widget.plugin_missing %}</del>
|
||||||
|
<span class="fa fa-info-circle" data-toggle="tooltip" title="{% trans "This sales channel cannot be used properly since the respective plugin is not active for this event." %}"></span>
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
@@ -438,3 +438,20 @@ class ButtonGroupRadioSelect(forms.RadioSelect):
|
|||||||
attrs['icon'] = self.option_icons[value]
|
attrs['icon'] = self.option_icons[value]
|
||||||
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||||
return opt
|
return opt
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||||
|
option_template_name = 'pretixbase/forms/widgets/checkbox_sales_channel_option.html'
|
||||||
|
|
||||||
|
def __init__(self, event, attrs=None, choices=()):
|
||||||
|
self.event = event
|
||||||
|
super().__init__(attrs, choices)
|
||||||
|
|
||||||
|
def create_option(
|
||||||
|
self, name, value, label, selected, index, subindex=None, attrs=None
|
||||||
|
):
|
||||||
|
plugin = value.instance.type_instance.required_event_plugin
|
||||||
|
return {
|
||||||
|
**super().create_option(name, value, label, selected, index, subindex, attrs),
|
||||||
|
"plugin_missing": plugin and plugin not in self.event.get_plugins(),
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ from django_scopes.forms import (
|
|||||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
from pretix.base.models import Gate
|
from pretix.base.models import Gate
|
||||||
from pretix.base.models.checkin import Checkin, CheckinList
|
from pretix.base.models.checkin import Checkin, CheckinList
|
||||||
from pretix.control.forms import ItemMultipleChoiceField
|
from pretix.control.forms import (
|
||||||
|
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
||||||
|
)
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
|
|
||||||
|
|
||||||
@@ -66,14 +67,9 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
kwargs.pop('locales', None)
|
kwargs.pop('locales', None)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.fields['limit_products'].queryset = self.event.items.all()
|
self.fields['limit_products'].queryset = self.event.items.all()
|
||||||
self.fields['auto_checkin_sales_channels'] = forms.MultipleChoiceField(
|
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||||
label=self.fields['auto_checkin_sales_channels'].label,
|
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
|
||||||
help_text=self.fields['auto_checkin_sales_channels'].help_text,
|
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
|
||||||
required=self.fields['auto_checkin_sales_channels'].required,
|
|
||||||
choices=(
|
|
||||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.event.organizer.gates.exists():
|
if not self.event.organizer.gates.exists():
|
||||||
@@ -123,13 +119,13 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'gates': forms.CheckboxSelectMultiple(attrs={
|
'gates': forms.CheckboxSelectMultiple(attrs={
|
||||||
'class': 'scrolling-multiple-choice'
|
'class': 'scrolling-multiple-choice'
|
||||||
}),
|
}),
|
||||||
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
|
|
||||||
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
|
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
|
||||||
}
|
}
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'limit_products': ItemMultipleChoiceField,
|
'limit_products': ItemMultipleChoiceField,
|
||||||
'gates': SafeModelMultipleChoiceField,
|
'gates': SafeModelMultipleChoiceField,
|
||||||
'subevent': SafeModelChoiceField,
|
'subevent': SafeModelChoiceField,
|
||||||
|
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
|
||||||
'exit_all_at': NextTimeField,
|
'exit_all_at': NextTimeField,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,16 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channel_types
|
||||||
from pretix.base.forms import I18nModelForm
|
from pretix.base.forms import I18nModelForm
|
||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
from pretix.base.models import Discount
|
from pretix.base.models import Discount
|
||||||
from pretix.control.forms import ItemMultipleChoiceField, SplitDateTimeField
|
from pretix.control.forms import (
|
||||||
|
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
||||||
|
SplitDateTimeField,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DiscountForm(I18nModelForm):
|
class DiscountForm(I18nModelForm):
|
||||||
@@ -38,7 +41,8 @@ class DiscountForm(I18nModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'active',
|
'active',
|
||||||
'internal_name',
|
'internal_name',
|
||||||
'sales_channels',
|
'all_sales_channels',
|
||||||
|
'limit_sales_channels',
|
||||||
'available_from',
|
'available_from',
|
||||||
'available_until',
|
'available_until',
|
||||||
'subevent_mode',
|
'subevent_mode',
|
||||||
@@ -60,6 +64,7 @@ class DiscountForm(I18nModelForm):
|
|||||||
'available_until': SplitDateTimeField,
|
'available_until': SplitDateTimeField,
|
||||||
'condition_limit_products': ItemMultipleChoiceField,
|
'condition_limit_products': ItemMultipleChoiceField,
|
||||||
'benefit_limit_products': ItemMultipleChoiceField,
|
'benefit_limit_products': ItemMultipleChoiceField,
|
||||||
|
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'subevent_mode': forms.RadioSelect,
|
'subevent_mode': forms.RadioSelect,
|
||||||
@@ -83,15 +88,12 @@ class DiscountForm(I18nModelForm):
|
|||||||
self.event = kwargs['event']
|
self.event = kwargs['event']
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.filter(
|
||||||
label=_('Sales channels'),
|
type__in=[k for k, v in get_all_sales_channel_types().items() if v.discounts_supported]
|
||||||
required=True,
|
|
||||||
choices=(
|
|
||||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
|
||||||
if c.discounts_supported
|
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
)
|
)
|
||||||
|
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||||
|
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||||
|
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||||
self.fields['condition_limit_products'].queryset = self.event.items.all()
|
self.fields['condition_limit_products'].queryset = self.event.items.all()
|
||||||
self.fields['benefit_limit_products'].queryset = self.event.items.all()
|
self.fields['benefit_limit_products'].queryset = self.event.items.all()
|
||||||
self.fields['condition_min_count'].required = False
|
self.fields['condition_min_count'].required = False
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
||||||
from django.core.validators import MaxValueValidator
|
from django.core.validators import MaxValueValidator
|
||||||
from django.db.models import Prefetch, Q, prefetch_related_objects
|
from django.db.models import Prefetch, Q, prefetch_related_objects
|
||||||
from django.forms import (
|
from django.forms import formset_factory, inlineformset_factory
|
||||||
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
|
|
||||||
)
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import escape, format_html
|
from django.utils.html import escape, format_html
|
||||||
@@ -54,12 +52,12 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.timezone import get_current_timezone_name
|
from django.utils.timezone import get_current_timezone_name
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries.fields import LazyTypedChoiceField
|
from django_countries.fields import LazyTypedChoiceField
|
||||||
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
from i18nfield.forms import (
|
from i18nfield.forms import (
|
||||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
||||||
)
|
)
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.forms import (
|
from pretix.base.forms import (
|
||||||
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
|
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
|
||||||
)
|
)
|
||||||
@@ -73,8 +71,8 @@ from pretix.base.settings import (
|
|||||||
)
|
)
|
||||||
from pretix.base.validators import multimail_validate
|
from pretix.base.validators import multimail_validate
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
|
||||||
SplitDateTimePickerWidget,
|
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
@@ -378,16 +376,10 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('You need to configure the custom domain in the webserver beforehand.')
|
help_text=_('You need to configure the custom domain in the webserver beforehand.')
|
||||||
)
|
)
|
||||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||||
label=self.fields['sales_channels'].label,
|
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||||
help_text=self.fields['sales_channels'].help_text,
|
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||||
required=self.fields['sales_channels'].required,
|
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||||
initial=self.fields['sales_channels'].initial,
|
|
||||||
choices=(
|
|
||||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_domain(self):
|
def clean_domain(self):
|
||||||
d = self.cleaned_data['domain']
|
d = self.cleaned_data['domain']
|
||||||
@@ -444,7 +436,8 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
'location',
|
'location',
|
||||||
'geo_lat',
|
'geo_lat',
|
||||||
'geo_lon',
|
'geo_lon',
|
||||||
'sales_channels'
|
'all_sales_channels',
|
||||||
|
'limit_sales_channels',
|
||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'date_from': SplitDateTimeField,
|
'date_from': SplitDateTimeField,
|
||||||
@@ -452,6 +445,7 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
'date_admission': SplitDateTimeField,
|
'date_admission': SplitDateTimeField,
|
||||||
'presale_start': SplitDateTimeField,
|
'presale_start': SplitDateTimeField,
|
||||||
'presale_end': SplitDateTimeField,
|
'presale_end': SplitDateTimeField,
|
||||||
|
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'date_from': SplitDateTimePickerWidget(),
|
'date_from': SplitDateTimePickerWidget(),
|
||||||
@@ -459,7 +453,6 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
|
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
|
||||||
'presale_start': SplitDateTimePickerWidget(),
|
'presale_start': SplitDateTimePickerWidget(),
|
||||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
||||||
'sales_channels': CheckboxSelectMultiple(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -915,7 +908,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
locale_names = dict(settings.LANGUAGES)
|
locale_names = dict(settings.LANGUAGES)
|
||||||
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
|
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
|
||||||
self.fields['invoice_generate_sales_channels'].choices = (
|
self.fields['invoice_generate_sales_channels'].choices = (
|
||||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
|
||||||
)
|
)
|
||||||
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
|
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
|
||||||
|
|
||||||
@@ -961,7 +954,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
|
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
|
||||||
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
|
choices=[],
|
||||||
label=_('Sales channels for checkout emails'),
|
label=_('Sales channels for checkout emails'),
|
||||||
help_text=_('The order placed and paid emails will only be send to orders from these sales channels. '
|
help_text=_('The order placed and paid emails will only be send to orders from these sales channels. '
|
||||||
'The online shop must be enabled.'),
|
'The online shop must be enabled.'),
|
||||||
@@ -972,7 +965,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
mail_sales_channel_download_reminder = forms.MultipleChoiceField(
|
mail_sales_channel_download_reminder = forms.MultipleChoiceField(
|
||||||
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
|
choices=[],
|
||||||
label=_('Sales channels'),
|
label=_('Sales channels'),
|
||||||
help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'),
|
help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'),
|
||||||
widget=forms.CheckboxSelectMultiple(
|
widget=forms.CheckboxSelectMultiple(
|
||||||
@@ -1367,6 +1360,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
self.fields['mail_html_renderer'].choices = [
|
self.fields['mail_html_renderer'].choices = [
|
||||||
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
|
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
|
||||||
]
|
]
|
||||||
|
self.fields['mail_sales_channel_placed_paid'].choices = (
|
||||||
|
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
|
||||||
|
)
|
||||||
|
self.fields['mail_sales_channel_download_reminder'].choices = (
|
||||||
|
(c.identifier, c.label) for c in event.organizer.sales_channels.all()
|
||||||
|
)
|
||||||
|
|
||||||
prefetch_related_objects([self.event.organizer], Prefetch('meta_properties'))
|
prefetch_related_objects([self.event.organizer], Prefetch('meta_properties'))
|
||||||
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
||||||
|
|||||||
@@ -48,19 +48,20 @@ from django.utils.formats import date_format, localize
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||||
|
from django_countries.fields import CountryField
|
||||||
from django_scopes.forms import SafeModelChoiceField
|
from django_scopes.forms import SafeModelChoiceField
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.forms.widgets import (
|
from pretix.base.forms.widgets import (
|
||||||
DatePickerWidget, SplitDateTimePickerWidget, TimePickerWidget,
|
DatePickerWidget, SplitDateTimePickerWidget, TimePickerWidget,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
|
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
|
||||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||||
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SubEvent,
|
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel,
|
||||||
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
|
from pretix.control.forms import SplitDateTimeField
|
||||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||||
from pretix.control.signals import order_search_filter_q
|
from pretix.control.signals import order_search_filter_q
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
@@ -68,7 +69,7 @@ from pretix.helpers.database import (
|
|||||||
get_deterministic_ordering, rolledback_transaction,
|
get_deterministic_ordering, rolledback_transaction,
|
||||||
)
|
)
|
||||||
from pretix.helpers.dicts import move_to_end
|
from pretix.helpers.dicts import move_to_end
|
||||||
from pretix.helpers.i18n import i18ncomp
|
from pretix.helpers.i18n import get_format_without_seconds, i18ncomp
|
||||||
|
|
||||||
PAYMENT_PROVIDERS = []
|
PAYMENT_PROVIDERS = []
|
||||||
|
|
||||||
@@ -579,9 +580,11 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Maximal sum of payments and refunds'),
|
label=_('Maximal sum of payments and refunds'),
|
||||||
)
|
)
|
||||||
sales_channel = forms.ChoiceField(
|
sales_channel = SafeModelChoiceField(
|
||||||
label=_('Sales channel'),
|
label=_('Sales channel'),
|
||||||
required=False,
|
required=False,
|
||||||
|
queryset=SalesChannel.objects.none(),
|
||||||
|
to_field_name="identifier",
|
||||||
)
|
)
|
||||||
has_checkin = forms.NullBooleanField(
|
has_checkin = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -604,9 +607,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
del self.fields['subevents_from']
|
del self.fields['subevents_from']
|
||||||
del self.fields['subevents_to']
|
del self.fields['subevents_to']
|
||||||
|
|
||||||
self.fields['sales_channel'].choices = [('', '')] + [
|
self.fields['sales_channel'].queryset = self.event.organizer.sales_channels.all()
|
||||||
(k, v.verbose_name) for k, v in get_all_sales_channels().items()
|
|
||||||
]
|
|
||||||
|
|
||||||
locale_names = dict(settings.LANGUAGES)
|
locale_names = dict(settings.LANGUAGES)
|
||||||
self.fields['locale'].choices = [('', '')] + [(a, locale_names[a]) for a in self.event.settings.locales]
|
self.fields['locale'].choices = [('', '')] + [(a, locale_names[a]) for a in self.event.settings.locales]
|
||||||
@@ -688,11 +689,71 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
)
|
)
|
||||||
self.fields['quota'].widget.choices = self.fields['quota'].choices
|
self.fields['quota'].widget.choices = self.fields['quota'].choices
|
||||||
for q in self.event.questions.all():
|
for q in self.event.questions.all():
|
||||||
self.fields['question_{}'.format(q.pk)] = forms.CharField(
|
kwargs = {
|
||||||
label=q.question,
|
"label": q.question,
|
||||||
required=False,
|
"required": False,
|
||||||
help_text=_('Exact matches only')
|
}
|
||||||
)
|
fname = 'question_{}'.format(q.pk)
|
||||||
|
if q.type == Question.TYPE_NUMBER:
|
||||||
|
self.fields[fname] = forms.DecimalField(
|
||||||
|
help_text=_('Exact matches only'),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif q.type == Question.TYPE_BOOLEAN:
|
||||||
|
self.fields[fname] = forms.ChoiceField(
|
||||||
|
choices=(
|
||||||
|
("", ""),
|
||||||
|
("True", _("Yes")),
|
||||||
|
("False", _("No")),
|
||||||
|
),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||||
|
self.fields[fname] = forms.ModelChoiceField(
|
||||||
|
queryset=q.options,
|
||||||
|
widget=forms.Select,
|
||||||
|
to_field_name='identifier',
|
||||||
|
empty_label='',
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||||
|
self.fields[fname] = CountryField(
|
||||||
|
countries=CachedCountries,
|
||||||
|
blank=True, null=True, blank_label=' ',
|
||||||
|
).formfield(
|
||||||
|
**kwargs,
|
||||||
|
widget=forms.Select,
|
||||||
|
empty_label=' ',
|
||||||
|
)
|
||||||
|
elif q.type == Question.TYPE_DATE:
|
||||||
|
self.fields[fname] = forms.DateField(
|
||||||
|
widget=DatePickerWidget(),
|
||||||
|
help_text=_('Exact matches only'),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif q.type == Question.TYPE_TIME:
|
||||||
|
self.fields[fname] = forms.TimeField(
|
||||||
|
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||||
|
help_text=_('Exact matches only'),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif q.type == Question.TYPE_DATETIME:
|
||||||
|
self.fields[fname] = SplitDateTimeField(
|
||||||
|
widget=SplitDateTimePickerWidget(
|
||||||
|
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||||
|
min_date=q.valid_datetime_min,
|
||||||
|
max_date=q.valid_datetime_max
|
||||||
|
),
|
||||||
|
help_text=_('Exact matches only'),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif q.type == Question.TYPE_FILE:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.fields[fname] = forms.CharField(
|
||||||
|
help_text=_('Exact matches only'),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
def filter_qs(self, qs):
|
def filter_qs(self, qs):
|
||||||
fdata = self.cleaned_data
|
fdata = self.cleaned_data
|
||||||
@@ -719,7 +780,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
if fdata.get('comment'):
|
if fdata.get('comment'):
|
||||||
qs = qs.filter(comment__icontains=fdata.get('comment'))
|
qs = qs.filter(comment__icontains=fdata.get('comment'))
|
||||||
if fdata.get('sales_channel'):
|
if fdata.get('sales_channel'):
|
||||||
qs = qs.filter(sales_channel=fdata.get('sales_channel'))
|
qs = qs.filter(sales_channel__identifier=fdata.get('sales_channel').identifier)
|
||||||
if fdata.get('total'):
|
if fdata.get('total'):
|
||||||
qs = qs.filter(total=fdata.get('total'))
|
qs = qs.filter(total=fdata.get('total'))
|
||||||
if fdata.get('email_known_to_work') is not None:
|
if fdata.get('email_known_to_work') is not None:
|
||||||
@@ -788,11 +849,24 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
).distinct()
|
).distinct()
|
||||||
for q in self.event.questions.all():
|
for q in self.event.questions.all():
|
||||||
if fdata.get(f'question_{q.pk}'):
|
if fdata.get(f'question_{q.pk}'):
|
||||||
answers = QuestionAnswer.objects.filter(
|
if q.type == Question.TYPE_BOOLEAN:
|
||||||
question_id=q.pk,
|
answers = QuestionAnswer.objects.filter(
|
||||||
orderposition__order_id=OuterRef('pk'),
|
question_id=q.pk,
|
||||||
answer__iexact=fdata.get(f'question_{q.pk}')
|
orderposition__order_id=OuterRef('pk'),
|
||||||
)
|
answer__exact=fdata.get(f'question_{q.pk}')
|
||||||
|
)
|
||||||
|
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||||
|
answers = QuestionAnswer.objects.filter(
|
||||||
|
question_id=q.pk,
|
||||||
|
orderposition__order_id=OuterRef('pk'),
|
||||||
|
options=fdata.get(f'question_{q.pk}')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
answers = QuestionAnswer.objects.filter(
|
||||||
|
question_id=q.pk,
|
||||||
|
orderposition__order_id=OuterRef('pk'),
|
||||||
|
answer__iexact=fdata.get(f'question_{q.pk}')
|
||||||
|
)
|
||||||
qs = qs.annotate(**{f'q_{q.pk}': Exists(answers)}).filter(**{f'q_{q.pk}': True})
|
qs = qs.annotate(**{f'q_{q.pk}': Exists(answers)}).filter(**{f'q_{q.pk}': True})
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
@@ -2578,6 +2652,9 @@ class DeviceFilterForm(FilterForm):
|
|||||||
if fdata.get('gate'):
|
if fdata.get('gate'):
|
||||||
qs = qs.filter(gate=fdata['gate'])
|
qs = qs.filter(gate=fdata['gate'])
|
||||||
|
|
||||||
|
if fdata.get('software_brand'):
|
||||||
|
qs = qs.filter(software_brand=fdata['software_brand'])
|
||||||
|
|
||||||
if fdata.get('state') == 'active':
|
if fdata.get('state') == 'active':
|
||||||
qs = qs.filter(revoked=False)
|
qs = qs.filter(revoked=False)
|
||||||
elif fdata.get('state') == 'revoked':
|
elif fdata.get('state') == 'revoked':
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ from urllib.parse import urlencode
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.uploadedfile import UploadedFile
|
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.forms.formsets import DELETION_FIELD_NAME
|
from django.forms.formsets import DELETION_FIELD_NAME
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -55,7 +54,6 @@ from django_scopes.forms import (
|
|||||||
)
|
)
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
|
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
|
||||||
from pretix.base.forms.widgets import DatePickerWidget
|
from pretix.base.forms.widgets import DatePickerWidget
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -64,8 +62,9 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||||
from pretix.base.signals import item_copy_data
|
from pretix.base.signals import item_copy_data
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
|
ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField,
|
||||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
SalesChannelCheckboxSelectMultiple, SplitDateTimeField,
|
||||||
|
SplitDateTimePickerWidget,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
|
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
@@ -413,7 +412,7 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
'checkin_text',
|
'checkin_text',
|
||||||
'free_price',
|
'free_price',
|
||||||
'original_price',
|
'original_price',
|
||||||
'sales_channels',
|
'all_sales_channels',
|
||||||
'issue_giftcard',
|
'issue_giftcard',
|
||||||
'require_approval',
|
'require_approval',
|
||||||
'allow_waitinglist',
|
'allow_waitinglist',
|
||||||
@@ -443,9 +442,6 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
|
|
||||||
if src.picture:
|
if src.picture:
|
||||||
self.instance.picture.save(os.path.basename(src.picture.name), src.picture)
|
self.instance.picture.save(os.path.basename(src.picture.name), src.picture)
|
||||||
else:
|
|
||||||
# Add to all sales channels by default
|
|
||||||
self.instance.sales_channels = list(get_all_sales_channels().keys())
|
|
||||||
|
|
||||||
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||||
if not self.instance.admission:
|
if not self.instance.admission:
|
||||||
@@ -474,6 +470,8 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if self.cleaned_data.get('copy_from'):
|
if self.cleaned_data.get('copy_from'):
|
||||||
|
if not self.instance.all_sales_channels:
|
||||||
|
self.instance.limit_sales_channels.set(self.cleaned_data['copy_from'].limit_sales_channels.all())
|
||||||
self.instance.require_membership_types.set(
|
self.instance.require_membership_types.set(
|
||||||
self.cleaned_data['copy_from'].require_membership_types.all()
|
self.cleaned_data['copy_from'].require_membership_types.all()
|
||||||
)
|
)
|
||||||
@@ -563,6 +561,13 @@ class TicketNullBooleanSelect(forms.NullBooleanSelect):
|
|||||||
|
|
||||||
|
|
||||||
class ItemUpdateForm(I18nModelForm):
|
class ItemUpdateForm(I18nModelForm):
|
||||||
|
picture = ExtFileField(
|
||||||
|
label=_('Product picture'),
|
||||||
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||||
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
|
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
|
||||||
@@ -574,14 +579,10 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
if self.event.tax_rules.exists():
|
if self.event.tax_rules.exists():
|
||||||
self.fields['tax_rule'].required = True
|
self.fields['tax_rule'].required = True
|
||||||
self.fields['description'].widget.attrs['rows'] = '4'
|
self.fields['description'].widget.attrs['rows'] = '4'
|
||||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||||
label=_('Sales channels'),
|
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||||
required=False,
|
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||||
choices=(
|
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
|
||||||
),
|
|
||||||
widget=forms.CheckboxSelectMultiple
|
|
||||||
)
|
|
||||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||||
|
|
||||||
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
|
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
|
||||||
@@ -756,14 +757,6 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def clean_picture(self):
|
|
||||||
value = self.cleaned_data.get('picture')
|
|
||||||
if isinstance(value, UploadedFile) and value.size > settings.FILE_UPLOAD_MAX_SIZE_IMAGE:
|
|
||||||
raise forms.ValidationError(_("Please do not upload files larger than {size}!").format(
|
|
||||||
size=SizeValidationMixin._sizeof_fmt(settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
|
||||||
))
|
|
||||||
return value
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
@@ -772,7 +765,8 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
'name',
|
'name',
|
||||||
'internal_name',
|
'internal_name',
|
||||||
'active',
|
'active',
|
||||||
'sales_channels',
|
'all_sales_channels',
|
||||||
|
'limit_sales_channels',
|
||||||
'admission',
|
'admission',
|
||||||
'personalized',
|
'personalized',
|
||||||
'description',
|
'description',
|
||||||
@@ -829,6 +823,7 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
'hidden_if_item_available': SafeModelChoiceField,
|
'hidden_if_item_available': SafeModelChoiceField,
|
||||||
'grant_membership_type': SafeModelChoiceField,
|
'grant_membership_type': SafeModelChoiceField,
|
||||||
'require_membership_types': SafeModelMultipleChoiceField,
|
'require_membership_types': SafeModelMultipleChoiceField,
|
||||||
|
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'available_from': SplitDateTimePickerWidget(),
|
'available_from': SplitDateTimePickerWidget(),
|
||||||
@@ -900,18 +895,10 @@ class ItemVariationForm(I18nModelForm):
|
|||||||
qs = kwargs.pop('membership_types')
|
qs = kwargs.pop('membership_types')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||||
label=_('Sales channels'),
|
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||||
required=False,
|
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||||
choices=(
|
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
|
||||||
),
|
|
||||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
|
||||||
'selected here but not on product level, the variation will not be available.'),
|
|
||||||
widget=forms.CheckboxSelectMultiple
|
|
||||||
)
|
|
||||||
if not self.instance.pk:
|
|
||||||
self.initial.setdefault('sales_channels', list(get_all_sales_channels().keys()))
|
|
||||||
|
|
||||||
self.fields['description'].widget.attrs['rows'] = 3
|
self.fields['description'].widget.attrs['rows'] = 3
|
||||||
if qs:
|
if qs:
|
||||||
@@ -983,12 +970,14 @@ class ItemVariationForm(I18nModelForm):
|
|||||||
'available_from_mode',
|
'available_from_mode',
|
||||||
'available_until',
|
'available_until',
|
||||||
'available_until_mode',
|
'available_until_mode',
|
||||||
'sales_channels',
|
'all_sales_channels',
|
||||||
|
'limit_sales_channels',
|
||||||
'hide_without_voucher',
|
'hide_without_voucher',
|
||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'available_from': SplitDateTimeField,
|
'available_from': SplitDateTimeField,
|
||||||
'available_until': SplitDateTimeField,
|
'available_until': SplitDateTimeField,
|
||||||
|
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'available_from': SplitDateTimePickerWidget(),
|
'available_from': SplitDateTimePickerWidget(),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ from django_scopes.forms import SafeModelChoiceField
|
|||||||
from i18nfield.forms import (
|
from i18nfield.forms import (
|
||||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
||||||
)
|
)
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
from phonenumber_field.formfields import PhoneNumberField
|
from phonenumber_field.formfields import PhoneNumberField
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ from pretix.api.models import WebHook
|
|||||||
from pretix.api.webhooks import get_all_webhook_events
|
from pretix.api.webhooks import get_all_webhook_events
|
||||||
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
|
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
|
||||||
from pretix.base.forms import (
|
from pretix.base.forms import (
|
||||||
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
|
SECRET_REDACTED, I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator,
|
||||||
|
SecretKeySettingsField, SettingsForm,
|
||||||
)
|
)
|
||||||
from pretix.base.forms.questions import (
|
from pretix.base.forms.questions import (
|
||||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
|
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
|
||||||
@@ -68,7 +70,8 @@ from pretix.base.forms.widgets import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
||||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||||
|
SalesChannel, Team,
|
||||||
)
|
)
|
||||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||||
from pretix.base.models.organizer import OrganizerFooterLink
|
from pretix.base.models.organizer import OrganizerFooterLink
|
||||||
@@ -956,7 +959,7 @@ class SSOProviderForm(I18nModelForm):
|
|||||||
label=pgettext_lazy('sso_oidc', 'Client ID'),
|
label=pgettext_lazy('sso_oidc', 'Client ID'),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
config_oidc_client_secret = forms.CharField(
|
config_oidc_client_secret = SecretKeySettingsField(
|
||||||
label=pgettext_lazy('sso_oidc', 'Client secret'),
|
label=pgettext_lazy('sso_oidc', 'Client secret'),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
@@ -1013,7 +1016,13 @@ class SSOProviderForm(I18nModelForm):
|
|||||||
if self.instance and self.instance.method == method:
|
if self.instance and self.instance.method == method:
|
||||||
f.initial = self.instance.configuration.get(suffix)
|
f.initial = self.instance.configuration.get(suffix)
|
||||||
|
|
||||||
|
def _unmask_secret_fields(self):
|
||||||
|
for k, v in self.cleaned_data.items():
|
||||||
|
if isinstance(self.fields.get(k), SecretKeySettingsField) and self.cleaned_data.get(k) == SECRET_REDACTED:
|
||||||
|
self.cleaned_data[k] = self.fields[k].initial
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
self._unmask_secret_fields()
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
if not data.get("method"):
|
if not data.get("method"):
|
||||||
return data
|
return data
|
||||||
@@ -1090,3 +1099,40 @@ class GiftCardAcceptanceInviteForm(forms.Form):
|
|||||||
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
|
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
|
||||||
raise ValidationError(_('The selected organizer has already been invited.'))
|
raise ValidationError(_('The selected organizer has already been invited.'))
|
||||||
return acceptor
|
return acceptor
|
||||||
|
|
||||||
|
|
||||||
|
class SalesChannelForm(I18nModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = SalesChannel
|
||||||
|
fields = ['label', 'identifier']
|
||||||
|
widgets = {
|
||||||
|
'default': forms.TextInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.type = kwargs.pop("type")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if not self.type.multiple_allowed or (self.instance and self.instance.pk):
|
||||||
|
self.fields["identifier"].initial = self.type.identifier
|
||||||
|
self.fields["identifier"].disabled = True
|
||||||
|
self.fields["label"].initial = LazyI18nString.from_gettext(self.type.verbose_name)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
d = super().clean()
|
||||||
|
|
||||||
|
if self.instance.pk:
|
||||||
|
d["identifier"] = self.instance.identifier
|
||||||
|
elif self.type.multiple_allowed:
|
||||||
|
d["identifier"] = self.type.identifier + "." + d["identifier"]
|
||||||
|
else:
|
||||||
|
d["identifier"] = self.type.identifier
|
||||||
|
|
||||||
|
if not self.instance.pk:
|
||||||
|
# self.event is actually the organizer, sorry I18nModelForm!
|
||||||
|
if self.event.sales_channels.filter(identifier=d["identifier"]).exists():
|
||||||
|
raise ValidationError(
|
||||||
|
_("A sales channel with the same identifier already exists.")
|
||||||
|
)
|
||||||
|
|
||||||
|
return d
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ class ControlFieldRenderer(FieldRenderer):
|
|||||||
attrs = ''
|
attrs = ''
|
||||||
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
|
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
|
||||||
|
|
||||||
|
def wrap_widget(self, html):
|
||||||
|
if isinstance(self.widget, CheckboxInput):
|
||||||
|
css_class = "checkbox"
|
||||||
|
if self.field.field.disabled:
|
||||||
|
css_class += " disabled"
|
||||||
|
html = f'<div class="{css_class}">{html}</div>'
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
|
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from pretix.base.forms import (
|
|||||||
I18nModelForm, MarkdownTextarea, PlaceholderValidator,
|
I18nModelForm, MarkdownTextarea, PlaceholderValidator,
|
||||||
)
|
)
|
||||||
from pretix.base.forms.widgets import format_placeholders_help_text
|
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||||
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Item, Voucher
|
from pretix.base.models import Item, Voucher
|
||||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||||
@@ -289,8 +290,9 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('You can either supply a list of email addresses with one email address per line, or a CSV file with a title column '
|
help_text=_('You can either supply a list of email addresses with one email address per line, or the contents '
|
||||||
'and one or more of the columns "email", "number", "name", or "tag".')
|
'of a CSV file with a title column and one or more of the columns "email", "number", "name", '
|
||||||
|
'or "tag".')
|
||||||
)
|
)
|
||||||
Recipient = namedtuple('Recipient', 'email number name tag')
|
Recipient = namedtuple('Recipient', 'email number name tag')
|
||||||
|
|
||||||
@@ -332,6 +334,11 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._set_field_placeholders('send_subject', ['event', 'name'])
|
self._set_field_placeholders('send_subject', ['event', 'name'])
|
||||||
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'])
|
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'])
|
||||||
|
|
||||||
|
with language(self.instance.event.settings.locale, self.instance.event.settings.region):
|
||||||
|
for f in ("send_subject", "send_message"):
|
||||||
|
self.fields[f].initial = str(self.fields[f].initial)
|
||||||
|
|
||||||
if 'seat' in self.fields:
|
if 'seat' in self.fields:
|
||||||
self.fields['seats'] = forms.CharField(
|
self.fields['seats'] = forms.CharField(
|
||||||
label=_("Specific seat IDs"),
|
label=_("Specific seat IDs"),
|
||||||
|
|||||||
@@ -357,6 +357,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
'pretix.membershiptype.created': _('The membership type has been created.'),
|
'pretix.membershiptype.created': _('The membership type has been created.'),
|
||||||
'pretix.membershiptype.changed': _('The membership type has been changed.'),
|
'pretix.membershiptype.changed': _('The membership type has been changed.'),
|
||||||
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
|
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
|
||||||
|
'pretix.saleschannel.created': _('The sales channel has been created.'),
|
||||||
|
'pretix.saleschannel.changed': _('The sales channel has been changed.'),
|
||||||
|
'pretix.saleschannel.deleted': _('The sales channel has been deleted.'),
|
||||||
'pretix.customer.created': _('The account has been created.'),
|
'pretix.customer.created': _('The account has been created.'),
|
||||||
'pretix.customer.changed': _('The account has been changed.'),
|
'pretix.customer.changed': _('The account has been changed.'),
|
||||||
'pretix.customer.membership.created': _('A membership for this account has been added.'),
|
'pretix.customer.membership.created': _('A membership for this account has been added.'),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user