forked from CGM_Public/pretix_original
Compare commits
322
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3113fdb53a | ||
|
|
f2f1d548df | ||
|
|
3b98d87a26 | ||
|
|
f045062055 | ||
|
|
eb501dd1ea | ||
|
|
2d8793c355 | ||
|
|
d32bd717b7 | ||
|
|
6e6b75d55e | ||
|
|
50b5f760bb | ||
|
|
9ab2e61c31 | ||
|
|
4876a0b61f | ||
|
|
56bbcb65c3 | ||
|
|
5bb1cb498f | ||
|
|
6bf23b0fdd | ||
|
|
5deb1a8c69 | ||
|
|
1523137300 | ||
|
|
04ef097eb1 | ||
|
|
a5d4434a64 | ||
|
|
3b3c668153 | ||
|
|
d339d67111 | ||
|
|
a8aee6c824 | ||
|
|
6466987493 | ||
|
|
ff962805cd | ||
|
|
2b5f46164f | ||
|
|
d74451ded1 | ||
|
|
62f0c82d8d | ||
|
|
5b587774bb | ||
|
|
88ea8ee2ea | ||
|
|
56e0ab8378 | ||
|
|
a9ae237b1a | ||
|
|
27823b7bf6 | ||
|
|
4231cd2576 | ||
|
|
6aa5196f18 | ||
|
|
c1eac5e91e | ||
|
|
410e06364a | ||
|
|
ae137f8f16 | ||
|
|
f9f3f9f868 | ||
|
|
395eadde47 | ||
|
|
2be790fa45 | ||
|
|
f9d78eaf1a | ||
|
|
2d5d27e950 | ||
|
|
c6fa19d771 | ||
|
|
3129253eef | ||
|
|
b69ab86458 | ||
|
|
80f7ae0b76 | ||
|
|
160f9a4363 | ||
|
|
24b5b9373d | ||
|
|
178c40aee6 | ||
|
|
49c41878d2 | ||
|
|
fa4c29cf23 | ||
|
|
75b93eebc5 | ||
|
|
5a406abdd6 | ||
|
|
6712baf534 | ||
|
|
4d9243151f | ||
|
|
b89a4f7b32 | ||
|
|
c80d5b1bb2 | ||
|
|
0334c2f433 | ||
|
|
6bc46b7aec | ||
|
|
3ebe622189 | ||
|
|
25fb1ee3be | ||
|
|
a3586a73f1 | ||
|
|
93eb041acc | ||
|
|
63894ca3da | ||
|
|
73b2cce435 | ||
|
|
0a711f4965 | ||
|
|
75bf200aac | ||
|
|
11307de30a | ||
|
|
863db60786 | ||
|
|
f5a1adedca | ||
|
|
ea74688633 | ||
|
|
57738f19bf | ||
|
|
7b5ce5e198 | ||
|
|
d5f9beef69 | ||
|
|
eee39b1300 | ||
|
|
c2fdea020d | ||
|
|
f87e089734 | ||
|
|
0fad7472c0 | ||
|
|
bd0a223066 | ||
|
|
782c1a5d39 | ||
|
|
5c99d3bf69 | ||
|
|
5e6307acc9 | ||
|
|
d4bfa9d773 | ||
|
|
b7f540251c | ||
|
|
cb17d80b63 | ||
|
|
86b28b9b53 | ||
|
|
fac404631c | ||
|
|
fd547014a9 | ||
|
|
d23a625415 | ||
|
|
199416b904 | ||
|
|
0a7a113b4e | ||
|
|
5978a715b5 | ||
|
|
71beb54eb4 | ||
|
|
b92981353f | ||
|
|
a5f7115e19 | ||
|
|
f5e3d4b0bc | ||
|
|
6fba080b8f | ||
|
|
365ccf159e | ||
|
|
b40a41d742 | ||
|
|
bf1081071b | ||
|
|
e261ce7554 | ||
|
|
7cae0ceab8 | ||
|
|
064ee91225 | ||
|
|
495b3ec770 | ||
|
|
39f9329207 | ||
|
|
2b72cfdaff | ||
|
|
70d32ea1aa | ||
|
|
0871482681 | ||
|
|
5d4f3eab06 | ||
|
|
fa3265b1fb | ||
|
|
92e6ffc7ef | ||
|
|
22f91f7aa2 | ||
|
|
43facd1e43 | ||
|
|
11242a2325 | ||
|
|
28db9a5262 | ||
|
|
57e8c6aafd | ||
|
|
fa47f63307 | ||
|
|
bac673f3ab | ||
|
|
45ac391998 | ||
|
|
fed5097708 | ||
|
|
9d115c30d7 | ||
|
|
a769da62c7 | ||
|
|
e20edab98f | ||
|
|
4f4fcb84ce | ||
|
|
6560d161c9 | ||
|
|
7f23c590ca | ||
|
|
0ca33eddb1 | ||
|
|
45341c4a31 | ||
|
|
5de5ae4ca2 | ||
|
|
03f71f3cdf | ||
|
|
f97ad66026 | ||
|
|
31392e5852 | ||
|
|
77a5a685f1 | ||
|
|
2b77e59e0a | ||
|
|
70f755599a | ||
|
|
2a76b2a5dd | ||
|
|
ffea243eae | ||
|
|
a4012e6100 | ||
|
|
9bd250f9fc | ||
|
|
7f93fb6b4d | ||
|
|
1b3f27852d | ||
|
|
fab29088e6 | ||
|
|
9feea75bdb | ||
|
|
52f08c818f | ||
|
|
e89483e3f2 | ||
|
|
467469f26d | ||
|
|
aa9d526207 | ||
|
|
3338858420 | ||
|
|
3e758c6a1c | ||
|
|
fc7e69523c | ||
|
|
3289b4411a | ||
|
|
5c98512241 | ||
|
|
3ca22933f4 | ||
|
|
106d3f3c74 | ||
|
|
15d3fbc02e | ||
|
|
eaae7e9ea2 | ||
|
|
9eb1c5047b | ||
|
|
25d4b603be | ||
|
|
eac88b5ef7 | ||
|
|
734b376e05 | ||
|
|
2354affde7 | ||
|
|
11e922b1a9 | ||
|
|
b7f0c3cc6c | ||
|
|
f9f629299b | ||
|
|
645c9d5900 | ||
|
|
6af2d38a98 | ||
|
|
dba8e80868 | ||
|
|
324ff2ab67 | ||
|
|
5ea09a5b0a | ||
|
|
c046caa894 | ||
|
|
be8197825c | ||
|
|
d48c28bec7 | ||
|
|
b07c1802a0 | ||
|
|
7f0cf1655a | ||
|
|
0a154dc79f | ||
|
|
7022e5bf49 | ||
|
|
86ae18564d | ||
|
|
a07a04d115 | ||
|
|
51e7c889ec | ||
|
|
4cd2381a5e | ||
|
|
6474240d49 | ||
|
|
0938bf3246 | ||
|
|
aad94f1b2a | ||
|
|
a677575eef | ||
|
|
9160e01333 | ||
|
|
d2b9fe475b | ||
|
|
cff9ae6b18 | ||
|
|
4fb49820af | ||
|
|
1f465ddddb | ||
|
|
1bda102d46 | ||
|
|
8066a7ed0f | ||
|
|
bc960ffde1 | ||
|
|
cbbbf44f1f | ||
|
|
28ab0d118c | ||
|
|
15710cbec9 | ||
|
|
4aaeee64d0 | ||
|
|
ffd3882aa8 | ||
|
|
36a90c4e76 | ||
|
|
098580ccf4 | ||
|
|
c0f8c9b9e4 | ||
|
|
a1a553eb52 | ||
|
|
18e1e7716c | ||
|
|
081d3a73fa | ||
|
|
4234a6440d | ||
|
|
a09275c582 | ||
|
|
a8099a1284 | ||
|
|
2f19b48f97 | ||
|
|
abbf7bdd69 | ||
|
|
6cf8c7d4f0 | ||
|
|
c6196f9900 | ||
|
|
cd7850337b | ||
|
|
0220965ca9 | ||
|
|
bae1512235 | ||
|
|
71f8a3ad3e | ||
|
|
24dd065839 | ||
|
|
08f0150177 | ||
|
|
367a4fc64e | ||
|
|
33ace8554b | ||
|
|
94cbb19db0 | ||
|
|
ea33c7b1b9 | ||
|
|
cba5fd4c27 | ||
|
|
297999a00e | ||
|
|
cbbf13e9e2 | ||
|
|
4437706494 | ||
|
|
3fff023a8a | ||
|
|
583429b6d9 | ||
|
|
61c12ca871 | ||
|
|
e3145b79d5 | ||
|
|
1d8f789c21 | ||
|
|
eb8e36e1eb | ||
|
|
21d8d93bed | ||
|
|
f2e09c020b | ||
|
|
ebe5647066 | ||
|
|
5b5e831e37 | ||
|
|
8a155d5fe3 | ||
|
|
2c67b82f4a | ||
|
|
7a2878657d | ||
|
|
90f9dd2bde | ||
|
|
6c292caa62 | ||
|
|
d3e73c4fc2 | ||
|
|
bfe0b218de | ||
|
|
0f44702640 | ||
|
|
8185175d14 | ||
|
|
8c24209e55 | ||
|
|
5c80b75d09 | ||
|
|
79c00dfa6c | ||
|
|
3793300d45 | ||
|
|
a70cd792c8 | ||
|
|
7c479c1df6 | ||
|
|
efd074a1ee | ||
|
|
7d27440003 | ||
|
|
269f7bf789 | ||
|
|
c96bfab70f | ||
|
|
ec63456b9a | ||
|
|
3a39eb3815 | ||
|
|
ce0ab8bd07 | ||
|
|
289496d6d2 | ||
|
|
abe45bccae | ||
|
|
a8893bdc96 | ||
|
|
cb76956887 | ||
|
|
809b971b4f | ||
|
|
d5f2f0e3af | ||
|
|
301d9de18e | ||
|
|
dcb1d920eb | ||
|
|
fabe476397 | ||
|
|
3101660a3e | ||
|
|
f8b677a2a9 | ||
|
|
15643f8dd5 | ||
|
|
fbaa0d667f | ||
|
|
50c5a2e8d9 | ||
|
|
7bc559acab | ||
|
|
c58a1aaa48 | ||
|
|
385c8c6ef1 | ||
|
|
d3457ce5b6 | ||
|
|
7087f783e6 | ||
|
|
8c8650090d | ||
|
|
12804ff1a0 | ||
|
|
4cb271868b | ||
|
|
9d546904e6 | ||
|
|
092b51f8f3 | ||
|
|
4a49519869 | ||
|
|
aa121b900e | ||
|
|
9e3ce4f1ec | ||
|
|
2e5385ca32 | ||
|
|
f5dfe31fdd | ||
|
|
032974e9ce | ||
|
|
31c086e2f0 | ||
|
|
3c4de2aecd | ||
|
|
8d9543c01e | ||
|
|
608d82ce4f | ||
|
|
7f0ed374b5 | ||
|
|
c7c9c95fbb | ||
|
|
5667da9ad2 | ||
|
|
558d0f1a4e | ||
|
|
0e832a7c41 | ||
|
|
7f948bf263 | ||
|
|
d4a32fd39d | ||
|
|
7bd5fc7ae0 | ||
|
|
36e8a4bf62 | ||
|
|
5ab01eb537 | ||
|
|
451ef2e734 | ||
|
|
2e657940bc | ||
|
|
6ef542837a | ||
|
|
619126bd24 | ||
|
|
1bdc49e087 | ||
|
|
cf4ee53f0d | ||
|
|
d742aa2f48 | ||
|
|
b7f693b934 | ||
|
|
c5ede32649 | ||
|
|
f543cf2da5 | ||
|
|
40cdb0c507 | ||
|
|
7a8b3d08df | ||
|
|
12a898476e | ||
|
|
2acf043872 | ||
|
|
062395dac1 | ||
|
|
6fc76d4122 | ||
|
|
859df96afc | ||
|
|
b95da484a0 | ||
|
|
d6d6b73a38 | ||
|
|
c8b8fba171 | ||
|
|
4580d6f710 | ||
|
|
2a19a423de | ||
|
|
409c1eef30 |
@@ -10,7 +10,9 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
versioning-strategy: increase
|
versioning-strategy: increase
|
||||||
|
open-pull-requests-limit: 10
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/src/pretix/static/npm_dir"
|
directory: "/src/pretix/static/npm_dir"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "monthly"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ Example::
|
|||||||
currency=EUR
|
currency=EUR
|
||||||
datadir=/data
|
datadir=/data
|
||||||
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
|
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
|
||||||
cookie_domain=.pretix.de
|
|
||||||
|
|
||||||
``instance_name``
|
``instance_name``
|
||||||
The name of this installation. Default: ``pretix.de``
|
The name of this installation. Default: ``pretix.de``
|
||||||
@@ -71,9 +70,6 @@ Example::
|
|||||||
``auth_backends``
|
``auth_backends``
|
||||||
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
||||||
|
|
||||||
``cookie_domain``
|
|
||||||
The cookie domain to be set. Defaults to ``None``.
|
|
||||||
|
|
||||||
``registration``
|
``registration``
|
||||||
Enables or disables the registration of new admin users. Defaults to ``off``.
|
Enables or disables the registration of new admin users. Defaults to ``off``.
|
||||||
|
|
||||||
@@ -349,7 +345,7 @@ to speed up various operations::
|
|||||||
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
|
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
|
||||||
or ``unix://[:password]@/path/to/socket.sock?db=0``
|
or ``unix://[:password]@/path/to/socket.sock?db=0``
|
||||||
|
|
||||||
``session``
|
``sessions``
|
||||||
When this is set to ``True``, redis will be used as the session storage.
|
When this is set to ``True``, redis will be used as the session storage.
|
||||||
|
|
||||||
``sentinels``
|
``sentinels``
|
||||||
@@ -525,4 +521,4 @@ pretix can optionally make use of a GeoIP database for some features. It needs a
|
|||||||
|
|
||||||
|
|
||||||
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
|
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
|
||||||
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ If you want the user to return to your application after the payment is complete
|
|||||||
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
||||||
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
||||||
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
||||||
either enter ``https://example.org`` or ``https://example.org/order/``.
|
either enter ``https://example.org/order/`` or ``https://example.org/``.
|
||||||
|
Please note that in the latter case the trailing slash is required, ``https://example.org`` is not allowed to prevent.
|
||||||
|
Only base URLs with a secure (``https://``) or local (``http://localhost``) origin are permitted.
|
||||||
|
|
||||||
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
||||||
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
|
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ geo_lon float Longitude of th
|
|||||||
has_subevents boolean ``true`` if the event series feature is active for this
|
has_subevents boolean ``true`` if the event series feature is active for this
|
||||||
event. Cannot change after event is created.
|
event. Cannot change after event is created.
|
||||||
meta_data object Values set for organizer-specific meta data parameters.
|
meta_data object Values set for organizer-specific meta data parameters.
|
||||||
|
The allowed keys need to be set up as meta properties
|
||||||
|
in the organizer configuration.
|
||||||
plugins list A list of package names of the enabled plugins for this
|
plugins list A list of package names of the enabled plugins for this
|
||||||
event.
|
event.
|
||||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
seating_plan integer If reserved seating is in use, the ID of a seating
|
||||||
@@ -343,8 +345,8 @@ Endpoints
|
|||||||
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
|
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
|
||||||
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||||
|
|
||||||
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
|
If the ``plugins``, ``has_subevents``, ``meta_data`` and/or ``is_public`` fields are present in the post body this will
|
||||||
value. Otherwise their value will be copied from the existing event.
|
determine their value. Otherwise their value will be copied from the existing event.
|
||||||
|
|
||||||
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
||||||
when creating a new event for this instead.
|
when creating a new event for this instead.
|
||||||
|
|||||||
@@ -45,8 +45,16 @@ sales_channels list of strings Sales channels
|
|||||||
available.
|
available.
|
||||||
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
|
||||||
|
if unavailable due to the available_from setting.
|
||||||
|
If ``info``, the variation is visible, but can't be purchased,
|
||||||
|
and a note explaining the unavailability is displayed.
|
||||||
available_until datetime The last date time at which this variation can be bought
|
available_until datetime The last date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
|
available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
|
if unavailable due to the available_until setting.
|
||||||
|
If ``info``, the variation is visible, but can't be purchased,
|
||||||
|
and a note explaining the unavailability is displayed.
|
||||||
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||||
redemption process, but not in the normal shop
|
redemption process, but not in the normal shop
|
||||||
frontend.
|
frontend.
|
||||||
@@ -105,7 +113,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Test2"
|
"en": "Test2"
|
||||||
@@ -131,7 +141,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": {},
|
"description": {},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
@@ -192,7 +204,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 0,
|
"position": 0,
|
||||||
@@ -232,7 +246,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 0,
|
"position": 0,
|
||||||
@@ -263,7 +279,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 0,
|
"position": 0,
|
||||||
@@ -325,7 +343,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
|||||||
@@ -50,8 +50,16 @@ sales_channels list of strings Sales channel
|
|||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||||
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
|
||||||
|
if unavailable due to the ``available_from`` setting.
|
||||||
|
If ``info``, the item is visible, but can't be purchased,
|
||||||
|
and a note explaining the unavailability is displayed.
|
||||||
available_until datetime The last date time at which this item can be bought
|
available_until datetime The last date time at which this item can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
|
available_until_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||||
|
if unavailable due to the ``available_until`` setting.
|
||||||
|
If ``info``, the item is visible, but can't be purchased,
|
||||||
|
and a note explaining the unavailability is displayed.
|
||||||
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
|
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
|
||||||
set, this item won't be shown publicly as long as this
|
set, this item won't be shown publicly as long as this
|
||||||
quota is available.
|
quota is available.
|
||||||
@@ -156,8 +164,16 @@ variations list of objects A list with o
|
|||||||
available.
|
available.
|
||||||
├ 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
|
||||||
|
if unavailable due to the ``available_from`` setting.
|
||||||
|
If ``info``, the variation is visible, but can't be purchased,
|
||||||
|
and a note explaining the unavailability is displayed.
|
||||||
├ available_until datetime The last date time at which this variation can be bought
|
├ available_until datetime The last date time at which this variation can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
|
├ available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||||
|
if unavailable due to the ``available_until`` setting.
|
||||||
|
If ``info``, the variation is visible, but can't be purchased,
|
||||||
|
and a note explaining the unavailability is displayed.
|
||||||
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||||
redemption process, but not in the normal shop
|
redemption process, but not in the normal shop
|
||||||
frontend.
|
frontend.
|
||||||
@@ -279,7 +295,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -324,7 +342,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -344,7 +364,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -417,7 +439,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -463,7 +487,9 @@ Endpoints
|
|||||||
"description": null,
|
"description": null,
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"position": 0
|
"position": 0
|
||||||
@@ -482,7 +508,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -536,7 +564,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -580,7 +610,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -600,7 +632,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -642,7 +676,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -687,7 +723,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -707,7 +745,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -780,7 +820,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -825,7 +867,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -845,7 +889,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
|
|||||||
@@ -137,13 +137,17 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2023.9
|
||||||
|
|
||||||
|
The ``customer`` query parameter has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2023.10
|
.. versionchanged:: 2023.10
|
||||||
|
|
||||||
The ``checkin_text`` attribute has been added.
|
The ``checkin_text`` attribute has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2023.9
|
.. versionchanged:: 2024.1
|
||||||
|
|
||||||
The ``customer`` query parameter has been added.
|
The ``expires`` attribute can now be passed during order creation.
|
||||||
|
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
@@ -175,6 +179,11 @@ country string Attendee countr
|
|||||||
state string Attendee state (ISO 3166-2 code). Only supported in
|
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||||
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||||
|
voucher_budget_use money (string) Amount of money discounted by the voucher, corresponding
|
||||||
|
to how much of the ``budget`` of the voucher is consumed.
|
||||||
|
**Important:** Do not rely on this amount to be a useful
|
||||||
|
value if the position's price, product or voucher
|
||||||
|
are changed *after* the order was created. Can be ``null``.
|
||||||
tax_rate decimal (string) VAT rate applied for this position
|
tax_rate decimal (string) VAT rate applied for this position
|
||||||
tax_value money (string) VAT included in this position
|
tax_value money (string) VAT included in this position
|
||||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||||
@@ -363,6 +372,7 @@ List of all orders
|
|||||||
"country": "DE",
|
"country": "DE",
|
||||||
"state": null,
|
"state": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
|
"voucher_budget_use": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
@@ -585,6 +595,7 @@ Fetching individual orders
|
|||||||
"country": "DE",
|
"country": "DE",
|
||||||
"state": null,
|
"state": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
|
"voucher_budget_use": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
@@ -729,6 +740,8 @@ Updating order fields
|
|||||||
|
|
||||||
* ``valid_if_pending``
|
* ``valid_if_pending``
|
||||||
|
|
||||||
|
* ``expires``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -1535,6 +1548,7 @@ List of all order positions
|
|||||||
},
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
|
"voucher_budget_use": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
@@ -1648,6 +1662,7 @@ Fetching individual positions
|
|||||||
},
|
},
|
||||||
"attendee_email": null,
|
"attendee_email": null,
|
||||||
"voucher": null,
|
"voucher": null,
|
||||||
|
"voucher_budget_use": null,
|
||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ 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_channels, 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
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
.. highlight:: python
|
.. highlight:: python
|
||||||
:linenothreshold: 5
|
:linenothreshold: 5
|
||||||
|
|
||||||
Writing an e-mail placeholder plugin
|
Writing a template placeholder plugin
|
||||||
====================================
|
=====================================
|
||||||
|
|
||||||
An email placeholder is a dynamic value that pretix users can use in their email templates.
|
A template placeholder is a dynamic value that pretix users can use in their email templates and in other
|
||||||
|
configurable texts.
|
||||||
|
|
||||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||||
|
|
||||||
@@ -12,31 +13,31 @@ Placeholder registration
|
|||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
The placeholder API does not make a lot of usage from signals, however, it
|
The placeholder API does not make a lot of usage from signals, however, it
|
||||||
does use a signal to get a list of all available email placeholders. Your plugin
|
does use a signal to get a list of all available placeholders. Your plugin
|
||||||
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:
|
should listen for this signal and return an instance of a subclass of ``pretix.base.services.placeholders.BaseTextPlaceholder``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from pretix.base.signals import register_mail_placeholders
|
from pretix.base.signals import register_text_placeholders
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
|
@receiver(register_text_placeholders, dispatch_uid="placeholder_custom")
|
||||||
def register_mail_renderers(sender, **kwargs):
|
def register_placeholder_renderers(sender, **kwargs):
|
||||||
from .email import MyPlaceholderClass
|
from .placeholders import MyPlaceholderClass
|
||||||
return MyPlaceholder()
|
return MyPlaceholder()
|
||||||
|
|
||||||
|
|
||||||
Context mechanism
|
Context mechanism
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
|
Templates are used in different "contexts" within pretix. For example, many emails are rendered from
|
||||||
the context of an order, but some are not, such as the notification of a waiting list voucher.
|
templates in the context of an order, but some are not, such as the notification of a waiting list voucher.
|
||||||
|
|
||||||
Not all placeholders make sense in every email, and placeholders usually depend some parameters
|
Not all placeholders make sense everywhere, and placeholders usually depend on some parameters
|
||||||
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
|
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
|
||||||
what values they depend on and they will only be available in an email if all those dependencies are
|
what values they depend on and they will only be available in a context where all those dependencies are
|
||||||
met. Currently, placeholders can depend on the following context parameters:
|
met. Currently, placeholders can depend on the following context parameters:
|
||||||
|
|
||||||
* ``event``
|
* ``event``
|
||||||
@@ -51,7 +52,7 @@ There are a few more that are only to be used internally but not by plugins.
|
|||||||
The placeholder class
|
The placeholder class
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
.. class:: pretix.base.email.BaseMailTextPlaceholder
|
.. class:: pretix.base.services.placeholders.BaseTextPlaceholder
|
||||||
|
|
||||||
.. autoattribute:: identifier
|
.. autoattribute:: identifier
|
||||||
|
|
||||||
@@ -77,7 +78,15 @@ functions:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
placeholder = SimpleFunctionalMailTextPlaceholder(
|
placeholder = SimpleFunctionalTextPlaceholder(
|
||||||
'code', ['order'], lambda order: order.code, sample='F8VVL'
|
'code', ['order'], lambda order: order.code, sample='F8VVL'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Signals
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. automodule:: pretix.base.signals
|
||||||
|
:members: register_text_placeholders
|
||||||
|
.. automodule:: pretix.base.signals
|
||||||
|
:members: register_mail_placeholders
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ transactions list of objects Transactions in
|
|||||||
├ checksum string Checksum computed from payer, reference, amount and
|
├ checksum string Checksum computed from payer, reference, amount and
|
||||||
date
|
date
|
||||||
├ payer string Payment source
|
├ payer string Payment source
|
||||||
|
├ external_id string Unique ID of the payment from an external source
|
||||||
├ reference string Payment reference
|
├ reference string Payment reference
|
||||||
├ amount string Payment amount
|
├ amount string Payment amount
|
||||||
├ iban string Payment IBAN
|
├ iban string Payment IBAN
|
||||||
@@ -85,6 +86,7 @@ Endpoints
|
|||||||
"date": "26.06.2017",
|
"date": "26.06.2017",
|
||||||
"payer": "John Doe",
|
"payer": "John Doe",
|
||||||
"order": null,
|
"order": null,
|
||||||
|
"external_id": null,
|
||||||
"iban": "",
|
"iban": "",
|
||||||
"bic": "",
|
"bic": "",
|
||||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||||
@@ -139,6 +141,7 @@ Endpoints
|
|||||||
"iban": "",
|
"iban": "",
|
||||||
"bic": "",
|
"bic": "",
|
||||||
"order": null,
|
"order": null,
|
||||||
|
"external_id": null,
|
||||||
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
"checksum": "5de03a601644dfa63420dacfd285565f8375a8f2",
|
||||||
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
|
"reference": "GUTSCHRIFT\r\nSAMPLECONF-NAB12 EREF: SAMPLECONF-NAB12\r\nIBAN: DE1234556…",
|
||||||
"state": "nomatch",
|
"state": "nomatch",
|
||||||
|
|||||||
@@ -34,12 +34,16 @@ internal_id string Can be used for
|
|||||||
contact_name string Contact person (or ``null``)
|
contact_name string Contact person (or ``null``)
|
||||||
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
||||||
contact_email string Contact person email address (or ``null``)
|
contact_email string Contact person email address (or ``null``)
|
||||||
|
contact_cc_email string Copy email addresses, can be multiple separated by comma (or ``null``)
|
||||||
booth string Booth number (or ``null``). Maximum 100 characters.
|
booth string Booth number (or ``null``). Maximum 100 characters.
|
||||||
locale string Locale for communication with the exhibitor.
|
locale string Locale for communication with the exhibitor.
|
||||||
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
||||||
|
lead_scanning_access_code string Access code for the exhibitor to use the lead scanning app but not access data (read-only).
|
||||||
allow_lead_scanning boolean Enables lead scanning app
|
allow_lead_scanning boolean Enables lead scanning app
|
||||||
allow_lead_access boolean Enables access to data gathered by the lead scanning app
|
allow_lead_access boolean Enables access to data gathered by the lead scanning app
|
||||||
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
|
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
|
||||||
|
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
|
||||||
|
per scanning device, instead of only per exhibitor.
|
||||||
comment string Internal comment, not shown to exhibitor
|
comment string Internal comment, not shown to exhibitor
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
@@ -62,6 +66,7 @@ data list of objects Attendee data s
|
|||||||
except in a few cases where it contains an additional list of objects
|
except in a few cases where it contains an additional list of objects
|
||||||
with ``value`` and ``label`` keys (e.g. splitting of names).
|
with ``value`` and ``label`` keys (e.g. splitting of names).
|
||||||
device_name string User-defined name for the device used for scanning (or ``null``).
|
device_name string User-defined name for the device used for scanning (or ``null``).
|
||||||
|
device_uuid string UUID of device used for scanning (or ``null``).
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
@@ -105,9 +110,12 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU8",
|
"access_code": "VKHZ2FU84",
|
||||||
|
"lead_scanning_access_code": "WVK2B8PZ",
|
||||||
|
"lead_scanning_scope_by_device": false,
|
||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
@@ -156,9 +164,12 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU8",
|
"access_code": "VKHZ2FU84",
|
||||||
|
"lead_scanning_access_code": "WVK2B8PZ",
|
||||||
|
"lead_scanning_scope_by_device": false,
|
||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
@@ -357,6 +368,7 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
@@ -386,9 +398,12 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU8",
|
"access_code": "VKHZ2FU84",
|
||||||
|
"lead_scanning_access_code": "WVK2B8PZ",
|
||||||
|
"lead_scanning_scope_by_device": false,
|
||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
@@ -444,9 +459,12 @@ Endpoints
|
|||||||
"title": "Dr"
|
"title": "Dr"
|
||||||
},
|
},
|
||||||
"contact_email": "johnson@as.example.org",
|
"contact_email": "johnson@as.example.org",
|
||||||
|
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"locale": "de",
|
"locale": "de",
|
||||||
"access_code": "VKHZ2FU8",
|
"access_code": "VKHZ2FU84",
|
||||||
|
"lead_scanning_access_code": "WVK2B8PZ",
|
||||||
|
"lead_scanning_scope_by_device": false,
|
||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
@@ -561,6 +579,7 @@ name string Exhibitor name
|
|||||||
booth string Booth number (or ``null``)
|
booth string Booth number (or ``null``)
|
||||||
event object Object describing the event
|
event object Object describing the event
|
||||||
├ name multi-lingual string Event name
|
├ name multi-lingual string Event name
|
||||||
|
├ end_date datetime End date of the event. After this time, the app could show a warning that the event is over.
|
||||||
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
|
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
|
||||||
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
|
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
|
||||||
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
|
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
|
||||||
@@ -596,6 +615,7 @@ scan_types list of objects Only used for a
|
|||||||
"booth": "A2",
|
"booth": "A2",
|
||||||
"event": {
|
"event": {
|
||||||
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
|
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
|
||||||
|
"end_date": "2017-12-28T10:00:00+00:00",
|
||||||
"slug": "bigevents",
|
"slug": "bigevents",
|
||||||
"imprint_url": null,
|
"imprint_url": null,
|
||||||
"privacy_url": null,
|
"privacy_url": null,
|
||||||
@@ -634,6 +654,7 @@ On the request, you should set the following properties:
|
|||||||
* ``tags`` with the list of selected tags
|
* ``tags`` with the list of selected tags
|
||||||
* ``rating`` with the rating assigned by the exhibitor
|
* ``rating`` with the rating assigned by the exhibitor
|
||||||
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
|
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
|
||||||
|
* ``device_uuid`` with a auto-generated UUID of the device used for scanning, or ``null``
|
||||||
|
|
||||||
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
|
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
|
||||||
responds with the previously saved information and will not delete that information. If you
|
responds with the previously saved information and will not delete that information. If you
|
||||||
@@ -668,7 +689,8 @@ The request for this looks like this:
|
|||||||
"scan_type": "lead",
|
"scan_type": "lead",
|
||||||
"tags": ["foo"],
|
"tags": ["foo"],
|
||||||
"rating": 4,
|
"rating": 4,
|
||||||
"device_name": "DEV1"
|
"device_name": "DEV1",
|
||||||
|
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response:**
|
**Example response:**
|
||||||
@@ -701,7 +723,9 @@ The request for this looks like this:
|
|||||||
},
|
},
|
||||||
"rating": 4,
|
"rating": 4,
|
||||||
"tags": ["foo"],
|
"tags": ["foo"],
|
||||||
"notes": "Great customer, wants our newsletter"
|
"notes": "Great customer, wants our newsletter",
|
||||||
|
"device_name": "DEV1",
|
||||||
|
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
|
||||||
}
|
}
|
||||||
|
|
||||||
:statuscode 200: No error, leads was not scanned for the first time
|
:statuscode 200: No error, leads was not scanned for the first time
|
||||||
@@ -756,7 +780,9 @@ You can also fetch existing leads (if you are authorized to do so):
|
|||||||
},
|
},
|
||||||
"rating": 4,
|
"rating": 4,
|
||||||
"tags": ["foo"],
|
"tags": ["foo"],
|
||||||
"notes": "Great customer, wants our newsletter"
|
"notes": "Great customer, wants our newsletter",
|
||||||
|
"device_name": "DEV1",
|
||||||
|
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
sphinx==7.0.*
|
sphinx==7.2.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==7.0.*
|
sphinx==7.2.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
|
|||||||
@@ -194,17 +194,23 @@ A complete record could look like this::
|
|||||||
|
|
||||||
v=spf1 a mx include:_spf.pretix.eu ~all
|
v=spf1 a mx include:_spf.pretix.eu ~all
|
||||||
|
|
||||||
Make sure to read up on the `SPF specification`_. If you want to authenticate your emails with DKIM, set up a DNS TXT
|
Make sure to read up on the `SPF specification`_.
|
||||||
record for the subdomain ``pretix._domainkey`` with the following contents::
|
|
||||||
|
|
||||||
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXrDk6lwOWX00e2MbiiJac6huI+gnzLf9N4G1FnBv3PXq8fz3i2q1szH72OF5mAlKm3zXO4cl/uxx+lfidS1ERbX6Bn9BRstBTQUKWC4JFj8Yk9+fwT7LWehDURazLdTzfsIjJFudLLvxtOKSaOCtMhbPX05DIhziaqVCBqgz/NQIDAQAB
|
If you want to authenticate your emails with `DKIM`_, set up a ``CNAME`` record for the subdomain ``pretix._domainkey``
|
||||||
|
pointing to ``dkim.pretix.eu``::
|
||||||
|
|
||||||
|
pretix._domainkey.mydomain.com. CNAME dkim.pretix.eu.
|
||||||
|
|
||||||
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
|
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
|
||||||
|
|
||||||
|
For senders with larger volumes, Google Mail also requires you to have a `DMARC`_ policy (that may however be ``p=none``).
|
||||||
|
|
||||||
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
|
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
|
||||||
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
|
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
|
||||||
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
|
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
|
||||||
in this case, as pretix only retries email delivery for a certain time period.
|
in this case, as pretix only retries email delivery for a certain time period.
|
||||||
|
|
||||||
|
.. _DKIM: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
|
||||||
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
||||||
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
||||||
|
.. _DMARC: https://en.wikipedia.org/wiki/DMARC
|
||||||
@@ -19,4 +19,3 @@ Then, head to the **Bundled products** tab of the "conference ticket" and add th
|
|||||||
|
|
||||||
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
|
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
|
||||||
|
|
||||||
You can find more use cases in these specialized guides:
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ the button-style of that checkbox with the one in the pretix shop, you can use t
|
|||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Due to compatibilty with existing widget installations, the default value for ``single-item-select``
|
Due to compatibility with existing widget installations, the default value for ``single-item-select``
|
||||||
is ``checkbox``. This might change in the future, so make sure, to set the attribute to
|
is ``checkbox``. This might change in the future, so make sure, to set the attribute to
|
||||||
``single-item-select="checkbox"`` if you need it.
|
``single-item-select="checkbox"`` if you need it.
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ settings. For example, if you set up a meta data property called "Promoted" that
|
|||||||
|
|
||||||
<pretix-widget event="https://pretix.eu/demo/series/" list-type="list" filter="attr[Promoted]=Yes"></pretix-widget>
|
<pretix-widget event="https://pretix.eu/demo/series/" list-type="list" filter="attr[Promoted]=Yes"></pretix-widget>
|
||||||
|
|
||||||
If you have enabled public filters in your meta data attribute configuration, a filter formshows up. To disable, use::
|
If you have enabled public filters in your meta data attribute configuration, a filter-form shows up. To disable, use::
|
||||||
|
|
||||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-filters></pretix-widget>
|
<pretix-widget event="https://pretix.eu/demo/democon/" disable-filters></pretix-widget>
|
||||||
|
|
||||||
@@ -429,4 +429,25 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
Offering wallet payments (Apple Pay, Google Pay) within the widget
|
||||||
|
------------------------------------------------------------------
|
||||||
|
|
||||||
|
Some payment providers (such as Stripe) also offer Apple or Google Pay. But in order to use them, the domain of the
|
||||||
|
payment needs to be approved first. As of right now, pretix will take care of the domain verification process for you
|
||||||
|
automatically, when using Stripe. However, pretix can only validate the domain that is being used for your default,
|
||||||
|
"stand-alone" shop (such as https://pretix.eu/demo/democon/ ).
|
||||||
|
|
||||||
|
When embedding the widget on your website, the domain of the embedding page will also need to be validated in order to
|
||||||
|
be able to use it for wallet payments.
|
||||||
|
|
||||||
|
The details might vary from payment provider to payment provider, but generally speaking, it will either involve just
|
||||||
|
telling your payment provider the domain name and (for Apple Pay) placing an
|
||||||
|
``apple-developer-merchantid-domain-association``-file into the ``.well-known``-directory of your domain.
|
||||||
|
|
||||||
|
Further reading:
|
||||||
|
|
||||||
|
* `Stripe Payment Method Domain registration`_
|
||||||
|
|
||||||
.. _Let's Encrypt: https://letsencrypt.org/
|
.. _Let's Encrypt: https://letsencrypt.org/
|
||||||
|
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ to get a better plain text representation of your text. Note however, that for
|
|||||||
security reasons you can only use the following HTML elements::
|
security reasons you can only use the following HTML elements::
|
||||||
|
|
||||||
a, abbr, acronym, b, br, code, div, em, h1, h2,
|
a, abbr, acronym, b, br, code, div, em, h1, h2,
|
||||||
h3, h4, h5, h6, hr, i, li, ol, p, pre, span, strong,
|
h3, h4, h5, h6, hr, i, li, ol, p, pre, s, span, strong,
|
||||||
table, tbody, td, thead, tr, ul
|
table, tbody, td, thead, tr, ul
|
||||||
|
|
||||||
Additionally, only the following attributes are allowed on them::
|
Additionally, only the following attributes are allowed on them::
|
||||||
|
|||||||
+34
-37
@@ -31,41 +31,41 @@ dependencies = [
|
|||||||
"BeautifulSoup4==4.12.*",
|
"BeautifulSoup4==4.12.*",
|
||||||
"bleach==5.0.*",
|
"bleach==5.0.*",
|
||||||
"celery==5.3.*",
|
"celery==5.3.*",
|
||||||
"chardet==5.1.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=3.4.2",
|
"cryptography>=3.4.2",
|
||||||
"css-inline==0.8.*",
|
"css-inline==0.13.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dj-static",
|
"dj-static",
|
||||||
"Django==4.2.*",
|
"Django==4.2.*",
|
||||||
"django-bootstrap3==23.1.*",
|
"django-bootstrap3==23.6.*",
|
||||||
"django-compressor==4.3.*",
|
"django-compressor==4.4",
|
||||||
"django-countries==7.5.*",
|
"django-countries==7.5.*",
|
||||||
"django-filter==23.2",
|
"django-filter==23.5",
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.4.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.1.*",
|
"django-hierarkey==1.1.*",
|
||||||
"django-hijack==3.3.*",
|
"django-hijack==3.4.*",
|
||||||
"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",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
"django-oauth-toolkit==2.2.*",
|
"django-oauth-toolkit==2.3.*",
|
||||||
"django-otp==1.2.*",
|
"django-otp==1.3.*",
|
||||||
"django-phonenumber-field==7.1.*",
|
"django-phonenumber-field==7.3.*",
|
||||||
"django-redis==5.2.*",
|
"django-redis==5.4.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.3.*",
|
"django-statici18n==2.4.*",
|
||||||
"djangorestframework==3.14.*",
|
"djangorestframework==3.14.*",
|
||||||
"dnspython==2.3.*",
|
"dnspython==2.5.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==4.*",
|
"geoip2==4.*",
|
||||||
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.3.*",
|
"kombu==5.3.*",
|
||||||
"libsass==0.22.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.5.2", # 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,63 +73,60 @@ dependencies = [
|
|||||||
"packaging",
|
"packaging",
|
||||||
"paypalrestsdk==1.13.*",
|
"paypalrestsdk==1.13.*",
|
||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.7.*",
|
"PyJWT==2.8.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==9.5.*",
|
"Pillow==10.2.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==4.23.*",
|
"protobuf==4.25.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.21",
|
"pycparser==2.21",
|
||||||
"pycryptodome==3.18.*",
|
"pycryptodome==3.20.*",
|
||||||
"pypdf==3.9.*",
|
"pypdf==3.9.*",
|
||||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.8.*",
|
"python-dateutil==2.9.*",
|
||||||
"python-u2flib-server==4.*",
|
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==7.4.*",
|
"qrcode==7.4.*",
|
||||||
"redis==4.6.*",
|
"redis==5.0.*",
|
||||||
"reportlab==4.0.*",
|
"reportlab==4.1.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==1.15.*",
|
"sentry-sdk==1.40.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
"slimit",
|
||||||
"static3==0.7.*",
|
"static3==0.7.*",
|
||||||
"stripe==5.4.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==0.4.*",
|
"webauthn==2.0.*",
|
||||||
"zeep==4.2.*"
|
"zeep==4.2.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.8.*",
|
"aiohttp==3.9.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.18.*",
|
"fakeredis==2.21.*",
|
||||||
"flake8==6.0.*",
|
"flake8==7.0.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==5.12.*",
|
"isort==5.13.*",
|
||||||
"pep8-naming==0.13.*",
|
"pep8-naming==0.13.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pycodestyle==2.10.*",
|
|
||||||
"pyflakes==3.0.*",
|
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.10.*",
|
"pytest-mock==3.12.*",
|
||||||
"pytest-rerunfailures==11.*",
|
"pytest-rerunfailures==13.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.3.*",
|
"pytest-xdist==3.5.*",
|
||||||
"pytest==7.3.*",
|
"pytest==8.0.*",
|
||||||
"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__ = "2023.11.0.dev0"
|
__version__ = "2024.3.0.dev0"
|
||||||
|
|||||||
@@ -267,9 +267,10 @@ CACHE_LARGE_VALUES_ALIAS = 'default'
|
|||||||
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
|
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
|
||||||
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
|
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
|
||||||
|
|
||||||
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
|
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", ".jpg", ".gif", ".jpeg")
|
||||||
|
PILLOW_FORMATS_QUESTIONS_FAVICON = ('PNG', 'GIF', 'JPEG', 'ICO')
|
||||||
|
|
||||||
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", ".jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
||||||
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
||||||
|
|
||||||
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||||
@@ -278,3 +279,5 @@ FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
|||||||
".bmp", ".tif", ".tiff"
|
".bmp", ".tif", ".tiff"
|
||||||
)
|
)
|
||||||
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
||||||
|
|
||||||
|
PRETIX_MAX_ORDER_SIZE = 500
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ MAIL_FROM_ORGANIZERS = 'invalid@invalid'
|
|||||||
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
|
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
|
||||||
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
|
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
|
||||||
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
|
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
|
||||||
|
FILE_UPLOAD_MAX_SIZE_FAVICON = 10
|
||||||
DEFAULT_CURRENCY = 'EUR'
|
DEFAULT_CURRENCY = 'EUR'
|
||||||
SECRET_KEY = "build-time-secret-key"
|
SECRET_KEY = "build-time-secret-key"
|
||||||
HAS_REDIS = False
|
HAS_REDIS = False
|
||||||
|
|||||||
@@ -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 logging
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
@@ -29,6 +31,8 @@ from pretix.api.auth.devicesecurity import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models import Device
|
from pretix.base.models import Device
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTokenAuthentication(TokenAuthentication):
|
class DeviceTokenAuthentication(TokenAuthentication):
|
||||||
model = Device
|
model = Device
|
||||||
@@ -46,6 +50,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
raise exceptions.AuthenticationFailed('Device has not been initialized.')
|
raise exceptions.AuthenticationFailed('Device has not been initialized.')
|
||||||
|
|
||||||
if device.revoked:
|
if device.revoked:
|
||||||
|
logging.warning(f'Connection attempt of revoked device {device.pk}.')
|
||||||
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
||||||
|
|
||||||
return AnonymousUser(), device
|
return AnonymousUser(), device
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:order-detail'),
|
('GET', 'api-v1:order-detail'),
|
||||||
('DELETE', 'api-v1:orderposition-detail'),
|
('DELETE', 'api-v1:orderposition-detail'),
|
||||||
('PATCH', 'api-v1:orderposition-detail'),
|
('PATCH', 'api-v1:orderposition-detail'),
|
||||||
|
('GET', 'api-v1:orderposition-list'),
|
||||||
('GET', 'api-v1:orderposition-answer'),
|
('GET', 'api-v1:orderposition-answer'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
('POST', 'api-v1:order-mark-canceled'),
|
('POST', 'api-v1:order-mark-canceled'),
|
||||||
@@ -223,6 +224,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
|||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
('POST', 'api-v1:reusablemedium-lookup'),
|
('POST', 'api-v1:reusablemedium-lookup'),
|
||||||
|
('GET', 'api-v1:reusablemedium-list'),
|
||||||
('POST', 'api-v1:reusablemedium-list'),
|
('POST', 'api-v1:reusablemedium-list'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#
|
||||||
|
# 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 rest_framework import exceptions
|
||||||
|
from rest_framework.authentication import (
|
||||||
|
SessionAuthentication as BaseSessionAuthentication,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pretix.multidomain.middlewares import CsrfViewMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
class CustomCSRFCheck(CsrfViewMiddleware):
|
||||||
|
def _reject(self, request, reason):
|
||||||
|
# Return the failure reason instead of an HttpResponse
|
||||||
|
return reason
|
||||||
|
|
||||||
|
|
||||||
|
class SessionAuthentication(BaseSessionAuthentication):
|
||||||
|
# Override from DRF to user our custom CSRF middleware
|
||||||
|
|
||||||
|
def enforce_csrf(self, request):
|
||||||
|
def dummy_get_response(request): # pragma: no cover
|
||||||
|
return None
|
||||||
|
|
||||||
|
check = CustomCSRFCheck(dummy_get_response)
|
||||||
|
# populates request.META['CSRF_COOKIE'], which is used in process_view()
|
||||||
|
check.process_request(request)
|
||||||
|
reason = check.process_view(request, None, (), {})
|
||||||
|
if reason:
|
||||||
|
# CSRF failed, bail with explicit error message
|
||||||
|
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
|
||||||
@@ -54,7 +54,7 @@ class IdempotencyMiddleware:
|
|||||||
|
|
||||||
auth_hash_parts = '{}:{}'.format(
|
auth_hash_parts = '{}:{}'.format(
|
||||||
request.headers.get('Authorization', ''),
|
request.headers.get('Authorization', ''),
|
||||||
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
|
||||||
)
|
)
|
||||||
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
||||||
idempotency_key = request.headers.get('X-Idempotency-Key', '')
|
idempotency_key = request.headers.get('X-Idempotency-Key', '')
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.10 on 2024-02-12 11:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixapi", "0011_bigint"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="oauthapplication",
|
||||||
|
name="post_logout_redirect_uris",
|
||||||
|
field=models.TextField(default=""),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -42,6 +42,11 @@ class OAuthApplication(AbstractApplication):
|
|||||||
verbose_name=_("Redirection URIs"),
|
verbose_name=_("Redirection URIs"),
|
||||||
help_text=_("Allowed URIs list, space separated")
|
help_text=_("Allowed URIs list, space separated")
|
||||||
)
|
)
|
||||||
|
post_logout_redirect_uris = models.TextField(
|
||||||
|
blank=True, validators=[URIValidator],
|
||||||
|
help_text=_("Allowed Post Logout URIs list, space separated"),
|
||||||
|
default="",
|
||||||
|
)
|
||||||
client_id = models.CharField(
|
client_id = models.CharField(
|
||||||
verbose_name=_("Client ID"),
|
verbose_name=_("Client ID"),
|
||||||
max_length=100, unique=True, default=generate_client_id, db_index=True
|
max_length=100, unique=True, default=generate_client_id, db_index=True
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
new_event = super().create({**validated_data, 'plugins': None})
|
new_event = super().create({**validated_data, 'plugins': None})
|
||||||
|
|
||||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||||
new_event.copy_data_from(event)
|
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
|
||||||
|
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
new_event.set_active_plugins(plugins)
|
new_event.set_active_plugins(plugins)
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'value', 'active', 'description',
|
fields = ('id', 'value', 'active', 'description',
|
||||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
'checkin_attention', 'checkin_text',
|
||||||
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -85,7 +86,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'value', 'active', 'description',
|
fields = ('id', 'value', 'active', 'description',
|
||||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
'checkin_attention', 'checkin_text',
|
||||||
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -235,7 +237,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
model = Item
|
model = Item
|
||||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||||
'personalized', 'position', 'picture', 'available_from', 'available_until',
|
'personalized', 'position', 'picture',
|
||||||
|
'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',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
|
|||||||
@@ -486,11 +486,11 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||||
'valid_from', 'valid_until', 'blocked')
|
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
||||||
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked'
|
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1035,13 +1035,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
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
|
||||||
|
|
||||||
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')
|
'require_approval', 'valid_if_pending', 'expires')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1050,6 +1051,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError('The given payment provider is not known.')
|
raise ValidationError('The given payment provider is not known.')
|
||||||
return pp
|
return pp
|
||||||
|
|
||||||
|
def validate_expires(self, expires):
|
||||||
|
if expires < now():
|
||||||
|
raise ValidationError('Expiration date must be in the future.')
|
||||||
|
return expires
|
||||||
|
|
||||||
def validate_sales_channel(self, channel):
|
def validate_sales_channel(self, channel):
|
||||||
if channel not in get_all_sales_channels():
|
if channel not in get_all_sales_channels():
|
||||||
raise ValidationError('Unknown sales channel.')
|
raise ValidationError('Unknown sales channel.')
|
||||||
@@ -1071,6 +1077,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
'An order cannot be empty.'
|
'An order cannot be empty.'
|
||||||
)
|
)
|
||||||
|
if len(data) > settings.PRETIX_MAX_ORDER_SIZE:
|
||||||
|
raise ValidationError(
|
||||||
|
'Orders cannot have more than %(max)s positions.' % {'max': settings.PRETIX_MAX_ORDER_SIZE}
|
||||||
|
)
|
||||||
errs = [{} for p in data]
|
errs = [{} for p in data]
|
||||||
if any([p.get('positionid') for p in data]):
|
if any([p.get('positionid') for p in data]):
|
||||||
if not all([p.get('positionid') for p in data]):
|
if not all([p.get('positionid') for p in data]):
|
||||||
@@ -1356,7 +1366,8 @@ 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)
|
||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
if not validated_data.get('expires'):
|
||||||
|
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||||
order.meta_info = "{}"
|
order.meta_info = "{}"
|
||||||
order.total = Decimal('0.00')
|
order.total = Decimal('0.00')
|
||||||
if validated_data.get('require_approval') is not None:
|
if validated_data.get('require_approval') is not None:
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from django.http import Http404
|
|||||||
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.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext
|
||||||
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 packaging.version import parse
|
from packaging.version import parse
|
||||||
@@ -285,6 +286,8 @@ with scopes_disabled():
|
|||||||
return queryset.filter(last_checked_in__isnull=not value)
|
return queryset.filter(last_checked_in__isnull=not value)
|
||||||
|
|
||||||
def check_rules_qs(self, queryset, name, value):
|
def check_rules_qs(self, queryset, name, value):
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
if not self.checkinlist.rules:
|
if not self.checkinlist.rules:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@@ -584,6 +587,32 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
else:
|
else:
|
||||||
|
if media.linked_orderposition.order.event_id not in list_by_event:
|
||||||
|
# Medium exists but connected ticket is for the wrong event
|
||||||
|
if not simulate:
|
||||||
|
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||||
|
'datetime': datetime,
|
||||||
|
'type': checkin_type,
|
||||||
|
'list': checkinlists[0].pk,
|
||||||
|
'barcode': raw_barcode,
|
||||||
|
'searched_lists': [cl.pk for cl in checkinlists]
|
||||||
|
}, user=user, auth=auth)
|
||||||
|
Checkin.objects.create(
|
||||||
|
position=None,
|
||||||
|
successful=False,
|
||||||
|
error_reason=Checkin.REASON_INVALID,
|
||||||
|
error_explanation=gettext('Medium connected to other event'),
|
||||||
|
**common_checkin_args,
|
||||||
|
)
|
||||||
|
return Response({
|
||||||
|
'detail': 'Not found.', # for backwards compatibility
|
||||||
|
'status': 'error',
|
||||||
|
'reason': Checkin.REASON_INVALID,
|
||||||
|
'reason_explanation': gettext('Medium connected to other event'),
|
||||||
|
'require_attention': False,
|
||||||
|
'checkin_texts': [],
|
||||||
|
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||||
|
}, status=404)
|
||||||
op_candidates = [media.linked_orderposition]
|
op_candidates = [media.linked_orderposition]
|
||||||
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
|
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
|
||||||
op_candidates += list(media.linked_orderposition.addons.all())
|
op_candidates += list(media.linked_orderposition.addons.all())
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event = serializer.save(organizer=self.request.organizer)
|
new_event = serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
if copy_from:
|
if copy_from:
|
||||||
new_event.copy_data_from(copy_from)
|
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
||||||
|
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
new_event.set_active_plugins(plugins)
|
new_event.set_active_plugins(plugins)
|
||||||
@@ -291,7 +291,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
instance.organizer.log_action(
|
instance.organizer.log_action(
|
||||||
'pretix.event.deleted', user=self.request.user,
|
'pretix.event.deleted', user=self.request.user, auth=self.request.auth,
|
||||||
data={
|
data={
|
||||||
'event_id': instance.pk,
|
'event_id': instance.pk,
|
||||||
'name': str(instance.name),
|
'name': str(instance.name),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class IdempotencyQueryView(APIView):
|
|||||||
idempotency_key = request.GET.get("key")
|
idempotency_key = request.GET.get("key")
|
||||||
auth_hash_parts = '{}:{}'.format(
|
auth_hash_parts = '{}:{}'.format(
|
||||||
request.headers.get('Authorization', ''),
|
request.headers.get('Authorization', ''),
|
||||||
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
|
||||||
)
|
)
|
||||||
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
||||||
if not idempotency_key:
|
if not idempotency_key:
|
||||||
|
|||||||
@@ -222,6 +222,8 @@ class OrderViewSetMixin:
|
|||||||
qs = qs.prefetch_related('refunds', 'refunds__payment')
|
qs = qs.prefetch_related('refunds', 'refunds__payment')
|
||||||
if 'invoice_address' not in self.request.GET.getlist('exclude'):
|
if 'invoice_address' not in self.request.GET.getlist('exclude'):
|
||||||
qs = qs.select_related('invoice_address')
|
qs = qs.select_related('invoice_address')
|
||||||
|
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||||
|
qs = qs.select_related('customer')
|
||||||
|
|
||||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
||||||
return qs
|
return qs
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
def notify_webhooks(logentry_ids: list):
|
def notify_webhooks(logentry_ids: list):
|
||||||
if not isinstance(logentry_ids, list):
|
if not isinstance(logentry_ids, list):
|
||||||
logentry_ids = [logentry_ids]
|
logentry_ids = [logentry_ids]
|
||||||
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer_link').filter(id__in=logentry_ids)
|
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
|
||||||
_org, _at, webhooks = None, None, None
|
_org, _at, webhooks = None, None, None
|
||||||
for logentry in qs:
|
for logentry in qs:
|
||||||
if not logentry.organizer:
|
if not logentry.organizer:
|
||||||
|
|||||||
+12
-505
@@ -19,10 +19,7 @@
|
|||||||
# 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 inspect
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
from decimal import Decimal
|
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from smtplib import SMTPResponseException
|
from smtplib import SMTPResponseException
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
@@ -33,21 +30,21 @@ from django.core.mail.backends.smtp import EmailBackend
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.formats import date_format
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import get_language, gettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.i18n import (
|
|
||||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
|
||||||
)
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.signals import register_html_mail_renderers
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
|
||||||
from pretix.base.signals import (
|
|
||||||
register_html_mail_renderers, register_mail_placeholders,
|
|
||||||
)
|
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
|
|
||||||
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
|
get_available_placeholders, PlaceholderContext
|
||||||
|
)
|
||||||
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
|
BaseTextPlaceholder as BaseMailTextPlaceholder,
|
||||||
|
SimpleFunctionalTextPlaceholder as SimpleFunctionalMailTextPlaceholder,
|
||||||
|
)
|
||||||
|
from pretix.base.settings import get_name_parts_localized # noqa
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.email')
|
logger = logging.getLogger('pretix.base.email')
|
||||||
|
|
||||||
T = TypeVar("T", bound=EmailBackend)
|
T = TypeVar("T", bound=EmailBackend)
|
||||||
@@ -192,7 +189,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
tpl = get_template(self.template_name)
|
tpl = get_template(self.template_name)
|
||||||
body_html = tpl.render(htmlctx)
|
body_html = tpl.render(htmlctx)
|
||||||
|
|
||||||
inliner = css_inline.CSSInliner(remove_style_tags=True)
|
inliner = css_inline.CSSInliner(keep_style_tags=False)
|
||||||
body_html = inliner.inline(body_html)
|
body_html = inliner.inline(body_html)
|
||||||
|
|
||||||
return body_html
|
return body_html
|
||||||
@@ -217,495 +214,5 @@ def base_renderers(sender, **kwargs):
|
|||||||
return [ClassicMailRenderer, UnembellishedMailRenderer]
|
return [ClassicMailRenderer, UnembellishedMailRenderer]
|
||||||
|
|
||||||
|
|
||||||
class BaseMailTextPlaceholder:
|
|
||||||
"""
|
|
||||||
This is the base class for for all email text placeholders.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def required_context(self):
|
|
||||||
"""
|
|
||||||
This property should return a list of all attribute names that need to be
|
|
||||||
contained in the base context so that this placeholder is available. By default,
|
|
||||||
it returns a list containing the string "event".
|
|
||||||
"""
|
|
||||||
return ["event"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def identifier(self):
|
|
||||||
"""
|
|
||||||
This should return the identifier of this placeholder in the email.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
"""
|
|
||||||
This method is called to generate the actual text that is being
|
|
||||||
used in the email. You will be passed a context dictionary with the
|
|
||||||
base context attributes specified in ``required_context``. You are
|
|
||||||
expected to return a plain-text string.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def render_sample(self, event):
|
|
||||||
"""
|
|
||||||
This method is called to generate a text to be used in email previews.
|
|
||||||
This may only depend on the event.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
|
|
||||||
def __init__(self, identifier, args, func, sample):
|
|
||||||
self._identifier = identifier
|
|
||||||
self._args = args
|
|
||||||
self._func = func
|
|
||||||
self._sample = sample
|
|
||||||
|
|
||||||
@property
|
|
||||||
def identifier(self):
|
|
||||||
return self._identifier
|
|
||||||
|
|
||||||
@property
|
|
||||||
def required_context(self):
|
|
||||||
return self._args
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
return self._func(**{k: context[k] for k in self._args})
|
|
||||||
|
|
||||||
def render_sample(self, event):
|
|
||||||
if callable(self._sample):
|
|
||||||
return self._sample(event)
|
|
||||||
else:
|
|
||||||
return self._sample
|
|
||||||
|
|
||||||
|
|
||||||
def get_available_placeholders(event, base_parameters):
|
|
||||||
if 'order' in base_parameters:
|
|
||||||
base_parameters.append('invoice_address')
|
|
||||||
base_parameters.append('position_or_address')
|
|
||||||
params = {}
|
|
||||||
for r, val in register_mail_placeholders.send(sender=event):
|
|
||||||
if not isinstance(val, (list, tuple)):
|
|
||||||
val = [val]
|
|
||||||
for v in val:
|
|
||||||
if all(rp in base_parameters for rp in v.required_context):
|
|
||||||
params[v.identifier] = v
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def get_email_context(**kwargs):
|
def get_email_context(**kwargs):
|
||||||
from pretix.base.models import InvoiceAddress
|
return PlaceholderContext(**kwargs).render_all()
|
||||||
|
|
||||||
event = kwargs['event']
|
|
||||||
if 'position' in kwargs:
|
|
||||||
kwargs.setdefault("position_or_address", kwargs['position'])
|
|
||||||
if 'order' in kwargs:
|
|
||||||
try:
|
|
||||||
if not kwargs.get('invoice_address'):
|
|
||||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
|
||||||
except InvoiceAddress.DoesNotExist:
|
|
||||||
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
|
|
||||||
finally:
|
|
||||||
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
|
|
||||||
ctx = {}
|
|
||||||
for r, val in register_mail_placeholders.send(sender=event):
|
|
||||||
if not isinstance(val, (list, tuple)):
|
|
||||||
val = [val]
|
|
||||||
for v in val:
|
|
||||||
if all(rp in kwargs for rp in v.required_context):
|
|
||||||
try:
|
|
||||||
ctx[v.identifier] = v.render(kwargs)
|
|
||||||
except:
|
|
||||||
ctx[v.identifier] = '(error)'
|
|
||||||
logger.exception(f'Failed to process email placeholder {v.identifier}.')
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
def _placeholder_payments(order, payments):
|
|
||||||
d = []
|
|
||||||
for payment in payments:
|
|
||||||
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
|
||||||
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
|
|
||||||
else:
|
|
||||||
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
|
|
||||||
d = [line for line in d if line.strip()]
|
|
||||||
if d:
|
|
||||||
return '\n\n'.join(d)
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
def get_best_name(position_or_address, parts=False):
|
|
||||||
"""
|
|
||||||
Return the best name we got for either an invoice address or an order position, falling back to the respective other
|
|
||||||
"""
|
|
||||||
from pretix.base.models import InvoiceAddress, OrderPosition
|
|
||||||
if isinstance(position_or_address, InvoiceAddress):
|
|
||||||
if position_or_address.name:
|
|
||||||
return position_or_address.name_parts if parts else position_or_address.name
|
|
||||||
elif position_or_address.order:
|
|
||||||
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
|
||||||
|
|
||||||
if isinstance(position_or_address, OrderPosition):
|
|
||||||
if position_or_address.attendee_name:
|
|
||||||
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
|
|
||||||
elif position_or_address.order:
|
|
||||||
try:
|
|
||||||
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
|
|
||||||
except InvoiceAddress.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {} if parts else ""
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
|
|
||||||
def base_placeholders(sender, **kwargs):
|
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
|
||||||
|
|
||||||
ph = [
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
|
||||||
lambda event_or_subevent: event_or_subevent.name
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'code', ['order'], lambda order: order.code, 'F8VVL'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'order_email', ['order'], lambda order: order.email, 'john@example.org'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'invoice_number', ['invoice'],
|
|
||||||
lambda invoice: invoice.full_invoice_no,
|
|
||||||
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
|
||||||
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
|
||||||
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'pending_sum', ['event', 'pending_sum'],
|
|
||||||
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
|
|
||||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
|
||||||
event.currency),
|
|
||||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
|
|
||||||
lambda event: LazyDate(now() + timedelta(days=15))
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.open', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
'hash': order.email_confirm_hash()
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.open', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
'hash': '98kusd8ofsj8dnkd'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.modify', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.modify', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.change', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.change', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.cancel', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.cancel', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.position',
|
|
||||||
kwargs={
|
|
||||||
'order': position.order.code,
|
|
||||||
'secret': position.web_secret,
|
|
||||||
'position': position.positionid
|
|
||||||
}
|
|
||||||
),
|
|
||||||
lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.position', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
'position': '123'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
|
||||||
lambda order, event:
|
|
||||||
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
|
|
||||||
if order.modify_deadline
|
|
||||||
else '',
|
|
||||||
lambda event: date_format(
|
|
||||||
event.settings.get(
|
|
||||||
'last_order_modification_date', as_type=RelativeDateWrapper
|
|
||||||
).datetime(event).astimezone(event.timezone),
|
|
||||||
'SHORT_DATETIME_FORMAT'
|
|
||||||
) if event.settings.get('last_order_modification_date') else '',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
|
||||||
lambda event: str(event.location or ''),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event_admission_time', ['event_or_subevent'],
|
|
||||||
lambda event_or_subevent:
|
|
||||||
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
|
||||||
if event_or_subevent.date_admission
|
|
||||||
else '',
|
|
||||||
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'subevent', ['waiting_list_entry', 'event'],
|
|
||||||
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
|
|
||||||
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'subevent_date_from', ['waiting_list_entry', 'event'],
|
|
||||||
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
|
|
||||||
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_remove', ['waiting_list_voucher', 'event'],
|
|
||||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
|
||||||
event, 'presale:event.waitinglist.remove'
|
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
|
||||||
lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.waitinglist.remove',
|
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['waiting_list_voucher', 'event'],
|
|
||||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
|
||||||
event, 'presale:event.redeem'
|
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
|
||||||
lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.redeem',
|
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
|
|
||||||
_('John Doe')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
|
||||||
_('Sample Corporation')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
|
||||||
'* {} - {}'.format(
|
|
||||||
order.full_code,
|
|
||||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
'hash': order.email_confirm_hash(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
for order in orders
|
|
||||||
), lambda event: '\n' + '\n\n'.join(
|
|
||||||
'* {} - {}'.format(
|
|
||||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
|
||||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
'order': order['code'],
|
|
||||||
'secret': order['secret'],
|
|
||||||
'hash': order['hash'],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
for order in [
|
|
||||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
|
|
||||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
|
|
||||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
|
||||||
event.settings.waiting_list_hours,
|
|
||||||
lambda event: event.settings.waiting_list_hours
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
|
|
||||||
_('Sample Admission Ticket')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
|
||||||
'68CYU2H6ZTP3WLK5'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
|
||||||
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
|
||||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
|
||||||
'voucher_url_list', ['event', 'voucher_list'],
|
|
||||||
lambda event, voucher_list: ' \n'.join([
|
|
||||||
build_absolute_uri(
|
|
||||||
event, 'presale:event.redeem'
|
|
||||||
) + '?voucher=' + c
|
|
||||||
for c in voucher_list
|
|
||||||
]),
|
|
||||||
lambda event: ' \n'.join([
|
|
||||||
build_absolute_uri(
|
|
||||||
event, 'presale:event.redeem'
|
|
||||||
) + '?voucher=' + c
|
|
||||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name', ['name'], lambda name: name,
|
|
||||||
_('John Doe')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'comment', ['comment'], lambda comment: comment,
|
|
||||||
_('An individual text with a reason can be inserted here.'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'payment_info', ['order', 'payments'], _placeholder_payments,
|
|
||||||
_('The amount has been charged to your card.'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
|
||||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
|
||||||
_('John Doe'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'positionid', ['position'], lambda position: str(position.positionid),
|
|
||||||
'1'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name', ['position_or_address'],
|
|
||||||
get_best_name,
|
|
||||||
_('John Doe'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
|
|
||||||
if "concatenation_for_salutation" in name_scheme:
|
|
||||||
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
|
|
||||||
else:
|
|
||||||
concatenation_for_salutation = name_scheme["concatenation"]
|
|
||||||
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
"name_for_salutation", ["waiting_list_entry"],
|
|
||||||
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
|
|
||||||
_("Mr Doe"),
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
"name", ["waiting_list_entry"],
|
|
||||||
lambda waiting_list_entry: waiting_list_entry.name or "",
|
|
||||||
_("Mr Doe"),
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
"name_for_salutation", ["position_or_address"],
|
|
||||||
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
|
|
||||||
_("Mr Doe"),
|
|
||||||
))
|
|
||||||
|
|
||||||
for f, l, w in name_scheme['fields']:
|
|
||||||
if f == 'full_name':
|
|
||||||
continue
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
|
|
||||||
name_scheme['sample'][f]
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
|
|
||||||
name_scheme['sample'][f]
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name_%s' % f, ['position_or_address'],
|
|
||||||
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
|
|
||||||
name_scheme['sample'][f]
|
|
||||||
))
|
|
||||||
|
|
||||||
for k, v in sender.meta_data.items():
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
|
||||||
v
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
|
||||||
v
|
|
||||||
))
|
|
||||||
|
|
||||||
return ph
|
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ from .items import * # noqa
|
|||||||
from .json import * # noqa
|
from .json import * # noqa
|
||||||
from .mail import * # noqa
|
from .mail import * # noqa
|
||||||
from .orderlist import * # noqa
|
from .orderlist import * # noqa
|
||||||
|
from .reusablemedia import * # noqa
|
||||||
from .waitinglist import * # noqa
|
from .waitinglist import * # noqa
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class InvoiceExporterMixin:
|
|||||||
('', _('All payment providers')),
|
('', _('All payment providers')),
|
||||||
] + [
|
] + [
|
||||||
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
||||||
|
if not v.is_meta
|
||||||
],
|
],
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('Only include invoices for orders that have at least one payment attempt '
|
help_text=_('Only include invoices for orders that have at least one payment attempt '
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
return qs.annotate(**annotations).filter(**filters)
|
return qs.annotate(**annotations).filter(**filters)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def iterate_orders(self, form_data: dict):
|
def orders_qs(self, form_data):
|
||||||
p_date = OrderPayment.objects.filter(
|
p_date = OrderPayment.objects.filter(
|
||||||
order=OuterRef('pk'),
|
order=OuterRef('pk'),
|
||||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||||
@@ -250,11 +250,15 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
|
|
||||||
if form_data['paid_only']:
|
if form_data['paid_only']:
|
||||||
qs = qs.filter(status=Order.STATUS_PAID)
|
qs = qs.filter(status=Order.STATUS_PAID)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def iterate_orders(self, form_data: dict):
|
||||||
|
qs = self.orders_qs(form_data)
|
||||||
tax_rates = self._get_all_tax_rates(qs)
|
tax_rates = self._get_all_tax_rates(qs)
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'),
|
_('Event slug'), _('Event name'), _('Order code'), _('Order total'), _('Status'), _('Email'),
|
||||||
_('Order date'), _('Order time'), _('Company'), _('Name'),
|
_('Phone number'), _('Order date'), _('Order time'), _('Company'), _('Name'),
|
||||||
]
|
]
|
||||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
|
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
|
||||||
if name_scheme and len(name_scheme['fields']) > 1:
|
if name_scheme and len(name_scheme['fields']) > 1:
|
||||||
@@ -331,6 +335,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
|
|
||||||
row = [
|
row = [
|
||||||
self.event_object_cache[order.event_id].slug,
|
self.event_object_cache[order.event_id].slug,
|
||||||
|
str(self.event_object_cache[order.event_id].name),
|
||||||
order.code,
|
order.code,
|
||||||
order.total,
|
order.total,
|
||||||
order.get_extended_status_display(),
|
order.get_extended_status_display(),
|
||||||
@@ -406,7 +411,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
row += self.event_object_cache[order.event_id].meta_data.values()
|
row += self.event_object_cache[order.event_id].meta_data.values()
|
||||||
yield row
|
yield row
|
||||||
|
|
||||||
def iterate_fees(self, form_data: dict):
|
def fees_qs(self, form_data):
|
||||||
p_providers = OrderPayment.objects.filter(
|
p_providers = OrderPayment.objects.filter(
|
||||||
order=OuterRef('order'),
|
order=OuterRef('order'),
|
||||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||||
@@ -425,9 +430,14 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||||
|
|
||||||
qs = self._date_filter(qs, form_data, rel='order__')
|
qs = self._date_filter(qs, form_data, rel='order__')
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def iterate_fees(self, form_data: dict):
|
||||||
|
qs = self.fees_qs(form_data)
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
_('Event slug'),
|
_('Event slug'),
|
||||||
|
_('Event name'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('Status'),
|
_('Status'),
|
||||||
_('Email'),
|
_('Email'),
|
||||||
@@ -464,6 +474,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
tz = ZoneInfo(order.event.settings.timezone)
|
tz = ZoneInfo(order.event.settings.timezone)
|
||||||
row = [
|
row = [
|
||||||
self.event_object_cache[order.event_id].slug,
|
self.event_object_cache[order.event_id].slug,
|
||||||
|
str(self.event_object_cache[order.event_id].name),
|
||||||
order.code,
|
order.code,
|
||||||
_("canceled") if op.canceled else order.get_extended_status_display(),
|
_("canceled") if op.canceled else order.get_extended_status_display(),
|
||||||
order.email,
|
order.email,
|
||||||
@@ -506,7 +517,19 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
row += self.event_object_cache[order.event_id].meta_data.values()
|
row += self.event_object_cache[order.event_id].meta_data.values()
|
||||||
yield row
|
yield row
|
||||||
|
|
||||||
|
def positions_qs(self, form_data: dict):
|
||||||
|
qs = OrderPosition.all.filter(
|
||||||
|
order__event__in=self.events,
|
||||||
|
)
|
||||||
|
if form_data['paid_only']:
|
||||||
|
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||||
|
|
||||||
|
qs = self._date_filter(qs, form_data, rel='order__')
|
||||||
|
return qs
|
||||||
|
|
||||||
def iterate_positions(self, form_data: dict):
|
def iterate_positions(self, form_data: dict):
|
||||||
|
base_qs = self.positions_qs(form_data)
|
||||||
|
|
||||||
p_providers = OrderPayment.objects.filter(
|
p_providers = OrderPayment.objects.filter(
|
||||||
order=OuterRef('order'),
|
order=OuterRef('order'),
|
||||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||||
@@ -516,9 +539,6 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
).values(
|
).values(
|
||||||
'm'
|
'm'
|
||||||
).order_by()
|
).order_by()
|
||||||
base_qs = OrderPosition.all.filter(
|
|
||||||
order__event__in=self.events,
|
|
||||||
)
|
|
||||||
qs = base_qs.annotate(
|
qs = base_qs.annotate(
|
||||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||||
).select_related(
|
).select_related(
|
||||||
@@ -528,15 +548,12 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
'subevent', 'subevent__meta_values',
|
'subevent', 'subevent__meta_values',
|
||||||
'answers', 'answers__question', 'answers__options'
|
'answers', 'answers__question', 'answers__options'
|
||||||
)
|
)
|
||||||
if form_data['paid_only']:
|
|
||||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
|
||||||
|
|
||||||
qs = self._date_filter(qs, form_data, rel='order__')
|
|
||||||
|
|
||||||
has_subevents = self.events.filter(has_subevents=True).exists()
|
has_subevents = self.events.filter(has_subevents=True).exists()
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
_('Event slug'),
|
_('Event slug'),
|
||||||
|
_('Event name'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('Position ID'),
|
_('Position ID'),
|
||||||
_('Status'),
|
_('Status'),
|
||||||
@@ -638,6 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
|
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
|
||||||
row = [
|
row = [
|
||||||
self.event_object_cache[order.event_id].slug,
|
self.event_object_cache[order.event_id].slug,
|
||||||
|
str(self.event_object_cache[order.event_id].name),
|
||||||
order.code,
|
order.code,
|
||||||
op.positionid,
|
op.positionid,
|
||||||
_("canceled") if op.canceled else order.get_extended_status_display(),
|
_("canceled") if op.canceled else order.get_extended_status_display(),
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.formats import date_format
|
||||||
|
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
|
||||||
|
|
||||||
|
from ..exporter import ListExporter, OrganizerLevelExportMixin
|
||||||
|
from ..models import ReusableMedium
|
||||||
|
from ..signals import register_multievent_data_exporters
|
||||||
|
|
||||||
|
|
||||||
|
class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||||
|
identifier = 'reusablemedia'
|
||||||
|
verbose_name = _('Reusable media')
|
||||||
|
category = pgettext_lazy('export_category', 'Reusable media')
|
||||||
|
description = _('Download a spread sheet with the data of all reusable medias on your account.')
|
||||||
|
|
||||||
|
def iterate_list(self, form_data):
|
||||||
|
media = ReusableMedium.objects.filter(
|
||||||
|
organizer=self.organizer,
|
||||||
|
).select_related(
|
||||||
|
'customer', 'linked_orderposition', 'linked_giftcard',
|
||||||
|
).order_by('created')
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
pgettext('reusable_medium', 'Media type'),
|
||||||
|
pgettext('reusable_medium', 'Identifier'),
|
||||||
|
_('Active'),
|
||||||
|
_('Expiration date'),
|
||||||
|
_('Customer account'),
|
||||||
|
_('Linked ticket'),
|
||||||
|
_('Linked gift card'),
|
||||||
|
_('Notes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
yield headers
|
||||||
|
yield self.ProgressSetTotal(total=media.count())
|
||||||
|
|
||||||
|
for medium in media.iterator(chunk_size=1000):
|
||||||
|
row = [
|
||||||
|
medium.type,
|
||||||
|
medium.identifier,
|
||||||
|
_('Yes') if medium.active else _('No'),
|
||||||
|
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
|
||||||
|
medium.customer.identifier if medium.customer_id else '',
|
||||||
|
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
|
||||||
|
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
|
||||||
|
medium.notes,
|
||||||
|
]
|
||||||
|
yield row
|
||||||
|
|
||||||
|
def get_filename(self):
|
||||||
|
return f'{self.organizer.slug}_media'
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_reusablemedia")
|
||||||
|
def register_multievent_i_reusable_media_exporter(sender, **kwargs):
|
||||||
|
return ReusableMediaExporter
|
||||||
@@ -125,7 +125,7 @@ class NamePartsWidget(forms.MultiWidget):
|
|||||||
if fname == 'title' and self.titles:
|
if fname == 'title' and self.titles:
|
||||||
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
|
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
|
||||||
elif fname == 'salutation':
|
elif fname == 'salutation':
|
||||||
widgets.append(Select(attrs=a, choices=[('', '---')] + PERSON_NAME_SALUTATIONS))
|
widgets.append(Select(attrs=a, choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS))
|
||||||
else:
|
else:
|
||||||
widgets.append(self.widget(attrs=a))
|
widgets.append(self.widget(attrs=a))
|
||||||
super().__init__(widgets, attrs)
|
super().__init__(widgets, attrs)
|
||||||
@@ -136,7 +136,10 @@ class NamePartsWidget(forms.MultiWidget):
|
|||||||
data = []
|
data = []
|
||||||
for i, field in enumerate(self.scheme['fields']):
|
for i, field in enumerate(self.scheme['fields']):
|
||||||
fname, label, size = field
|
fname, label, size = field
|
||||||
data.append(value.get(fname, ""))
|
fval = value.get(fname, "")
|
||||||
|
if fname == "salutation" and fname in value and fval == "":
|
||||||
|
fval = "empty"
|
||||||
|
data.append(fval)
|
||||||
if '_legacy' in value and not data[-1]:
|
if '_legacy' in value and not data[-1]:
|
||||||
data[-1] = value.get('_legacy', '')
|
data[-1] = value.get('_legacy', '')
|
||||||
elif not any(d for d in data) and '_scheme' in value:
|
elif not any(d for d in data) and '_scheme' in value:
|
||||||
@@ -190,7 +193,8 @@ class NamePartsFormField(forms.MultiValueField):
|
|||||||
data = {}
|
data = {}
|
||||||
data['_scheme'] = self.scheme_name
|
data['_scheme'] = self.scheme_name
|
||||||
for i, value in enumerate(data_list):
|
for i, value in enumerate(data_list):
|
||||||
data[self.scheme['fields'][i][0]] = value or ''
|
key = self.scheme['fields'][i][0]
|
||||||
|
data[key] = value or ''
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -239,7 +243,7 @@ class NamePartsFormField(forms.MultiValueField):
|
|||||||
d.pop('validators', None)
|
d.pop('validators', None)
|
||||||
field = forms.ChoiceField(
|
field = forms.ChoiceField(
|
||||||
**d,
|
**d,
|
||||||
choices=[('', '---')] + PERSON_NAME_SALUTATIONS
|
choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
field = forms.CharField(**defaults)
|
field = forms.CharField(**defaults)
|
||||||
@@ -265,6 +269,9 @@ class NamePartsFormField(forms.MultiValueField):
|
|||||||
if sum(len(v) for v in value.values() if v) > 250:
|
if sum(len(v) for v in value.values() if v) > 250:
|
||||||
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
|
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
|
||||||
|
|
||||||
|
if value.get("salutation") == "empty":
|
||||||
|
value["salutation"] = ""
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from bootstrap3.renderers import (
|
|
||||||
FieldRenderer as BaseFieldRenderer,
|
|
||||||
InlineFieldRenderer as BaseInlineFieldRenderer,
|
|
||||||
)
|
|
||||||
from django.forms import (
|
|
||||||
CheckboxInput, CheckboxSelectMultiple, ClearableFileInput, RadioSelect,
|
|
||||||
SelectDateWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FieldRenderer(BaseFieldRenderer):
|
|
||||||
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
|
|
||||||
|
|
||||||
def post_widget_render(self, html):
|
|
||||||
if isinstance(self.widget, CheckboxSelectMultiple):
|
|
||||||
html = self.list_to_class(html, "checkbox")
|
|
||||||
elif isinstance(self.widget, RadioSelect):
|
|
||||||
html = self.list_to_class(html, "radio")
|
|
||||||
elif isinstance(self.widget, SelectDateWidget):
|
|
||||||
html = self.fix_date_select_input(html)
|
|
||||||
elif isinstance(self.widget, ClearableFileInput):
|
|
||||||
html = self.fix_clearable_file_input(html)
|
|
||||||
elif isinstance(self.widget, CheckboxInput):
|
|
||||||
html = self.put_inside_label(html)
|
|
||||||
return html
|
|
||||||
|
|
||||||
|
|
||||||
class InlineFieldRenderer(BaseInlineFieldRenderer):
|
|
||||||
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
|
|
||||||
|
|
||||||
def post_widget_render(self, html):
|
|
||||||
if isinstance(self.widget, CheckboxSelectMultiple):
|
|
||||||
html = self.list_to_class(html, "checkbox")
|
|
||||||
elif isinstance(self.widget, RadioSelect):
|
|
||||||
html = self.list_to_class(html, "radio")
|
|
||||||
elif isinstance(self.widget, SelectDateWidget):
|
|
||||||
html = self.fix_date_select_input(html)
|
|
||||||
elif isinstance(self.widget, ClearableFileInput):
|
|
||||||
html = self.fix_clearable_file_input(html)
|
|
||||||
elif isinstance(self.widget, CheckboxInput):
|
|
||||||
html = self.put_inside_label(html)
|
|
||||||
return html
|
|
||||||
@@ -209,7 +209,10 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
|||||||
|
|
||||||
date_attrs['placeholder'] = lazy(date_placeholder, str)
|
date_attrs['placeholder'] = lazy(date_placeholder, str)
|
||||||
time_attrs['placeholder'] = lazy(time_placeholder, str)
|
time_attrs['placeholder'] = lazy(time_placeholder, str)
|
||||||
|
date_attrs['aria-label'] = _('Date')
|
||||||
|
time_attrs['aria-label'] = _('Time')
|
||||||
|
if 'aria-label' in attrs:
|
||||||
|
del attrs['aria-label']
|
||||||
widgets = (
|
widgets = (
|
||||||
forms.DateInput(attrs=date_attrs, format=date_format),
|
forms.DateInput(attrs=date_attrs, format=date_format),
|
||||||
forms.TimeInput(attrs=time_attrs, format=time_format),
|
forms.TimeInput(attrs=time_attrs, format=time_format),
|
||||||
|
|||||||
@@ -855,7 +855,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
class Modern1Renderer(ClassicInvoiceRenderer):
|
class Modern1Renderer(ClassicInvoiceRenderer):
|
||||||
identifier = 'modern1'
|
identifier = 'modern1'
|
||||||
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
verbose_name = gettext_lazy('Default invoice renderer (European-style letter)')
|
||||||
bottom_margin = 16.9 * mm
|
bottom_margin = 16.9 * mm
|
||||||
top_margin = 16.9 * mm
|
top_margin = 16.9 * mm
|
||||||
right_margin = 20 * mm
|
right_margin = 20 * mm
|
||||||
@@ -989,6 +989,37 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
canvas.drawText(textobject)
|
canvas.drawText(textobject)
|
||||||
|
|
||||||
|
|
||||||
|
class Modern1SimplifiedRenderer(Modern1Renderer):
|
||||||
|
identifier = 'modern1simplified'
|
||||||
|
verbose_name = gettext_lazy('Simplified invoice renderer')
|
||||||
|
|
||||||
|
logo_left = Modern1Renderer.left_margin
|
||||||
|
logo_width = pagesizes.A4[0] - Modern1Renderer.right_margin - logo_left
|
||||||
|
logo_height = 25 * mm
|
||||||
|
logo_top = 13 * mm
|
||||||
|
logo_anchor = 'nw'
|
||||||
|
|
||||||
|
def _draw_invoice_from(self, canvas):
|
||||||
|
super(Modern1Renderer, self)._draw_invoice_from(canvas)
|
||||||
|
|
||||||
|
def _draw_event(self, canvas):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_intro(self):
|
||||||
|
i = []
|
||||||
|
|
||||||
|
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||||
|
i.append(Paragraph(
|
||||||
|
pgettext('invoice', 'Event date: {date_range}').format(
|
||||||
|
date_range=self.invoice.event.get_date_range_display(),
|
||||||
|
),
|
||||||
|
self.stylesheet['Normal'],
|
||||||
|
))
|
||||||
|
i.append(Spacer(2 * mm, 2 * mm))
|
||||||
|
|
||||||
|
return i + super()._get_intro()
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
|
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
|
||||||
def recv_classic(sender, **kwargs):
|
def recv_classic(sender, **kwargs):
|
||||||
return [ClassicInvoiceRenderer, Modern1Renderer]
|
return [ClassicInvoiceRenderer, Modern1Renderer, Modern1SimplifiedRenderer]
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ from pretix.multidomain.urlreverse import (
|
|||||||
_supported = None
|
_supported = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_supported_language(requested_language, allowed_languages, default_language):
|
||||||
|
language = requested_language
|
||||||
|
if language not in allowed_languages:
|
||||||
|
firstpart = language.split('-')[0]
|
||||||
|
if firstpart in allowed_languages:
|
||||||
|
language = firstpart
|
||||||
|
else:
|
||||||
|
language = default_language
|
||||||
|
for lang in allowed_languages:
|
||||||
|
if lang.startswith(firstpart + '-'):
|
||||||
|
language = lang
|
||||||
|
break
|
||||||
|
if language not in allowed_languages:
|
||||||
|
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
|
||||||
|
# not part of settings.locales
|
||||||
|
language = allowed_languages[0]
|
||||||
|
return language
|
||||||
|
|
||||||
|
|
||||||
class LocaleMiddleware(MiddlewareMixin):
|
class LocaleMiddleware(MiddlewareMixin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -65,20 +84,11 @@ class LocaleMiddleware(MiddlewareMixin):
|
|||||||
settings_holder = None
|
settings_holder = None
|
||||||
|
|
||||||
if settings_holder:
|
if settings_holder:
|
||||||
if language not in settings_holder.settings.locales:
|
language = get_supported_language(
|
||||||
firstpart = language.split('-')[0]
|
language,
|
||||||
if firstpart in settings_holder.settings.locales:
|
settings_holder.settings.locales,
|
||||||
language = firstpart
|
settings_holder.settings.locale,
|
||||||
else:
|
)
|
||||||
language = settings_holder.settings.locale
|
|
||||||
for lang in settings_holder.settings.locales:
|
|
||||||
if lang.startswith(firstpart + '-'):
|
|
||||||
language = lang
|
|
||||||
break
|
|
||||||
if language not in settings_holder.settings.locales:
|
|
||||||
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
|
|
||||||
# not part of settings.locales
|
|
||||||
language = settings_holder.settings.locales[0]
|
|
||||||
if '-' not in language and settings_holder.settings.region:
|
if '-' not in language and settings_holder.settings.region:
|
||||||
language += '-' + settings_holder.settings.region
|
language += '-' + settings_holder.settings.region
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 4.2.4 on 2023-12-06 14:53
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0253_checkin_info"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="logentry",
|
||||||
|
name="organizer_link",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
db_column="organizer_link_id",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="pretixbase.organizer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="logentry",
|
||||||
|
old_name="organizer_link",
|
||||||
|
new_name="organizer",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.2.4 on 2023-11-22 20:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0254_alter_logentry_organizer_link_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="item",
|
||||||
|
name="available_from_mode",
|
||||||
|
field=models.CharField(default="hide", max_length=16),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="item",
|
||||||
|
name="available_until_mode",
|
||||||
|
field=models.CharField(default="hide", max_length=16),
|
||||||
|
)
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.2.4 on 2024-01-11 15:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0255_item_unavail_modes"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="itemvariation",
|
||||||
|
name="available_from_mode",
|
||||||
|
field=models.CharField(default="hide", max_length=16),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="itemvariation",
|
||||||
|
name="available_until_mode",
|
||||||
|
field=models.CharField(default="hide", max_length=16),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.2.9 on 2024-01-30 11:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0256_itemvariation_unavail_modes"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="item",
|
||||||
|
name="default_price",
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=13),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -37,9 +37,7 @@ import json
|
|||||||
import operator
|
import operator
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import webauthn
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
||||||
@@ -53,13 +51,12 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from u2flib_server.utils import (
|
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
|
||||||
pub_key_from_der, websafe_decode, websafe_encode,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.helpers.urls import build_absolute_uri
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
|
|
||||||
|
from ...helpers.u2f import pub_key_from_der, websafe_decode
|
||||||
from .base import LoggingMixin
|
from .base import LoggingMixin
|
||||||
|
|
||||||
|
|
||||||
@@ -608,7 +605,12 @@ class U2FDevice(Device):
|
|||||||
json_data = models.TextField()
|
json_data = models.TextField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def webauthnuser(self):
|
def webauthndevice(self):
|
||||||
|
d = json.loads(self.json_data)
|
||||||
|
return PublicKeyCredentialDescriptor(websafe_decode(d['keyHandle']))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def webauthnpubkey(self):
|
||||||
d = json.loads(self.json_data)
|
d = json.loads(self.json_data)
|
||||||
# We manually need to convert the pubkey from DER format (used in our
|
# We manually need to convert the pubkey from DER format (used in our
|
||||||
# former U2F implementation) to the format required by webauthn. This
|
# former U2F implementation) to the format required by webauthn. This
|
||||||
@@ -620,16 +622,7 @@ class U2FDevice(Device):
|
|||||||
pub_key.public_numbers().x, pub_key.public_numbers().y
|
pub_key.public_numbers().x, pub_key.public_numbers().y
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return webauthn.WebAuthnUser(
|
return pub_key
|
||||||
d['keyHandle'],
|
|
||||||
self.user.email,
|
|
||||||
str(self.user),
|
|
||||||
settings.SITE_URL,
|
|
||||||
d['keyHandle'],
|
|
||||||
websafe_encode(pub_key),
|
|
||||||
1,
|
|
||||||
urlparse(settings.SITE_URL).netloc
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WebAuthnDevice(Device):
|
class WebAuthnDevice(Device):
|
||||||
@@ -641,14 +634,9 @@ class WebAuthnDevice(Device):
|
|||||||
sign_count = models.IntegerField(default=0)
|
sign_count = models.IntegerField(default=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def webauthnuser(self):
|
def webauthndevice(self):
|
||||||
return webauthn.WebAuthnUser(
|
return PublicKeyCredentialDescriptor(websafe_decode(self.credential_id))
|
||||||
self.ukey,
|
|
||||||
self.user.email,
|
@property
|
||||||
str(self.user),
|
def webauthnpubkey(self):
|
||||||
settings.SITE_URL,
|
return websafe_decode(self.pub_key)
|
||||||
self.credential_id,
|
|
||||||
self.pub_key,
|
|
||||||
self.sign_count,
|
|
||||||
urlparse(settings.SITE_URL).netloc
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class LoggingMixin:
|
|||||||
kwargs['api_token'] = api_token
|
kwargs['api_token'] = api_token
|
||||||
|
|
||||||
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
|
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
|
||||||
organizer_link_id=organizer_id, **kwargs)
|
organizer_id=organizer_id, **kwargs)
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
sensitivekeys = ['password', 'secret', 'api_key']
|
sensitivekeys = ['password', 'secret', 'api_key']
|
||||||
|
|
||||||
|
|||||||
@@ -280,7 +280,8 @@ class CheckinList(LoggedModel):
|
|||||||
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
|
||||||
}
|
}
|
||||||
allowed_operators = top_level_operators | {
|
allowed_operators = top_level_operators | {
|
||||||
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
|
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before', 'entries_days_since',
|
||||||
|
'entries_days_before',
|
||||||
}
|
}
|
||||||
allowed_vars = {
|
allowed_vars = {
|
||||||
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
|
||||||
@@ -309,7 +310,7 @@ class CheckinList(LoggedModel):
|
|||||||
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
|
||||||
return rules
|
return rules
|
||||||
|
|
||||||
if operator in ('entries_since', 'entries_before'):
|
if operator in ('entries_since', 'entries_before', 'entries_days_since', 'entries_days_before'):
|
||||||
if len(values) != 1 or "buildTime" not in values[0]:
|
if len(values) != 1 or "buildTime" not in values[0]:
|
||||||
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
|
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')
|
||||||
|
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
||||||
def key(idx):
|
def key(idx):
|
||||||
return positions[idx][1] # subevent_id
|
return positions[idx][1] or 0 # subevent_id
|
||||||
|
|
||||||
# Build groups of candidates with the same subevent, then apply our regular algorithm
|
# Build groups of candidates with the same subevent, then apply our regular algorithm
|
||||||
# to each group
|
# to each group
|
||||||
|
|||||||
@@ -265,6 +265,9 @@ class EventMixin:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def event_microdata(self):
|
def event_microdata(self):
|
||||||
|
if self.settings.event_microdata:
|
||||||
|
return self.settings.event_microdata
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
eventdict = {
|
eventdict = {
|
||||||
@@ -775,7 +778,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
time(hour=23, minute=59, second=59)
|
time(hour=23, minute=59, second=59)
|
||||||
), tz)
|
), tz)
|
||||||
|
|
||||||
def copy_data_from(self, other):
|
def copy_data_from(self, other, skip_meta_data=False):
|
||||||
from pretix.presale.style import regenerate_css
|
from pretix.presale.style import regenerate_css
|
||||||
|
|
||||||
from ..signals import event_copy_data
|
from ..signals import event_copy_data
|
||||||
@@ -798,10 +801,11 @@ class Event(EventMixin, LoggedModel):
|
|||||||
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})
|
||||||
|
|
||||||
for emv in EventMetaValue.objects.filter(event=other):
|
if not skip_meta_data:
|
||||||
emv.pk = None
|
for emv in EventMetaValue.objects.filter(event=other):
|
||||||
emv.event = self
|
emv.pk = None
|
||||||
emv.save(force_insert=True)
|
emv.event = self
|
||||||
|
emv.save(force_insert=True)
|
||||||
|
|
||||||
for fl in EventFooterLink.objects.filter(event=other):
|
for fl in EventFooterLink.objects.filter(event=other):
|
||||||
fl.pk = None
|
fl.pk = None
|
||||||
@@ -1063,7 +1067,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
providers[pp.identifier] = pp
|
providers[pp.identifier] = pp
|
||||||
|
|
||||||
self._cached_payment_providers = OrderedDict(sorted(
|
self._cached_payment_providers = OrderedDict(sorted(
|
||||||
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name))
|
providers.items(), key=lambda v: (-v[1].priority, str(v[1].verbose_name).title())
|
||||||
))
|
))
|
||||||
return self._cached_payment_providers
|
return self._cached_payment_providers
|
||||||
|
|
||||||
|
|||||||
@@ -263,8 +263,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
|||||||
# 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=now()))
|
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
|
||||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
||||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||||
)
|
)
|
||||||
if not allow_addons:
|
if not allow_addons:
|
||||||
@@ -374,6 +374,13 @@ class Item(LoggedModel):
|
|||||||
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
|
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
UNAVAIL_MODE_HIDDEN = "hide"
|
||||||
|
UNAVAIL_MODE_INFO = "info"
|
||||||
|
UNAVAIL_MODES = (
|
||||||
|
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
|
||||||
|
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
|
||||||
|
)
|
||||||
|
|
||||||
MEDIA_POLICY_REUSE = 'reuse'
|
MEDIA_POLICY_REUSE = 'reuse'
|
||||||
MEDIA_POLICY_NEW = 'new'
|
MEDIA_POLICY_NEW = 'new'
|
||||||
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
||||||
@@ -423,7 +430,7 @@ class Item(LoggedModel):
|
|||||||
help_text=_("If this product has multiple variations, you can set different prices for each of the "
|
help_text=_("If this product has multiple variations, you can set different prices for each of the "
|
||||||
"variations. If a variation does not have a special price or if you do not have variations, "
|
"variations. If a variation does not have a special price or if you do not have variations, "
|
||||||
"this price will be used."),
|
"this price will be used."),
|
||||||
max_digits=13, decimal_places=2, null=True
|
max_digits=13, decimal_places=2,
|
||||||
)
|
)
|
||||||
free_price = models.BooleanField(
|
free_price = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@@ -487,11 +494,21 @@ class Item(LoggedModel):
|
|||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This product will not be sold before the given date.')
|
help_text=_('This product will not be sold before the given date.')
|
||||||
)
|
)
|
||||||
|
available_from_mode = models.CharField(
|
||||||
|
choices=UNAVAIL_MODES,
|
||||||
|
default=UNAVAIL_MODE_HIDDEN,
|
||||||
|
max_length=16,
|
||||||
|
)
|
||||||
available_until = models.DateTimeField(
|
available_until = models.DateTimeField(
|
||||||
verbose_name=_("Available until"),
|
verbose_name=_("Available until"),
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This product will not be sold after the given date.')
|
help_text=_('This product will not be sold after the given date.')
|
||||||
)
|
)
|
||||||
|
available_until_mode = models.CharField(
|
||||||
|
choices=UNAVAIL_MODES,
|
||||||
|
default=UNAVAIL_MODE_HIDDEN,
|
||||||
|
max_length=16,
|
||||||
|
)
|
||||||
hidden_if_available = models.ForeignKey(
|
hidden_if_available = models.ForeignKey(
|
||||||
'Quota',
|
'Quota',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@@ -631,7 +648,7 @@ class Item(LoggedModel):
|
|||||||
null=True, blank=True, max_length=16,
|
null=True, blank=True, max_length=16,
|
||||||
verbose_name=_('Validity'),
|
verbose_name=_('Validity'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'When setting up a regular event, or an event series with time slots, you typically to NOT need to change '
|
'When setting up a regular event, or an event series with time slots, you typically do NOT need to change '
|
||||||
'this value. The default setting means that the validity time of tickets will not be decided by the '
|
'this value. The default setting means that the validity time of tickets will not be decided by the '
|
||||||
'product, but by the event and check-in configuration. Only use the other options if you need them to '
|
'product, but by the event and check-in configuration. Only use the other options if you need them to '
|
||||||
'realize e.g. a booking of a year-long ticket with a dynamic start date. Note that the validity will be '
|
'realize e.g. a booking of a year-long ticket with a dynamic start date. Note that the validity will be '
|
||||||
@@ -703,6 +720,8 @@ class Item(LoggedModel):
|
|||||||
return str(self.internal_name or self.name)
|
return str(self.internal_name or self.name)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
if self.hide_without_voucher:
|
||||||
|
self.require_voucher = True
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
@@ -780,6 +799,24 @@ class Item(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||||
|
now_dt = now_dt or now()
|
||||||
|
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
||||||
|
if not self.active:
|
||||||
|
return 'active'
|
||||||
|
elif self.available_from and self.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif self.available_until and self.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
elif (self.require_voucher or self.hide_without_voucher) and not has_voucher:
|
||||||
|
return 'require_voucher'
|
||||||
|
elif subevent_item and subevent_item.available_from and subevent_item.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||||
check_quotas = set(getattr(
|
check_quotas = set(getattr(
|
||||||
self, '_subevent_quotas', # Utilize cache in product list
|
self, '_subevent_quotas', # Utilize cache in product list
|
||||||
@@ -1078,11 +1115,21 @@ class ItemVariation(models.Model):
|
|||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This variation will not be sold before the given date.')
|
help_text=_('This variation will not be sold before the given date.')
|
||||||
)
|
)
|
||||||
|
available_from_mode = models.CharField(
|
||||||
|
choices=Item.UNAVAIL_MODES,
|
||||||
|
default=Item.UNAVAIL_MODE_HIDDEN,
|
||||||
|
max_length=16,
|
||||||
|
)
|
||||||
available_until = models.DateTimeField(
|
available_until = models.DateTimeField(
|
||||||
verbose_name=_("Available until"),
|
verbose_name=_("Available until"),
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This variation will not be sold after the given date.')
|
help_text=_('This variation will not be sold after the given date.')
|
||||||
)
|
)
|
||||||
|
available_until_mode = models.CharField(
|
||||||
|
choices=Item.UNAVAIL_MODES,
|
||||||
|
default=Item.UNAVAIL_MODE_HIDDEN,
|
||||||
|
max_length=16,
|
||||||
|
)
|
||||||
sales_channels = fields.MultiStringField(
|
sales_channels = fields.MultiStringField(
|
||||||
verbose_name=_('Sales channels'),
|
verbose_name=_('Sales channels'),
|
||||||
default=_all_sales_channels_identifiers,
|
default=_all_sales_channels_identifiers,
|
||||||
@@ -1260,6 +1307,22 @@ class ItemVariation(models.Model):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||||
|
now_dt = now_dt or now()
|
||||||
|
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
||||||
|
if not self.active:
|
||||||
|
return 'active'
|
||||||
|
elif self.available_from and self.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif self.available_until and self.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
elif subevent_var and subevent_var.available_from and subevent_var.available_from > now_dt:
|
||||||
|
return 'available_from'
|
||||||
|
elif subevent_var and subevent_var.available_until and subevent_var.available_until < now_dt:
|
||||||
|
return 'available_until'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meta_data(self):
|
def meta_data(self):
|
||||||
data = self.item.meta_data
|
data = self.item.meta_data
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class LogEntry(models.Model):
|
|||||||
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
|
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
|
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
|
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
organizer_link = models.ForeignKey('Organizer', null=True, blank=True, on_delete=models.PROTECT)
|
organizer = models.ForeignKey('Organizer', null=True, blank=True, on_delete=models.PROTECT, db_column='organizer_link_id')
|
||||||
action_type = models.CharField(max_length=255)
|
action_type = models.CharField(max_length=255)
|
||||||
data = models.TextField(default='{}')
|
data = models.TextField(default='{}')
|
||||||
visible = models.BooleanField(default=True)
|
visible = models.BooleanField(default=True)
|
||||||
@@ -123,22 +123,6 @@ class LogEntry(models.Model):
|
|||||||
typepath = typepath.rsplit('.', 1)[0]
|
typepath = typepath.rsplit('.', 1)[0]
|
||||||
return no_type
|
return no_type
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def organizer(self):
|
|
||||||
from .organizer import Organizer
|
|
||||||
|
|
||||||
if self.organizer_link:
|
|
||||||
return self.organizer_link
|
|
||||||
elif self.event:
|
|
||||||
return self.event.organizer
|
|
||||||
elif hasattr(self.content_object, 'event'):
|
|
||||||
return self.content_object.event.organizer
|
|
||||||
elif hasattr(self.content_object, 'organizer'):
|
|
||||||
return self.content_object.organizer
|
|
||||||
elif isinstance(self.content_object, Organizer):
|
|
||||||
return self.content_object
|
|
||||||
return None
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def display_object(self):
|
def display_object(self):
|
||||||
from . import (
|
from . import (
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from datetime import datetime, time, timedelta
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, Iterable, List, Union
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import dateutil
|
import dateutil
|
||||||
@@ -79,7 +79,7 @@ from pretix.base.i18n import language
|
|||||||
from pretix.base.models import Customer, User
|
from pretix.base.models import Customer, User
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import order_gracefully_delete
|
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
|
||||||
|
|
||||||
from ...helpers import OF_SELF
|
from ...helpers import OF_SELF
|
||||||
from ...helpers.countries import CachedCountries, FastCountryField
|
from ...helpers.countries import CachedCountries, FastCountryField
|
||||||
@@ -1090,9 +1090,6 @@ class Order(LockModel, LoggedModel):
|
|||||||
if not self.email and not (position and position.attendee_email):
|
if not self.email and not (position and position.attendee_email):
|
||||||
return
|
return
|
||||||
|
|
||||||
for k, v in self.event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
with language(self.locale, self.event.settings.region):
|
with language(self.locale, self.event.settings.region):
|
||||||
recipient = self.email
|
recipient = self.email
|
||||||
if position and position.attendee_email:
|
if position and position.attendee_email:
|
||||||
@@ -1137,12 +1134,19 @@ class Order(LockModel, LoggedModel):
|
|||||||
attach_tickets=True,
|
attach_tickets=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def positions_with_tickets_ignoring_plugins(self):
|
||||||
|
return (op for op in self.positions.select_related('item') if op.generate_ticket)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positions_with_tickets(self):
|
def positions_with_tickets(self):
|
||||||
for op in self.positions.select_related('item'):
|
signal_response = allow_ticket_download.send(self.event, order=self)
|
||||||
if not op.generate_ticket:
|
if all([r is True for rr, r in signal_response]):
|
||||||
continue
|
return self.positions_with_tickets_ignoring_plugins
|
||||||
yield op
|
elif any([r is False for rr, r in signal_response]):
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return set.intersection(set(self.positions_with_tickets_ignoring_plugins), *[set(r) for rr, r in signal_response if isinstance(r, Iterable)])
|
||||||
|
|
||||||
def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False,
|
def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False,
|
||||||
_backfill_before_cancellation=False, save=True):
|
_backfill_before_cancellation=False, save=True):
|
||||||
@@ -2143,6 +2147,12 @@ class OrderRefund(models.Model):
|
|||||||
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
self.local_id = (self.order.refunds.aggregate(m=Max('local_id'))['m'] or 0) + 1
|
||||||
if 'update_fields' in kwargs:
|
if 'update_fields' in kwargs:
|
||||||
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
|
kwargs['update_fields'] = {'local_id'}.union(kwargs['update_fields'])
|
||||||
|
|
||||||
|
if self.state == OrderRefund.REFUND_STATE_DONE and not self.execution_date:
|
||||||
|
self.execution_date = now()
|
||||||
|
if 'update_fields' in kwargs:
|
||||||
|
kwargs['update_fields'] = {'execution_date'}.union(kwargs['update_fields'])
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -2644,9 +2654,6 @@ class OrderPosition(AbstractPosition):
|
|||||||
if not self.attendee_email:
|
if not self.attendee_email:
|
||||||
return
|
return
|
||||||
|
|
||||||
for k, v in self.event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
with language(self.order.locale, self.order.event.settings.region):
|
with language(self.order.locale, self.order.event.settings.region):
|
||||||
recipient = self.attendee_email
|
recipient = self.attendee_email
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -251,7 +251,8 @@ class Voucher(LoggedModel):
|
|||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
on_delete=models.PROTECT, # We use a fake version of SET_NULL in Item.delete()
|
on_delete=models.PROTECT, # We use a fake version of SET_NULL in Item.delete()
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"This product is added to the user's cart if the voucher is redeemed."
|
"This product is added to the user's cart if the voucher is redeemed. Instead of a specific product, you "
|
||||||
|
"can also select a quota. In this case, all products assigned to this quota can be selected."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
variation = models.ForeignKey(
|
variation = models.ForeignKey(
|
||||||
@@ -350,9 +351,6 @@ class Voucher(LoggedModel):
|
|||||||
'variations.'))
|
'variations.'))
|
||||||
if variation and not item.variations.filter(pk=variation.pk).exists():
|
if variation and not item.variations.filter(pk=variation.pk).exists():
|
||||||
raise ValidationError(_('This variation does not belong to this product.'))
|
raise ValidationError(_('This variation does not belong to this product.'))
|
||||||
if item.has_variations and not variation and data.get('block_quota'):
|
|
||||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
|
||||||
'Otherwise it might be unclear which quotas to block.'))
|
|
||||||
if item.category and item.category.is_addon:
|
if item.category and item.category.is_addon:
|
||||||
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
|
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
|
||||||
elif block_quota:
|
elif block_quota:
|
||||||
@@ -430,7 +428,15 @@ class Voucher(LoggedModel):
|
|||||||
elif old_instance.variation:
|
elif old_instance.variation:
|
||||||
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
||||||
elif old_instance.item:
|
elif old_instance.item:
|
||||||
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
if old_instance.item.has_variations:
|
||||||
|
quotas |= set(
|
||||||
|
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
|
||||||
|
itemvariation__item=old_instance.item,
|
||||||
|
quota__subevent=old_instance.subevent,
|
||||||
|
).values('quota_id'))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
||||||
return quotas
|
return quotas
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -445,13 +451,19 @@ class Voucher(LoggedModel):
|
|||||||
|
|
||||||
if quota:
|
if quota:
|
||||||
new_quotas = {quota}
|
new_quotas = {quota}
|
||||||
elif item and item.has_variations and not variation:
|
|
||||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
|
||||||
'Otherwise it might be unclear which quotas to block.'))
|
|
||||||
elif item and variation:
|
elif item and variation:
|
||||||
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
||||||
elif item and not item.has_variations:
|
elif item and not item.has_variations:
|
||||||
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
||||||
|
elif item and item.has_variations:
|
||||||
|
new_quotas = set(
|
||||||
|
Quota.objects.filter(
|
||||||
|
pk__in=Quota.variations.through.objects.filter(
|
||||||
|
itemvariation__item=old_instance.item,
|
||||||
|
quota__subevent=data.get('subevent'),
|
||||||
|
).values('quota_id')
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||||
'tickets.'))
|
'tickets.'))
|
||||||
|
|||||||
@@ -259,9 +259,6 @@ class WaitingListEntry(LoggedModel):
|
|||||||
if not self.email:
|
if not self.email:
|
||||||
return
|
return
|
||||||
|
|
||||||
for k, v in self.event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
with language(self.locale, self.event.settings.region):
|
with language(self.locale, self.event.settings.region):
|
||||||
recipient = self.email
|
recipient = self.email
|
||||||
|
|
||||||
|
|||||||
@@ -849,7 +849,7 @@ class BasePaymentProvider:
|
|||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if 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 not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import DatabaseError, transaction
|
from django.db import DatabaseError, transaction
|
||||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
|
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
|
||||||
@@ -112,6 +113,15 @@ error_messages = {
|
|||||||
'Some of the products you selected are no longer available in '
|
'Some of the products you selected are no longer available in '
|
||||||
'the quantity you selected. Please see below for details.'
|
'the quantity you selected. Please see below for details.'
|
||||||
),
|
),
|
||||||
|
'unavailable_listed': gettext_lazy(
|
||||||
|
'Some of the products you selected are no longer available. '
|
||||||
|
'The following products are affected and have not been added to your cart: %s'
|
||||||
|
),
|
||||||
|
'in_part_listed': gettext_lazy(
|
||||||
|
'Some of the products you selected are no longer available in '
|
||||||
|
'the quantity you selected. The following products are affected and have not '
|
||||||
|
'been added to your cart: %s'
|
||||||
|
),
|
||||||
'max_items': ngettext_lazy(
|
'max_items': ngettext_lazy(
|
||||||
"You cannot select more than %s item per order.",
|
"You cannot select more than %s item per order.",
|
||||||
"You cannot select more than %s items per order."
|
"You cannot select more than %s items per order."
|
||||||
@@ -378,8 +388,9 @@ class CartManager:
|
|||||||
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
|
||||||
not op.position.addon_to_id])
|
not op.position.addon_to_id])
|
||||||
if cartsize > int(self.event.settings.max_items_per_order):
|
limit = min(int(self.event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
|
||||||
raise CartError(error_messages['max_items'] % self.event.settings.max_items_per_order)
|
if cartsize > limit:
|
||||||
|
raise CartError(error_messages['max_items'] % limit)
|
||||||
|
|
||||||
def _check_item_constraints(self, op, current_ops=[]):
|
def _check_item_constraints(self, op, current_ops=[]):
|
||||||
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||||
@@ -1103,6 +1114,8 @@ class CartManager:
|
|||||||
if 'sleep-after-quota-check' in debugflags_var.get():
|
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||||
sleep(2)
|
sleep(2)
|
||||||
|
|
||||||
|
err_unavailable_products = []
|
||||||
|
|
||||||
for iop, op in enumerate(self._operations):
|
for iop, op in enumerate(self._operations):
|
||||||
if isinstance(op, self.RemoveOperation):
|
if isinstance(op, self.RemoveOperation):
|
||||||
if op.position.expires > self.now_dt:
|
if op.position.expires > self.now_dt:
|
||||||
@@ -1130,9 +1143,15 @@ class CartManager:
|
|||||||
voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher])
|
voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher])
|
||||||
|
|
||||||
if quota_available_count < 1:
|
if quota_available_count < 1:
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable_listed']
|
||||||
|
err_unavailable_products.append(
|
||||||
|
f'{op.item.name} – {op.variation}' if op.variation else op.item.name
|
||||||
|
)
|
||||||
elif quota_available_count < requested_count:
|
elif quota_available_count < requested_count:
|
||||||
err = err or error_messages['in_part']
|
err = err or error_messages['in_part_listed']
|
||||||
|
err_unavailable_products.append(
|
||||||
|
f'{op.item.name} – {op.variation}' if op.variation else op.item.name
|
||||||
|
)
|
||||||
|
|
||||||
if voucher_available_count < 1:
|
if voucher_available_count < 1:
|
||||||
if op.voucher in self._voucher_depend_on_cart:
|
if op.voucher in self._voucher_depend_on_cart:
|
||||||
@@ -1149,16 +1168,25 @@ class CartManager:
|
|||||||
b_quotas = list(b.quotas)
|
b_quotas = list(b.quotas)
|
||||||
if not b_quotas:
|
if not b_quotas:
|
||||||
if not op.voucher or not op.voucher.allow_ignore_quota:
|
if not op.voucher or not op.voucher.allow_ignore_quota:
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable_listed']
|
||||||
|
err_unavailable_products.append(
|
||||||
|
f'{op.item.name} – {op.variation}' if op.variation else op.item.name
|
||||||
|
)
|
||||||
available_count = 0
|
available_count = 0
|
||||||
continue
|
continue
|
||||||
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b_quotas))
|
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b_quotas))
|
||||||
if b_quota_available_count < b.count:
|
if b_quota_available_count < b.count:
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable_listed']
|
||||||
|
err_unavailable_products.append(
|
||||||
|
f'{op.item.name} – {op.variation}' if op.variation else op.item.name
|
||||||
|
)
|
||||||
available_count = 0
|
available_count = 0
|
||||||
elif b_quota_available_count < available_count * b.count:
|
elif b_quota_available_count < available_count * b.count:
|
||||||
err = err or error_messages['in_part']
|
err = err or error_messages['in_part_listed']
|
||||||
available_count = b_quota_available_count // b.count
|
available_count = b_quota_available_count // b.count
|
||||||
|
err_unavailable_products.append(
|
||||||
|
f'{op.item.name} – {op.variation}' if op.variation else op.item.name
|
||||||
|
)
|
||||||
for q in b_quotas:
|
for q in b_quotas:
|
||||||
quotas_ok[q] -= available_count * b.count
|
quotas_ok[q] -= available_count * b.count
|
||||||
# TODO: is this correct?
|
# TODO: is this correct?
|
||||||
@@ -1297,10 +1325,23 @@ class CartManager:
|
|||||||
|
|
||||||
op.position.price_after_voucher = op.price_after_voucher
|
op.position.price_after_voucher = op.price_after_voucher
|
||||||
op.position.voucher = op.voucher
|
op.position.voucher = op.voucher
|
||||||
# op.posiiton.price will be set in recompute_final_prices_and_taxes
|
# op.position.price will be set in recompute_final_prices_and_taxes
|
||||||
op.position.save(update_fields=['price_after_voucher', 'voucher'])
|
op.position.save(update_fields=['price_after_voucher', 'voucher'])
|
||||||
vouchers_ok[op.voucher] -= 1
|
vouchers_ok[op.voucher] -= 1
|
||||||
|
|
||||||
|
if op.voucher.all_bundles_included or op.voucher.all_addons_included:
|
||||||
|
for a in op.position.addons.all():
|
||||||
|
if a.is_bundled and op.voucher.all_bundles_included and a.price:
|
||||||
|
a.listed_price = Decimal("0.00")
|
||||||
|
a.price_after_voucher = Decimal("0.00")
|
||||||
|
# a.price will be set in recompute_final_prices_and_taxes
|
||||||
|
a.save(update_fields=['listed_price', 'price_after_voucher'])
|
||||||
|
elif not a.is_bundled and op.voucher.all_addons_included and a.price and not a.custom_price_input:
|
||||||
|
a.listed_price = Decimal("0.00")
|
||||||
|
a.price_after_voucher = Decimal("0.00")
|
||||||
|
# op.positon.price will be set in recompute_final_prices_and_taxes
|
||||||
|
a.save(update_fields=['listed_price', 'price_after_voucher'])
|
||||||
|
|
||||||
for p in new_cart_positions:
|
for p in new_cart_positions:
|
||||||
if getattr(p, '_answers', None):
|
if getattr(p, '_answers', None):
|
||||||
if not p.pk: # We stored some to the database already before
|
if not p.pk: # We stored some to the database already before
|
||||||
@@ -1310,6 +1351,10 @@ class CartManager:
|
|||||||
|
|
||||||
if 'sleep-before-commit' in debugflags_var.get():
|
if 'sleep-before-commit' in debugflags_var.get():
|
||||||
sleep(2)
|
sleep(2)
|
||||||
|
|
||||||
|
if err in (error_messages['unavailable_listed'], error_messages['in_part_listed']):
|
||||||
|
err = err % ', '.join(str(p) for p in err_unavailable_products)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def recompute_final_prices_and_taxes(self):
|
def recompute_final_prices_and_taxes(self):
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from functools import partial, reduce
|
from functools import partial, reduce
|
||||||
@@ -65,6 +66,8 @@ from pretix.helpers.jsonlogic_query import (
|
|||||||
MinutesSince, tolerance,
|
MinutesSince, tolerance,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _build_time(t=None, value=None, ev=None, now_dt=None):
|
def _build_time(t=None, value=None, ev=None, now_dt=None):
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or now()
|
||||||
@@ -199,7 +202,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
'var': values[0]["var"],
|
'var': values[0]["var"],
|
||||||
'rhs': values[1:],
|
'rhs': values[1:],
|
||||||
}
|
}
|
||||||
elif "entries_since" in values[0] or "entries_before" in values[0]:
|
elif any(t in values[0] for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
|
||||||
_var_explanations[new_var_name] = {
|
_var_explanations[new_var_name] = {
|
||||||
'operator': operator,
|
'operator': operator,
|
||||||
'var': values[0],
|
'var': values[0],
|
||||||
@@ -277,11 +280,13 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
var_weights[vname] = (500, 0)
|
var_weights[vname] = (500, 0)
|
||||||
var_texts[vname] = _('Wrong entrance gate')
|
var_texts[vname] = _('Wrong entrance gate')
|
||||||
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday') \
|
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday') \
|
||||||
or (isinstance(var, dict) and ("entries_since" in var or "entries_before" in var)):
|
or (isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before"))):
|
||||||
w = {
|
w = {
|
||||||
'minutes_since_first_entry': 80,
|
'minutes_since_first_entry': 80,
|
||||||
'minutes_since_last_entry': 90,
|
'minutes_since_last_entry': 90,
|
||||||
'entries_days': 100,
|
'entries_days': 100,
|
||||||
|
'entries_days_since': 105,
|
||||||
|
'entries_days_before': 105,
|
||||||
'entries_since': 110,
|
'entries_since': 110,
|
||||||
'entries_before': 110,
|
'entries_before': 110,
|
||||||
'entries_number': 120,
|
'entries_number': 120,
|
||||||
@@ -304,10 +309,12 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
|
|||||||
'entries_today': _('number of entries today'),
|
'entries_today': _('number of entries today'),
|
||||||
'entries_since': _('number of entries since {datetime}'),
|
'entries_since': _('number of entries since {datetime}'),
|
||||||
'entries_before': _('number of entries before {datetime}'),
|
'entries_before': _('number of entries before {datetime}'),
|
||||||
|
'entries_days_since': _('number of days with an entry since {datetime}'),
|
||||||
|
'entries_days_before': _('number of days with an entry before {datetime}'),
|
||||||
'now_isoweekday': _('week day'),
|
'now_isoweekday': _('week day'),
|
||||||
}
|
}
|
||||||
|
|
||||||
if isinstance(var, dict) and ("entries_since" in var or "entries_before" in var):
|
if isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
|
||||||
varname = list(var.keys())[0]
|
varname = list(var.keys())[0]
|
||||||
cutoff = _build_time(*var[varname][0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
|
cutoff = _build_time(*var[varname][0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
|
||||||
if abs(now_dt - cutoff) < timedelta(hours=12):
|
if abs(now_dt - cutoff) < timedelta(hours=12):
|
||||||
@@ -407,6 +414,8 @@ def _get_logic_environment(ev, rule_data, now_dt):
|
|||||||
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
|
||||||
logic.add_operation('entries_since', lambda t1: rule_data.entries_since(t1))
|
logic.add_operation('entries_since', lambda t1: rule_data.entries_since(t1))
|
||||||
logic.add_operation('entries_before', lambda t1: rule_data.entries_before(t1))
|
logic.add_operation('entries_before', lambda t1: rule_data.entries_before(t1))
|
||||||
|
logic.add_operation('entries_days_since', lambda t1: rule_data.entries_days_since(t1))
|
||||||
|
logic.add_operation('entries_days_before', lambda t1: rule_data.entries_days_before(t1))
|
||||||
return logic
|
return logic
|
||||||
|
|
||||||
|
|
||||||
@@ -464,6 +473,32 @@ class LazyRuleVars:
|
|||||||
self.__cache['entries_before', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__lt=cutoff).count()
|
self.__cache['entries_before', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__lt=cutoff).count()
|
||||||
return self.__cache['entries_before', cutoff]
|
return self.__cache['entries_before', cutoff]
|
||||||
|
|
||||||
|
def entries_days_since(self, cutoff):
|
||||||
|
tz = self._clist.event.timezone
|
||||||
|
with override(tz):
|
||||||
|
if ('entries_days_since', cutoff) not in self.__cache:
|
||||||
|
self.__cache['entries_days_since', cutoff] = self._position.checkins.filter(
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
list=self._clist,
|
||||||
|
datetime__gte=cutoff
|
||||||
|
).annotate(
|
||||||
|
day=TruncDate('datetime', tzinfo=tz)
|
||||||
|
).values('day').distinct().count()
|
||||||
|
return self.__cache['entries_days_since', cutoff]
|
||||||
|
|
||||||
|
def entries_days_before(self, cutoff):
|
||||||
|
tz = self._clist.event.timezone
|
||||||
|
with override(tz):
|
||||||
|
if ('entries_days_before', cutoff) not in self.__cache:
|
||||||
|
self.__cache['entries_days_before', cutoff] = self._position.checkins.filter(
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
list=self._clist,
|
||||||
|
datetime__lt=cutoff
|
||||||
|
).annotate(
|
||||||
|
day=TruncDate('datetime', tzinfo=tz)
|
||||||
|
).values('day').distinct().count()
|
||||||
|
return self.__cache['entries_days_before', cutoff]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def entries_days(self):
|
def entries_days(self):
|
||||||
tz = self._clist.event.timezone
|
tz = self._clist.event.timezone
|
||||||
@@ -530,7 +565,8 @@ class SQLLogic:
|
|||||||
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
|
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
|
||||||
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
|
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
|
||||||
}
|
}
|
||||||
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'}
|
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before',
|
||||||
|
'entries_days_since', 'entries_days_before'}
|
||||||
|
|
||||||
def operation_to_expression(self, rule):
|
def operation_to_expression(self, rule):
|
||||||
if not isinstance(rule, dict):
|
if not isinstance(rule, dict):
|
||||||
@@ -608,6 +644,42 @@ class SQLLogic:
|
|||||||
Value(0),
|
Value(0),
|
||||||
output_field=IntegerField()
|
output_field=IntegerField()
|
||||||
)
|
)
|
||||||
|
elif operator == 'entries_days_since':
|
||||||
|
tz = self.list.event.timezone
|
||||||
|
return Coalesce(
|
||||||
|
Subquery(
|
||||||
|
Checkin.objects.filter(
|
||||||
|
position_id=OuterRef('pk'),
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
list_id=self.list.pk,
|
||||||
|
datetime__gte=self.operation_to_expression(values[0]),
|
||||||
|
).annotate(
|
||||||
|
day=TruncDate('datetime', tzinfo=tz)
|
||||||
|
).values('position_id').order_by().annotate(
|
||||||
|
c=Count('day', distinct=True)
|
||||||
|
).values('c')
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
output_field=IntegerField()
|
||||||
|
)
|
||||||
|
elif operator == 'entries_days_before':
|
||||||
|
tz = self.list.event.timezone
|
||||||
|
return Coalesce(
|
||||||
|
Subquery(
|
||||||
|
Checkin.objects.filter(
|
||||||
|
position_id=OuterRef('pk'),
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
list_id=self.list.pk,
|
||||||
|
datetime__lt=self.operation_to_expression(values[0]),
|
||||||
|
).annotate(
|
||||||
|
day=TruncDate('datetime', tzinfo=tz)
|
||||||
|
).values('position_id').order_by().annotate(
|
||||||
|
c=Count('day', distinct=True)
|
||||||
|
).values('c')
|
||||||
|
),
|
||||||
|
Value(0),
|
||||||
|
output_field=IntegerField()
|
||||||
|
)
|
||||||
elif operator == 'var':
|
elif operator == 'var':
|
||||||
if values[0] == 'now':
|
if values[0] == 'now':
|
||||||
return Value(now().astimezone(timezone.utc))
|
return Value(now().astimezone(timezone.utc))
|
||||||
@@ -964,7 +1036,16 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
if type == Checkin.TYPE_ENTRY and clist.rules:
|
if type == Checkin.TYPE_ENTRY and clist.rules:
|
||||||
rule_data = LazyRuleVars(op, clist, dt, gate=gate)
|
rule_data = LazyRuleVars(op, clist, dt, gate=gate)
|
||||||
logic = _get_logic_environment(op.subevent or clist.event, rule_data, now_dt=dt)
|
logic = _get_logic_environment(op.subevent or clist.event, rule_data, now_dt=dt)
|
||||||
if not logic.apply(clist.rules, rule_data):
|
try:
|
||||||
|
logic_result = logic.apply(clist.rules, rule_data)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Check-in rule evaluation failed")
|
||||||
|
raise CheckInError(
|
||||||
|
_('Evaluation of custom rules has failed.'),
|
||||||
|
'rules',
|
||||||
|
)
|
||||||
|
|
||||||
|
if not logic_result:
|
||||||
if force:
|
if force:
|
||||||
force_used = True
|
force_used = True
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
expire_date=date_format(invoice.order.expires, "SHORT_DATE_FORMAT")
|
expire_date=date_format(invoice.order.expires, "SHORT_DATE_FORMAT")
|
||||||
)
|
)
|
||||||
|
|
||||||
invoice.introductory_text = str(introductory).replace('\n', '<br />')
|
invoice.introductory_text = str(introductory).replace('\n', '<br />').replace('\r', '')
|
||||||
invoice.additional_text = str(additional).replace('\n', '<br />')
|
invoice.additional_text = str(additional).replace('\n', '<br />').replace('\r', '')
|
||||||
invoice.footer_text = str(footer)
|
invoice.footer_text = str(footer)
|
||||||
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
|
invoice.payment_provider_text = str(payment).replace('\n', '<br />').replace('\r', '')
|
||||||
invoice.payment_provider_stamp = str(payment_stamp) if payment_stamp else None
|
invoice.payment_provider_stamp = str(payment_stamp) if payment_stamp else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -462,10 +462,10 @@ def build_preview_invoice_pdf(event):
|
|||||||
footer = event.settings.get('invoice_footer_text', as_type=LazyI18nString)
|
footer = event.settings.get('invoice_footer_text', as_type=LazyI18nString)
|
||||||
payment = _("A payment provider specific text might appear here.")
|
payment = _("A payment provider specific text might appear here.")
|
||||||
|
|
||||||
invoice.introductory_text = str(introductory).replace('\n', '<br />')
|
invoice.introductory_text = str(introductory).replace('\n', '<br />').replace('\r', '')
|
||||||
invoice.additional_text = str(additional).replace('\n', '<br />')
|
invoice.additional_text = str(additional).replace('\n', '<br />').replace('\r', '')
|
||||||
invoice.footer_text = str(footer)
|
invoice.footer_text = str(footer)
|
||||||
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
|
invoice.payment_provider_text = str(payment).replace('\n', '<br />').replace('\r', '')
|
||||||
invoice.payment_provider_stamp = _('paid')
|
invoice.payment_provider_stamp = _('paid')
|
||||||
invoice.invoice_to_name = _("John Doe")
|
invoice.invoice_to_name = _("John Doe")
|
||||||
invoice.invoice_to_street = _("214th Example Street")
|
invoice.invoice_to_street = _("214th Example Street")
|
||||||
@@ -488,7 +488,7 @@ def build_preview_invoice_pdf(event):
|
|||||||
InvoiceLine.objects.create(
|
InvoiceLine.objects.create(
|
||||||
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||||
gross_value=tax.gross, tax_value=tax.tax,
|
gross_value=tax.gross, tax_value=tax.tax,
|
||||||
tax_rate=tax.rate
|
tax_rate=tax.rate, tax_name=tax.name
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
|
|||||||
@@ -183,12 +183,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
if auto_email:
|
if auto_email:
|
||||||
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
|
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
|
||||||
headers['Auto-Submitted'] = 'auto-generated'
|
headers['Auto-Submitted'] = 'auto-generated'
|
||||||
|
headers.setdefault('X-Mailer', 'pretix')
|
||||||
|
|
||||||
with language(locale):
|
with language(locale):
|
||||||
if isinstance(context, dict) and event:
|
|
||||||
for k, v in event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
if isinstance(context, dict) and order:
|
if isinstance(context, dict) and order:
|
||||||
try:
|
try:
|
||||||
context.update({
|
context.update({
|
||||||
@@ -386,6 +383,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
if event:
|
if event:
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
event = Event.objects.get(id=event)
|
event = Event.objects.get(id=event)
|
||||||
|
organizer = event.organizer
|
||||||
backend = event.get_mail_backend()
|
backend = event.get_mail_backend()
|
||||||
cm = lambda: scope(organizer=event.organizer) # noqa
|
cm = lambda: scope(organizer=event.organizer) # noqa
|
||||||
elif organizer:
|
elif organizer:
|
||||||
@@ -573,8 +571,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
except smtplib.SMTPRecipientsRefused as e:
|
except smtplib.SMTPRecipientsRefused as e:
|
||||||
smtp_codes = [a[0] for a in e.recipients.values()]
|
smtp_codes = [a[0] for a in e.recipients.values()]
|
||||||
|
|
||||||
if not any(c >= 500 for c in smtp_codes):
|
if not any(c >= 500 for c in smtp_codes) or any(b'Message is too large' in a[1] for a in e.recipients.values()):
|
||||||
# Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals
|
# This is not a permanent failure (mailbox full, service unavailable), retry later, but with large
|
||||||
|
# intervals. One would think that "Message is too lage" is a permanent failure, but apparently it is not.
|
||||||
|
# We have documented cases of emails to Microsoft returning the error occasionally and then later
|
||||||
|
# allowing the very same email.
|
||||||
try:
|
try:
|
||||||
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800, 1800][self.request.retries])
|
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800, 1800][self.request.retries])
|
||||||
except MaxRetriesExceededError:
|
except MaxRetriesExceededError:
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def send_notification_mail(notification: Notification, user: User):
|
|||||||
tpl_html = get_template('pretixbase/email/notification.html')
|
tpl_html = get_template('pretixbase/email/notification.html')
|
||||||
|
|
||||||
body_html = tpl_html.render(ctx)
|
body_html = tpl_html.render(ctx)
|
||||||
inliner = css_inline.CSSInliner(remove_style_tags=True)
|
inliner = css_inline.CSSInliner(keep_style_tags=False)
|
||||||
body_html = inliner.inline(body_html)
|
body_html = inliner.inline(body_html)
|
||||||
|
|
||||||
tpl_plain = get_template('pretixbase/email/notification.txt')
|
tpl_plain = get_template('pretixbase/email/notification.txt')
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import csv
|
|||||||
import io
|
import io
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.conf import settings as django_settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@@ -91,7 +92,15 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
|||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
with language(locale, event.settings.region):
|
with language(locale, event.settings.region):
|
||||||
cols = get_all_columns(event)
|
cols = get_all_columns(event)
|
||||||
parsed = parse_csv(cf.file, charset=charset)
|
try:
|
||||||
|
parsed = parse_csv(cf.file, charset=charset)
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
raise DataImportError(
|
||||||
|
_(
|
||||||
|
'Error decoding special characters in your file: {message}').format(
|
||||||
|
message=str(e)
|
||||||
|
)
|
||||||
|
)
|
||||||
orders = []
|
orders = []
|
||||||
order = None
|
order = None
|
||||||
data = []
|
data = []
|
||||||
@@ -116,6 +125,11 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
|||||||
)
|
)
|
||||||
data.append(values)
|
data.append(values)
|
||||||
|
|
||||||
|
if settings['orders'] == 'one' and len(data) > django_settings.PRETIX_MAX_ORDER_SIZE:
|
||||||
|
raise DataImportError(
|
||||||
|
_('Orders cannot have more than %(max)s positions.') % {'max': django_settings.PRETIX_MAX_ORDER_SIZE}
|
||||||
|
)
|
||||||
|
|
||||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||||
# shorter. We'll see what works better in reality…
|
# shorter. We'll see what works better in reality…
|
||||||
lock_seats = []
|
lock_seats = []
|
||||||
|
|||||||
@@ -98,10 +98,9 @@ from pretix.base.services.pricing import (
|
|||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||||
from pretix.base.signals import (
|
from pretix.base.signals import (
|
||||||
allow_ticket_download, order_approved, order_canceled, order_changed,
|
order_approved, order_canceled, order_changed, order_denied, order_expired,
|
||||||
order_denied, order_expired, order_fee_calculation, order_paid,
|
order_fee_calculation, order_paid, order_placed, order_split,
|
||||||
order_placed, order_split, order_valid_if_pending, periodic_task,
|
order_valid_if_pending, periodic_task, validate_order,
|
||||||
validate_order,
|
|
||||||
)
|
)
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
@@ -1408,23 +1407,16 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
if o.download_reminder_sent:
|
if o.download_reminder_sent:
|
||||||
# Race condition
|
# Race condition
|
||||||
continue
|
continue
|
||||||
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
|
positions = list(o.positions_with_tickets)
|
||||||
|
if not positions:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not o.ticket_download_available:
|
if not o.ticket_download_available:
|
||||||
continue
|
continue
|
||||||
positions = o.positions.select_related('item')
|
|
||||||
|
|
||||||
if o.status != Order.STATUS_PAID:
|
if o.status != Order.STATUS_PAID:
|
||||||
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
||||||
continue
|
continue
|
||||||
send = False
|
|
||||||
for p in positions:
|
|
||||||
if p.generate_ticket:
|
|
||||||
send = True
|
|
||||||
break
|
|
||||||
if not send:
|
|
||||||
continue
|
|
||||||
|
|
||||||
with language(o.locale, o.event.settings.region):
|
with language(o.locale, o.event.settings.region):
|
||||||
o.download_reminder_sent = True
|
o.download_reminder_sent = True
|
||||||
@@ -1442,10 +1434,7 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
logger.exception('Reminder email could not be sent')
|
logger.exception('Reminder email could not be sent')
|
||||||
|
|
||||||
if event.settings.mail_send_download_reminder_attendee:
|
if event.settings.mail_send_download_reminder_attendee:
|
||||||
for p in o.positions.all():
|
for p in positions:
|
||||||
if not p.generate_ticket:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if p.subevent_id:
|
if p.subevent_id:
|
||||||
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
||||||
hour=0, minute=0, second=0, microsecond=0
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
@@ -1512,6 +1501,7 @@ class OrderChangeManager:
|
|||||||
"You need to select at least %(min)s items of the product %(product)s.",
|
"You need to select at least %(min)s items of the product %(product)s.",
|
||||||
"min"
|
"min"
|
||||||
),
|
),
|
||||||
|
'max_order_size': gettext_lazy('Orders cannot have more than %(max)s positions.'),
|
||||||
}
|
}
|
||||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||||
@@ -2599,6 +2589,14 @@ class OrderChangeManager:
|
|||||||
self.order.total = total + payment_fee
|
self.order.total = total + payment_fee
|
||||||
self.order.save()
|
self.order.save()
|
||||||
|
|
||||||
|
def _check_order_size(self):
|
||||||
|
if (len(self.order.positions.all()) + len([op for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
|
||||||
|
raise OrderError(
|
||||||
|
self.error_messages['max_order_size'] % {
|
||||||
|
'max': settings.PRETIX_MAX_ORDER_SIZE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _payment_fee_diff(self):
|
def _payment_fee_diff(self):
|
||||||
total = self.order.total + self._totaldiff
|
total = self.order.total + self._totaldiff
|
||||||
if self.open_payment:
|
if self.open_payment:
|
||||||
@@ -2739,6 +2737,7 @@ class OrderChangeManager:
|
|||||||
|
|
||||||
# finally, incorporate difference in payment fees
|
# finally, incorporate difference in payment fees
|
||||||
self._payment_fee_diff()
|
self._payment_fee_diff()
|
||||||
|
self._check_order_size()
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
locked_instance = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
|
locked_instance = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.formats import date_format
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.forms import PlaceholderValidator
|
||||||
|
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||||
|
from pretix.base.i18n import (
|
||||||
|
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||||
|
)
|
||||||
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
|
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
||||||
|
from pretix.base.signals import (
|
||||||
|
register_mail_placeholders, register_text_placeholders,
|
||||||
|
)
|
||||||
|
from pretix.helpers.format import SafeFormatter
|
||||||
|
|
||||||
|
logger = logging.getLogger('pretix.base.services.placeholders')
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTextPlaceholder:
|
||||||
|
"""
|
||||||
|
This is the base class for all email text placeholders.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_context(self):
|
||||||
|
"""
|
||||||
|
This property should return a list of all attribute names that need to be
|
||||||
|
contained in the base context so that this placeholder is available. By default,
|
||||||
|
it returns a list containing the string "event".
|
||||||
|
"""
|
||||||
|
return ["event"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self):
|
||||||
|
"""
|
||||||
|
This should return the identifier of this placeholder in the email.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
"""
|
||||||
|
This method is called to generate the actual text that is being
|
||||||
|
used in the email. You will be passed a context dictionary with the
|
||||||
|
base context attributes specified in ``required_context``. You are
|
||||||
|
expected to return a plain-text string.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def render_sample(self, event):
|
||||||
|
"""
|
||||||
|
This method is called to generate a text to be used in email previews.
|
||||||
|
This may only depend on the event.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
|
||||||
|
def __init__(self, identifier, args, func, sample):
|
||||||
|
self._identifier = identifier
|
||||||
|
self._args = args
|
||||||
|
self._func = func
|
||||||
|
self._sample = sample
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self):
|
||||||
|
return self._identifier
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_context(self):
|
||||||
|
return self._args
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
return self._func(**{k: context[k] for k in self._args})
|
||||||
|
|
||||||
|
def render_sample(self, event):
|
||||||
|
if callable(self._sample):
|
||||||
|
return self._sample(event)
|
||||||
|
else:
|
||||||
|
return self._sample
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceholderContext(SafeFormatter):
|
||||||
|
"""
|
||||||
|
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
||||||
|
an email or other templated text.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
context = PlaceholderContext(event=my_event, order=my_order)
|
||||||
|
formatted_doc = context.format(input_doc)
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__({})
|
||||||
|
self.context_args = kwargs
|
||||||
|
self._extend_context_args()
|
||||||
|
self.placeholders = {}
|
||||||
|
self.cache = {}
|
||||||
|
event = kwargs['event']
|
||||||
|
for r, val in [
|
||||||
|
*register_mail_placeholders.send(sender=event),
|
||||||
|
*register_text_placeholders.send(sender=event)
|
||||||
|
]:
|
||||||
|
if not isinstance(val, (list, tuple)):
|
||||||
|
val = [val]
|
||||||
|
for v in val:
|
||||||
|
if all(rp in kwargs for rp in v.required_context):
|
||||||
|
self.placeholders[v.identifier] = v
|
||||||
|
|
||||||
|
def _extend_context_args(self):
|
||||||
|
from pretix.base.models import InvoiceAddress
|
||||||
|
|
||||||
|
if 'position' in self.context_args:
|
||||||
|
self.context_args.setdefault("position_or_address", self.context_args['position'])
|
||||||
|
if 'order' in self.context_args:
|
||||||
|
try:
|
||||||
|
if not self.context_args.get('invoice_address'):
|
||||||
|
self.context_args['invoice_address'] = self.context_args['order'].invoice_address
|
||||||
|
except InvoiceAddress.DoesNotExist:
|
||||||
|
self.context_args['invoice_address'] = InvoiceAddress(order=self.context_args['order'])
|
||||||
|
finally:
|
||||||
|
self.context_args.setdefault("position_or_address", self.context_args['invoice_address'])
|
||||||
|
|
||||||
|
def render_placeholder(self, placeholder):
|
||||||
|
try:
|
||||||
|
return self.cache[placeholder.identifier]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
value = self.cache[placeholder.identifier] = placeholder.render(self.context_args)
|
||||||
|
return value
|
||||||
|
except:
|
||||||
|
logger.exception(f'Failed to process template placeholder {placeholder.identifier}.')
|
||||||
|
return '(error)'
|
||||||
|
|
||||||
|
def render_all(self):
|
||||||
|
return {identifier: self.render_placeholder(placeholder)
|
||||||
|
for (identifier, placeholder) in self.placeholders.items()}
|
||||||
|
|
||||||
|
def get_value(self, key, args, kwargs):
|
||||||
|
if key not in self.placeholders:
|
||||||
|
return '{' + str(key) + '}'
|
||||||
|
return self.render_placeholder(self.placeholders[key])
|
||||||
|
|
||||||
|
|
||||||
|
def _placeholder_payments(order, payments):
|
||||||
|
d = []
|
||||||
|
for payment in payments:
|
||||||
|
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
||||||
|
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
|
||||||
|
else:
|
||||||
|
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
|
||||||
|
d = [line for line in d if line.strip()]
|
||||||
|
if d:
|
||||||
|
return '\n\n'.join(d)
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_name(position_or_address, parts=False):
|
||||||
|
"""
|
||||||
|
Return the best name we got for either an invoice address or an order position, falling back to the respective other
|
||||||
|
"""
|
||||||
|
from pretix.base.models import InvoiceAddress, OrderPosition
|
||||||
|
if isinstance(position_or_address, InvoiceAddress):
|
||||||
|
if position_or_address.name:
|
||||||
|
return position_or_address.name_parts if parts else position_or_address.name
|
||||||
|
elif position_or_address.order:
|
||||||
|
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
||||||
|
|
||||||
|
if isinstance(position_or_address, OrderPosition):
|
||||||
|
if position_or_address.attendee_name:
|
||||||
|
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
|
||||||
|
elif position_or_address.order:
|
||||||
|
try:
|
||||||
|
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
|
||||||
|
except InvoiceAddress.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {} if parts else ""
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
||||||
|
def base_placeholders(sender, **kwargs):
|
||||||
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
ph = [
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
||||||
|
lambda event_or_subevent: event_or_subevent.name
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'code', ['order'], lambda order: order.code, 'F8VVL'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'order_email', ['order'], lambda order: order.email, 'john@example.org'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'invoice_number', ['invoice'],
|
||||||
|
lambda invoice: invoice.full_invoice_no,
|
||||||
|
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
||||||
|
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
||||||
|
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'pending_sum', ['event', 'pending_sum'],
|
||||||
|
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
|
||||||
|
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
||||||
|
event.currency),
|
||||||
|
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
|
||||||
|
lambda event: LazyDate(now() + timedelta(days=15))
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.open', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.open', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'hash': '98kusd8ofsj8dnkd'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.modify', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.modify', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.change', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.change', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.cancel', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.cancel', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position',
|
||||||
|
kwargs={
|
||||||
|
'order': position.order.code,
|
||||||
|
'secret': position.web_secret,
|
||||||
|
'position': position.positionid
|
||||||
|
}
|
||||||
|
),
|
||||||
|
lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'position': '123'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||||
|
lambda order, event:
|
||||||
|
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||||
|
if order.modify_deadline
|
||||||
|
else '',
|
||||||
|
lambda event: date_format(
|
||||||
|
event.settings.get(
|
||||||
|
'last_order_modification_date', as_type=RelativeDateWrapper
|
||||||
|
).datetime(event).astimezone(event.timezone),
|
||||||
|
'SHORT_DATETIME_FORMAT'
|
||||||
|
) if event.settings.get('last_order_modification_date') else '',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||||
|
lambda event: str(event.location or ''),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_admission_time', ['event_or_subevent'],
|
||||||
|
lambda event_or_subevent:
|
||||||
|
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
||||||
|
if event_or_subevent.date_admission
|
||||||
|
else '',
|
||||||
|
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'subevent', ['waiting_list_entry', 'event'],
|
||||||
|
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
|
||||||
|
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'subevent_date_from', ['waiting_list_entry', 'event'],
|
||||||
|
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
|
||||||
|
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_remove', ['waiting_list_voucher', 'event'],
|
||||||
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
|
event, 'presale:event.waitinglist.remove'
|
||||||
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
|
lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.waitinglist.remove',
|
||||||
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['waiting_list_voucher', 'event'],
|
||||||
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
|
event, 'presale:event.redeem'
|
||||||
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
|
lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.redeem',
|
||||||
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
|
||||||
|
_('John Doe')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
||||||
|
_('Sample Corporation')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||||
|
'* {} - {}'.format(
|
||||||
|
order.full_code,
|
||||||
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
for order in orders
|
||||||
|
), lambda event: '\n' + '\n\n'.join(
|
||||||
|
'* {} - {}'.format(
|
||||||
|
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||||
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
'order': order['code'],
|
||||||
|
'secret': order['secret'],
|
||||||
|
'hash': order['hash'],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
for order in [
|
||||||
|
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
|
||||||
|
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
|
||||||
|
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
||||||
|
event.settings.waiting_list_hours,
|
||||||
|
lambda event: event.settings.waiting_list_hours
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
|
||||||
|
_('Sample Admission Ticket')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
||||||
|
'68CYU2H6ZTP3WLK5'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
|
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
||||||
|
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
|
'voucher_url_list', ['event', 'voucher_list'],
|
||||||
|
lambda event, voucher_list: ' \n'.join([
|
||||||
|
build_absolute_uri(
|
||||||
|
event, 'presale:event.redeem'
|
||||||
|
) + '?voucher=' + c
|
||||||
|
for c in voucher_list
|
||||||
|
]),
|
||||||
|
lambda event: ' \n'.join([
|
||||||
|
build_absolute_uri(
|
||||||
|
event, 'presale:event.redeem'
|
||||||
|
) + '?voucher=' + c
|
||||||
|
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'name', ['name'], lambda name: name,
|
||||||
|
_('John Doe')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'comment', ['comment'], lambda comment: comment,
|
||||||
|
_('An individual text with a reason can be inserted here.'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'payment_info', ['order', 'payments'], _placeholder_payments,
|
||||||
|
_('The amount has been charged to your card.'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
||||||
|
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||||
|
_('John Doe'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'positionid', ['position'], lambda position: str(position.positionid),
|
||||||
|
'1'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'name', ['position_or_address'],
|
||||||
|
get_best_name,
|
||||||
|
_('John Doe'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
|
||||||
|
if "concatenation_for_salutation" in name_scheme:
|
||||||
|
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
|
||||||
|
else:
|
||||||
|
concatenation_for_salutation = name_scheme["concatenation"]
|
||||||
|
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
"name_for_salutation", ["waiting_list_entry"],
|
||||||
|
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
|
||||||
|
_("Mr Doe"),
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
"name", ["waiting_list_entry"],
|
||||||
|
lambda waiting_list_entry: waiting_list_entry.name or "",
|
||||||
|
_("Mr Doe"),
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
"name_for_salutation", ["position_or_address"],
|
||||||
|
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
|
||||||
|
_("Mr Doe"),
|
||||||
|
))
|
||||||
|
|
||||||
|
for f, l, w in name_scheme['fields']:
|
||||||
|
if f == 'full_name':
|
||||||
|
continue
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
|
||||||
|
name_scheme['sample'][f]
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
|
||||||
|
name_scheme['sample'][f]
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'name_%s' % f, ['position_or_address'],
|
||||||
|
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
|
||||||
|
name_scheme['sample'][f]
|
||||||
|
))
|
||||||
|
|
||||||
|
for k, v in sender.meta_data.items():
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||||
|
v
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
||||||
|
v
|
||||||
|
))
|
||||||
|
|
||||||
|
return ph
|
||||||
|
|
||||||
|
|
||||||
|
class FormPlaceholderMixin:
|
||||||
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
|
placeholders = get_available_placeholders(self.event, base_parameters)
|
||||||
|
ht = format_placeholders_help_text(placeholders, self.event)
|
||||||
|
if self.fields[fn].help_text:
|
||||||
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
|
else:
|
||||||
|
self.fields[fn].help_text = ht
|
||||||
|
self.fields[fn].validators.append(
|
||||||
|
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_placeholders(event, base_parameters):
|
||||||
|
if 'order' in base_parameters:
|
||||||
|
base_parameters.append('invoice_address')
|
||||||
|
base_parameters.append('position_or_address')
|
||||||
|
params = {}
|
||||||
|
for r, val in [*register_mail_placeholders.send(sender=event), *register_text_placeholders.send(sender=event)]:
|
||||||
|
if not isinstance(val, (list, tuple)):
|
||||||
|
val = [val]
|
||||||
|
for v in val:
|
||||||
|
if all(rp in base_parameters for rp in v.required_context):
|
||||||
|
params[v.identifier] = v
|
||||||
|
return params
|
||||||
@@ -90,8 +90,8 @@ class QuotaAvailability:
|
|||||||
self._count_waitinglist = count_waitinglist
|
self._count_waitinglist = count_waitinglist
|
||||||
self._ignore_closed = ignore_closed
|
self._ignore_closed = ignore_closed
|
||||||
self._full_results = full_results
|
self._full_results = full_results
|
||||||
self._item_to_quotas = defaultdict(list)
|
self._item_to_quotas = defaultdict(set)
|
||||||
self._var_to_quotas = defaultdict(list)
|
self._var_to_quotas = defaultdict(set)
|
||||||
self._early_out = early_out
|
self._early_out = early_out
|
||||||
self._quota_objects = {}
|
self._quota_objects = {}
|
||||||
self.results = {}
|
self.results = {}
|
||||||
@@ -243,13 +243,16 @@ class QuotaAvailability:
|
|||||||
quota_id__in=[q.pk for q in quotas]
|
quota_id__in=[q.pk for q in quotas]
|
||||||
).values('quota_id', 'item_id')
|
).values('quota_id', 'item_id')
|
||||||
for m in q_items:
|
for m in q_items:
|
||||||
self._item_to_quotas[m['item_id']].append(self._quota_objects[m['quota_id']])
|
self._item_to_quotas[m['item_id']].add(self._quota_objects[m['quota_id']])
|
||||||
|
|
||||||
q_vars = Quota.variations.through.objects.filter(
|
q_vars = Quota.variations.through.objects.filter(
|
||||||
quota_id__in=[q.pk for q in quotas]
|
quota_id__in=[q.pk for q in quotas]
|
||||||
).values('quota_id', 'itemvariation_id')
|
).values('quota_id', 'itemvariation_id', 'itemvariation__item_id')
|
||||||
for m in q_vars:
|
for m in q_vars:
|
||||||
self._var_to_quotas[m['itemvariation_id']].append(self._quota_objects[m['quota_id']])
|
self._var_to_quotas[m['itemvariation_id']].add(self._quota_objects[m['quota_id']])
|
||||||
|
# We can't be 100% certain that a quota, when it is connected to a variation, is also always connected to
|
||||||
|
# the parent item, so we double-check here just to be sure.
|
||||||
|
self._item_to_quotas[m['itemvariation__item_id']].add(self._quota_objects[m['quota_id']])
|
||||||
|
|
||||||
self._compute_orders(quotas, q_items, q_vars, size_left)
|
self._compute_orders(quotas, q_items, q_vars, size_left)
|
||||||
|
|
||||||
@@ -378,7 +381,10 @@ class QuotaAvailability:
|
|||||||
Q(
|
Q(
|
||||||
Q(
|
Q(
|
||||||
Q(variation_id__isnull=True) &
|
Q(variation_id__isnull=True) &
|
||||||
Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids})
|
Q(item_id__in=(
|
||||||
|
{i['item_id'] for i in q_items if i['quota_id'] in quota_ids} |
|
||||||
|
{i['itemvariation__item_id'] for i in q_vars if i['quota_id'] in quota_ids}
|
||||||
|
))
|
||||||
) | Q(
|
) | Q(
|
||||||
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}
|
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}
|
||||||
) | Q(
|
) | Q(
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ def dictsum(*dicts) -> dict:
|
|||||||
|
|
||||||
def order_overview(
|
def order_overview(
|
||||||
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
|
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
|
||||||
admission_only=False, base_qs=None, base_fees_qs=None,
|
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None
|
||||||
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
|
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
|
||||||
items = event.items.all().select_related(
|
items = event.items.all().select_related(
|
||||||
'category', # for re-grouping
|
'category', # for re-grouping
|
||||||
@@ -125,6 +125,11 @@ def order_overview(
|
|||||||
qs = qs.filter(subevent__in=subevent)
|
qs = qs.filter(subevent__in=subevent)
|
||||||
elif subevent:
|
elif subevent:
|
||||||
qs = qs.filter(subevent=subevent)
|
qs = qs.filter(subevent=subevent)
|
||||||
|
if subevent_date_from:
|
||||||
|
qs = qs.filter(subevent__date_from__gte=subevent_date_from)
|
||||||
|
if subevent_date_until:
|
||||||
|
qs = qs.filter(subevent__date_from__lt=subevent_date_until)
|
||||||
|
|
||||||
if admission_only:
|
if admission_only:
|
||||||
qs = qs.filter(item__admission=True)
|
qs = qs.filter(item__admission=True)
|
||||||
items = items.filter(admission=True)
|
items = items.filter(admission=True)
|
||||||
@@ -232,7 +237,7 @@ def order_overview(
|
|||||||
payment_cat_obj.name = _('Fees')
|
payment_cat_obj.name = _('Fees')
|
||||||
payment_items = []
|
payment_items = []
|
||||||
|
|
||||||
if subevent is None and fees:
|
if subevent is None and not subevent_date_from and not subevent_date_until and fees:
|
||||||
qs = OrderFee.all if base_fees_qs is None else base_fees_qs
|
qs = OrderFee.all if base_fees_qs is None else base_fees_qs
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
order__event=event
|
order__event=event
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.services.tasks import EventTask, ProfiledTask
|
from pretix.base.services.tasks import EventTask, ProfiledTask
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.database import rolledback_transaction
|
from pretix.helpers.database import rolledback_transaction
|
||||||
|
|
||||||
@@ -124,8 +124,8 @@ def preview(event: int, provider: str):
|
|||||||
|
|
||||||
|
|
||||||
def get_tickets_for_order(order, base_position=None):
|
def get_tickets_for_order(order, base_position=None):
|
||||||
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
|
positions = list(order.positions_with_tickets)
|
||||||
if not can_download:
|
if not positions:
|
||||||
return []
|
return []
|
||||||
if not order.ticket_download_available:
|
if not order.ticket_download_available:
|
||||||
return []
|
return []
|
||||||
@@ -135,10 +135,8 @@ def get_tickets_for_order(order, base_position=None):
|
|||||||
for receiver, response
|
for receiver, response
|
||||||
in register_ticket_outputs.send(order.event)
|
in register_ticket_outputs.send(order.event)
|
||||||
]
|
]
|
||||||
|
|
||||||
tickets = []
|
tickets = []
|
||||||
|
|
||||||
positions = list(order.positions_with_tickets)
|
|
||||||
if base_position:
|
if base_position:
|
||||||
# Only the given position and its children
|
# Only the given position and its children
|
||||||
positions = [
|
positions = [
|
||||||
@@ -202,7 +200,6 @@ def get_tickets_for_order(order, base_position=None):
|
|||||||
))
|
))
|
||||||
except:
|
except:
|
||||||
logger.exception('Failed to generate ticket.')
|
logger.exception('Failed to generate ticket.')
|
||||||
|
|
||||||
return tickets
|
return tickets
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -306,9 +306,11 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.IntegerField,
|
'serializer_class': serializers.IntegerField,
|
||||||
'serializer_kwargs': dict(
|
'serializer_kwargs': dict(
|
||||||
min_value=1,
|
min_value=1,
|
||||||
|
max_value=settings.PRETIX_MAX_ORDER_SIZE,
|
||||||
),
|
),
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
min_value=1,
|
min_value=1,
|
||||||
|
max_value=settings.PRETIX_MAX_ORDER_SIZE,
|
||||||
required=True,
|
required=True,
|
||||||
label=_("Maximum number of items per order"),
|
label=_("Maximum number of items per order"),
|
||||||
help_text=_("Add-on products will not be counted.")
|
help_text=_("Add-on products will not be counted.")
|
||||||
@@ -2910,6 +2912,25 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
label=_('Use header image also for events without an individually uploaded logo'),
|
label=_('Use header image also for events without an individually uploaded logo'),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'favicon': {
|
||||||
|
'default': None,
|
||||||
|
'type': File,
|
||||||
|
'form_class': ExtFileField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_('Favicon'),
|
||||||
|
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
||||||
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||||
|
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
||||||
|
'We recommend a size of at least 200x200px to accommodate most devices.')
|
||||||
|
),
|
||||||
|
'serializer_class': UploadedFileField,
|
||||||
|
'serializer_kwargs': dict(
|
||||||
|
allowed_types=[
|
||||||
|
'image/png', 'image/jpeg', 'image/gif', 'image/x-icon', 'image/vnd.microsoft.icon',
|
||||||
|
],
|
||||||
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||||
|
)
|
||||||
|
},
|
||||||
'og_image': {
|
'og_image': {
|
||||||
'default': None,
|
'default': None,
|
||||||
'type': File,
|
'type': File,
|
||||||
@@ -3208,6 +3229,12 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
label=_('Length of gift card codes'),
|
label=_('Length of gift card codes'),
|
||||||
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
|
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
|
||||||
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
|
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
|
||||||
|
min_value=6,
|
||||||
|
max_value=64,
|
||||||
|
),
|
||||||
|
'serializer_kwargs': dict(
|
||||||
|
min_value=6,
|
||||||
|
max_value=64,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'giftcard_expiry_years': {
|
'giftcard_expiry_years': {
|
||||||
@@ -3636,7 +3663,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
|
|||||||
'BR': (['State'], 'short'),
|
'BR': (['State'], 'short'),
|
||||||
'CA': (['Province', 'Territory'], 'short'),
|
'CA': (['Province', 'Territory'], 'short'),
|
||||||
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
|
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
|
||||||
'MY': (['State'], 'long'),
|
'MY': (['State', 'Federal territory'], 'long'),
|
||||||
'MX': (['State', 'Federal district'], 'short'),
|
'MX': (['State', 'Federal district'], 'short'),
|
||||||
'US': (['State', 'Outlying area', 'District'], 'short'),
|
'US': (['State', 'Outlying area', 'District'], 'short'),
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-11
@@ -153,19 +153,47 @@ class BaseDataShredder:
|
|||||||
|
|
||||||
|
|
||||||
def shred_log_fields(logentry, banlist=None, whitelist=None):
|
def shred_log_fields(logentry, banlist=None, whitelist=None):
|
||||||
|
def _shred(d, banlist, whitelist):
|
||||||
|
shredded = False
|
||||||
|
if whitelist:
|
||||||
|
for k, v in d.items():
|
||||||
|
if k not in whitelist:
|
||||||
|
if isinstance(d[k], list):
|
||||||
|
newlist = []
|
||||||
|
for i in d[k]:
|
||||||
|
if isinstance(i, dict):
|
||||||
|
_shred(i, None, [None])
|
||||||
|
else:
|
||||||
|
i = '█'
|
||||||
|
newlist.append(i)
|
||||||
|
d[k] = newlist
|
||||||
|
elif isinstance(d[k], dict):
|
||||||
|
_shred(d[k], None, [None])
|
||||||
|
elif d[k]:
|
||||||
|
d[k] = '█'
|
||||||
|
shredded = True
|
||||||
|
elif banlist:
|
||||||
|
for k in banlist:
|
||||||
|
if k in d:
|
||||||
|
if isinstance(d[k], list):
|
||||||
|
newlist = []
|
||||||
|
for i in d[k]:
|
||||||
|
if isinstance(i, dict):
|
||||||
|
_shred(i, None, [None])
|
||||||
|
else:
|
||||||
|
i = '█'
|
||||||
|
newlist.append(i)
|
||||||
|
d[k] = newlist
|
||||||
|
elif isinstance(d[k], dict):
|
||||||
|
_shred(d[k], None, [None])
|
||||||
|
elif d[k]:
|
||||||
|
d[k] = '█'
|
||||||
|
shredded = True
|
||||||
|
return shredded
|
||||||
|
|
||||||
d = logentry.parsed_data
|
d = logentry.parsed_data
|
||||||
initial_data = copy.copy(d)
|
initial_data = copy.copy(d)
|
||||||
shredded = False
|
shredded = _shred(d, banlist, whitelist)
|
||||||
if whitelist:
|
|
||||||
for k, v in d.items():
|
|
||||||
if k not in whitelist:
|
|
||||||
d[k] = '█'
|
|
||||||
shredded = True
|
|
||||||
elif banlist:
|
|
||||||
for f in banlist:
|
|
||||||
if f in d:
|
|
||||||
d[f] = '█'
|
|
||||||
shredded = True
|
|
||||||
if d != initial_data:
|
if d != initial_data:
|
||||||
logentry.data = json.dumps(d)
|
logentry.data = json.dumps(d)
|
||||||
logentry.shredded = logentry.shredded or shredded
|
logentry.shredded = logentry.shredded or shredded
|
||||||
|
|||||||
@@ -220,14 +220,20 @@ subclass of pretix.base.payment.BasePaymentProvider or a list of these
|
|||||||
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_mail_placeholders = EventPluginSignal()
|
register_text_placeholders = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known email text placeholders. Receivers should return
|
This signal is sent out to get all known text placeholders. Receivers should return
|
||||||
an instance of a subclass of pretix.base.email.BaseMailTextPlaceholder or a list of these.
|
an instance of a subclass of pretix.base.services.placeholders.BaseTextPlaceholder or a
|
||||||
|
list of these.
|
||||||
|
|
||||||
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_mail_placeholders = EventPluginSignal()
|
||||||
|
"""
|
||||||
|
**DEPRECATED**: This signal has a new name, please use ``register_text_placeholders`` instead.
|
||||||
|
"""
|
||||||
|
|
||||||
register_html_mail_renderers = EventPluginSignal()
|
register_html_mail_renderers = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known HTML email renderers. Receivers should return a
|
This signal is sent out to get all known HTML email renderers. Receivers should return a
|
||||||
@@ -646,7 +652,7 @@ allow_ticket_download = EventPluginSignal()
|
|||||||
Arguments: ``order``
|
Arguments: ``order``
|
||||||
|
|
||||||
This signal is sent out to check if tickets for an order can be downloaded. If any receiver returns false,
|
This signal is sent out to check if tickets for an order can be downloaded. If any receiver returns false,
|
||||||
a download will not be offered.
|
a download will not be offered. If a receiver returns a list of OrderPositions, only those will be downloadable.
|
||||||
|
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{# this is the version from django 3.x, prior to https://github.com/django/django/commit/5942ab5eb165ee2e759174e297148a40dd855920 so that django-bootstrap3 can keep doing its magic #}
|
|
||||||
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
|
|
||||||
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}"{% endif %}>{% endif %}{% for option in options %}
|
|
||||||
<li>{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
|
|
||||||
</ul></li>{% endif %}{% endfor %}
|
|
||||||
</ul>{% endwith %}
|
|
||||||
@@ -19,27 +19,16 @@
|
|||||||
# 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 random
|
|
||||||
import string
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
def generate_challenge(challenge_len):
|
@register.filter("iter_weekdays")
|
||||||
return ''.join([
|
def iter_weekdays(weeks):
|
||||||
random.SystemRandom().choice(string.ascii_letters + string.digits)
|
for i in range(7):
|
||||||
for i in range(challenge_len)
|
for w in weeks:
|
||||||
])
|
if i <= len(w) and w[i]:
|
||||||
|
yield w[i]['date']
|
||||||
|
break
|
||||||
def generate_ukey():
|
|
||||||
"""
|
|
||||||
Its value's id member is required, and contains an identifier
|
|
||||||
for the account, specified by the Relying Party. This is not meant
|
|
||||||
to be displayed to the user, but is used by the Relying Party to
|
|
||||||
control the number of credentials - an authenticator will never
|
|
||||||
contain more than one credential for a given Relying Party under
|
|
||||||
the same id.
|
|
||||||
A unique identifier for the entity. For a relying party entity,
|
|
||||||
sets the RP ID. For a user account entity, this will be an
|
|
||||||
arbitrary string specified by the relying party.
|
|
||||||
"""
|
|
||||||
return generate_challenge(20)
|
|
||||||
@@ -65,6 +65,8 @@ ALLOWED_TAGS_SNIPPET = [
|
|||||||
'i',
|
'i',
|
||||||
'strong',
|
'strong',
|
||||||
'span',
|
'span',
|
||||||
|
'strike',
|
||||||
|
's',
|
||||||
# Update doc/user/markdown.rst if you change this!
|
# Update doc/user/markdown.rst if you change this!
|
||||||
]
|
]
|
||||||
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ class BaseTicketOutput:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_tickets_to_print(self, order):
|
||||||
|
return order.positions_with_tickets
|
||||||
|
|
||||||
def generate_order(self, order: Order) -> Tuple[str, str, str]:
|
def generate_order(self, order: Order) -> Tuple[str, str, str]:
|
||||||
"""
|
"""
|
||||||
This method is the same as order() but should not generate one file per order position
|
This method is the same as order() but should not generate one file per order position
|
||||||
@@ -116,7 +119,7 @@ class BaseTicketOutput:
|
|||||||
"""
|
"""
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||||
for pos in order.positions_with_tickets:
|
for pos in self.get_tickets_to_print(order):
|
||||||
fname, __, content = self.generate(pos)
|
fname, __, content = self.generate(pos)
|
||||||
zipf.writestr('{}-{}{}'.format(
|
zipf.writestr('{}-{}{}'.format(
|
||||||
order.code, pos.positionid, os.path.splitext(fname)[1]
|
order.code, pos.positionid, os.path.splitext(fname)[1]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from datetime import datetime, time, timedelta
|
|||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.text import format_lazy
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django.utils.translation import pgettext_lazy
|
from django.utils.translation import pgettext_lazy
|
||||||
|
|
||||||
@@ -89,9 +90,10 @@ def timeline_for_event(event, subevent=None):
|
|||||||
datetime=(
|
datetime=(
|
||||||
ev.presale_end or ev.date_to or ev.date_from.astimezone(ev.timezone).replace(hour=23, minute=59, second=59)
|
ev.presale_end or ev.date_to or ev.date_from.astimezone(ev.timezone).replace(hour=23, minute=59, second=59)
|
||||||
),
|
),
|
||||||
description='{}{}'.format(
|
description=format_lazy(
|
||||||
|
'{} ({})',
|
||||||
pgettext_lazy('timeline', 'End of ticket sales'),
|
pgettext_lazy('timeline', 'End of ticket sales'),
|
||||||
f" ({pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured')})" if not ev.presale_end else ""
|
pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured') if not ev.presale_end else ""
|
||||||
),
|
),
|
||||||
edit_url=ev_edit_url
|
edit_url=ev_edit_url
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class EventSlugBanlistValidator(BanlistValidator):
|
|||||||
'widget',
|
'widget',
|
||||||
'customer',
|
'customer',
|
||||||
'account',
|
'account',
|
||||||
|
'lead',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
|
|||||||
'api',
|
'api',
|
||||||
'csp_report',
|
'csp_report',
|
||||||
'widget',
|
'widget',
|
||||||
|
'lead',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
|
|||||||
from django.core.files.uploadedfile import UploadedFile
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse, JsonResponse, QueryDict
|
from django.http import HttpResponse, JsonResponse, QueryDict
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import render
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
@@ -47,6 +47,7 @@ from redis import ResponseError
|
|||||||
from pretix.base.models import CachedFile, User
|
from pretix.base.models import CachedFile, User
|
||||||
from pretix.base.services.tasks import ProfiledEventTask
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
from pretix.helpers.http import redirect_to_url
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.tasks')
|
logger = logging.getLogger('pretix.base.tasks')
|
||||||
|
|
||||||
@@ -152,7 +153,7 @@ class AsyncMixin:
|
|||||||
'redirect': self.get_success_url(value),
|
'redirect': self.get_success_url(value),
|
||||||
'message': str(self.get_success_message(value))
|
'message': str(self.get_success_message(value))
|
||||||
})
|
})
|
||||||
return redirect(self.get_success_url(value))
|
return redirect_to_url(self.get_success_url(value))
|
||||||
|
|
||||||
def error(self, exception):
|
def error(self, exception):
|
||||||
if isinstance(exception, PermissionDenied):
|
if isinstance(exception, PermissionDenied):
|
||||||
@@ -165,7 +166,7 @@ class AsyncMixin:
|
|||||||
'redirect': self.get_error_url(),
|
'redirect': self.get_error_url(),
|
||||||
'message': str(self.get_error_message(exception))
|
'message': str(self.get_error_message(exception))
|
||||||
})
|
})
|
||||||
return redirect(self.get_error_url())
|
return redirect_to_url(self.get_error_url())
|
||||||
|
|
||||||
def get_error_message(self, exception):
|
def get_error_message(self, exception):
|
||||||
if isinstance(exception, dict) and exception['exc_type'] in self.known_errortypes:
|
if isinstance(exception, dict) and exception['exc_type'] in self.known_errortypes:
|
||||||
@@ -203,7 +204,7 @@ class AsyncAction(AsyncMixin):
|
|||||||
return self.success(res.info)
|
return self.success(res.info)
|
||||||
else:
|
else:
|
||||||
return self.error(res.info)
|
return self.error(res.info)
|
||||||
return redirect(self.get_check_url(res.id, False))
|
return redirect_to_url(self.get_check_url(res.id, False))
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||||
@@ -375,7 +376,7 @@ class AsyncFormView(AsyncMixin, FormView):
|
|||||||
return self.success(res.info)
|
return self.success(res.info)
|
||||||
else:
|
else:
|
||||||
return self.error(res.info)
|
return self.error(res.info)
|
||||||
return redirect(self.get_check_url(res.id, False))
|
return redirect_to_url(self.get_check_url(res.id, False))
|
||||||
|
|
||||||
|
|
||||||
class AsyncPostView(AsyncMixin, View):
|
class AsyncPostView(AsyncMixin, View):
|
||||||
@@ -478,4 +479,4 @@ class AsyncPostView(AsyncMixin, View):
|
|||||||
return self.success(res.info)
|
return self.success(res.info)
|
||||||
else:
|
else:
|
||||||
return self.error(res.info)
|
return self.error(res.info)
|
||||||
return redirect(self.get_check_url(res.id, False))
|
return redirect_to_url(self.get_check_url(res.id, False))
|
||||||
|
|||||||
@@ -123,8 +123,6 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
if hasattr(self.file, 'display_name'):
|
|
||||||
return self.file.display_name
|
|
||||||
return self.file.name
|
return self.file.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -219,15 +217,17 @@ class ExtValidationMixin:
|
|||||||
|
|
||||||
def clean(self, *args, **kwargs):
|
def clean(self, *args, **kwargs):
|
||||||
data = super().clean(*args, **kwargs)
|
data = super().clean(*args, **kwargs)
|
||||||
if isinstance(data, UploadedFile):
|
|
||||||
filename = data.name
|
from ...base.models import CachedFile
|
||||||
|
if isinstance(data, (UploadedFile, CachedFile)):
|
||||||
|
filename = data.name if isinstance(data, UploadedFile) else data.filename
|
||||||
ext = os.path.splitext(filename)[1]
|
ext = os.path.splitext(filename)[1]
|
||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
if ext not in self.ext_whitelist:
|
if ext not in self.ext_whitelist:
|
||||||
raise forms.ValidationError(_("Filetype not allowed!"))
|
raise forms.ValidationError(_("Filetype not allowed!"))
|
||||||
|
|
||||||
if ext in IMAGE_EXTS:
|
if ext in IMAGE_EXTS:
|
||||||
validate_uploaded_file_for_valid_image(data)
|
validate_uploaded_file_for_valid_image(data if isinstance(data, UploadedFile) else data.file)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -257,6 +257,12 @@ class CachedFileField(ExtFileField):
|
|||||||
if isinstance(data, File):
|
if isinstance(data, File):
|
||||||
if hasattr(data, '_uploaded_to'):
|
if hasattr(data, '_uploaded_to'):
|
||||||
return data._uploaded_to
|
return data._uploaded_to
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.clean(data)
|
||||||
|
except ValidationError:
|
||||||
|
return None
|
||||||
|
|
||||||
cf = CachedFile.objects.create(
|
cf = CachedFile.objects.create(
|
||||||
expires=now() + datetime.timedelta(days=1),
|
expires=now() + datetime.timedelta(days=1),
|
||||||
date=now(),
|
date=now(),
|
||||||
@@ -268,6 +274,9 @@ class CachedFileField(ExtFileField):
|
|||||||
cf.save()
|
cf.save()
|
||||||
data._uploaded_to = cf
|
data._uploaded_to = cf
|
||||||
return cf
|
return cf
|
||||||
|
if isinstance(data, CachedFile):
|
||||||
|
return data
|
||||||
|
|
||||||
return super().bound_data(data, initial)
|
return super().bound_data(data, initial)
|
||||||
|
|
||||||
def clean(self, *args, **kwargs):
|
def clean(self, *args, **kwargs):
|
||||||
@@ -415,3 +424,17 @@ class FontSelect(forms.RadioSelect):
|
|||||||
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
|
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
|
||||||
def label_from_instance(self, obj):
|
def label_from_instance(self, obj):
|
||||||
return str(obj) if obj.active else mark_safe(f'<strike class="text-muted">{escape(obj)}</strike>')
|
return str(obj) if obj.active else mark_safe(f'<strike class="text-muted">{escape(obj)}</strike>')
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonGroupRadioSelect(forms.RadioSelect):
|
||||||
|
template_name = 'pretixcontrol/button_group_radio.html'
|
||||||
|
option_template_name = 'pretixcontrol/button_group_radio_option.html'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.option_icons = kwargs.pop('option_icons')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||||
|
attrs['icon'] = self.option_icons[value]
|
||||||
|
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||||
|
return opt
|
||||||
|
|||||||
@@ -60,12 +60,11 @@ from i18nfield.forms import (
|
|||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.email import get_available_placeholders
|
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||||
from pretix.base.forms.widgets import format_placeholders_help_text
|
|
||||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
|
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||||
from pretix.base.settings import (
|
from pretix.base.settings import (
|
||||||
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
|
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
|
||||||
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
||||||
@@ -503,7 +502,7 @@ class EventSettingsValidationMixin:
|
|||||||
del self.cleaned_data[field]
|
del self.cleaned_data[field]
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, SettingsForm):
|
||||||
timezone = forms.ChoiceField(
|
timezone = forms.ChoiceField(
|
||||||
choices=((a, a) for a in common_timezones),
|
choices=((a, a) for a in common_timezones),
|
||||||
label=_("Event timezone"),
|
label=_("Event timezone"),
|
||||||
@@ -593,6 +592,10 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
'og_image',
|
'og_image',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
base_context = {
|
||||||
|
'frontpage_text': ['event'],
|
||||||
|
}
|
||||||
|
|
||||||
def _resolve_virtual_keys_input(self, data, prefix=''):
|
def _resolve_virtual_keys_input(self, data, prefix=''):
|
||||||
# set all dependants of virtual_keys and
|
# set all dependants of virtual_keys and
|
||||||
# delete all virtual_fields to prevent them from being saved
|
# delete all virtual_fields to prevent them from being saved
|
||||||
@@ -682,6 +685,9 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
else:
|
else:
|
||||||
self.initial[virtual_key] = 'do_not_ask'
|
self.initial[virtual_key] = 'do_not_ask'
|
||||||
|
|
||||||
|
for k, v in self.base_context.items():
|
||||||
|
self._set_field_placeholders(k, v)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def changed_data(self):
|
def changed_data(self):
|
||||||
data = []
|
data = []
|
||||||
@@ -932,7 +938,7 @@ def contains_web_channel_validate(val):
|
|||||||
raise ValidationError(_("The online shop must be selected to receive these emails."))
|
raise ValidationError(_("The online shop must be selected to receive these emails."))
|
||||||
|
|
||||||
|
|
||||||
class MailSettingsForm(SettingsForm):
|
class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||||
auto_fields = [
|
auto_fields = [
|
||||||
'mail_prefix',
|
'mail_prefix',
|
||||||
'mail_from_name',
|
'mail_from_name',
|
||||||
@@ -1344,17 +1350,6 @@ class MailSettingsForm(SettingsForm):
|
|||||||
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
|
||||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
|
||||||
ht = format_placeholders_help_text(placeholders, self.event)
|
|
||||||
if self.fields[fn].help_text:
|
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
|
||||||
else:
|
|
||||||
self.fields[fn].help_text = ht
|
|
||||||
self.fields[fn].validators.append(
|
|
||||||
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = event = kwargs.get('obj')
|
self.event = event = kwargs.get('obj')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ from pretix.base.forms.widgets import (
|
|||||||
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, SubEvent,
|
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SubEvent,
|
||||||
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
@@ -230,6 +230,7 @@ class OrderFilterForm(FilterForm):
|
|||||||
('partially_paid', _('Partially paid')),
|
('partially_paid', _('Partially paid')),
|
||||||
('underpaid', _('Underpaid (but confirmed)')),
|
('underpaid', _('Underpaid (but confirmed)')),
|
||||||
('pendingpaid', _('Pending (but fully paid)')),
|
('pendingpaid', _('Pending (but fully paid)')),
|
||||||
|
('pendingnopayment', _('Pending (but no current payment)')),
|
||||||
)),
|
)),
|
||||||
(_('Approval process'), (
|
(_('Approval process'), (
|
||||||
('na', _('Approved, payment pending')),
|
('na', _('Approved, payment pending')),
|
||||||
@@ -327,6 +328,18 @@ class OrderFilterForm(FilterForm):
|
|||||||
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
|
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
|
||||||
& Q(require_approval=False)
|
& Q(require_approval=False)
|
||||||
)
|
)
|
||||||
|
elif s == 'pendingnopayment':
|
||||||
|
qs = qs.exclude(
|
||||||
|
Exists(
|
||||||
|
OrderPayment.objects.filter(
|
||||||
|
order=OuterRef('pk'),
|
||||||
|
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).filter(
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
require_approval=False,
|
||||||
|
)
|
||||||
elif s == 'partially_paid':
|
elif s == 'partially_paid':
|
||||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
@@ -578,11 +591,10 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
widget=FilterNullBooleanSelect,
|
widget=FilterNullBooleanSelect,
|
||||||
label=_('At least one ticket with check-in'),
|
label=_('At least one ticket with check-in'),
|
||||||
)
|
)
|
||||||
checkin_attention = forms.NullBooleanField(
|
quota = SafeModelChoiceField(
|
||||||
|
queryset=Quota.objects.none(),
|
||||||
|
label=_('Affected quota'),
|
||||||
required=False,
|
required=False,
|
||||||
widget=FilterNullBooleanSelect,
|
|
||||||
label=_('Requires special attention'),
|
|
||||||
help_text=_('Only matches orders with the attention checkbox set directly for the order, not based on the product.'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -667,6 +679,17 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
label=_('Ticket secret'),
|
label=_('Ticket secret'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
self.fields['quota'].queryset = self.event.quotas.all()
|
||||||
|
self.fields['quota'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'generic',
|
||||||
|
'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['quota'].widget.choices = self.fields['quota'].choices
|
||||||
for q in self.event.questions.all():
|
for q in self.event.questions.all():
|
||||||
self.fields['question_{}'.format(q.pk)] = forms.CharField(
|
self.fields['question_{}'.format(q.pk)] = forms.CharField(
|
||||||
label=q.question,
|
label=q.question,
|
||||||
@@ -760,6 +783,12 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
all_positions__secret__icontains=fdata.get('ticket_secret')
|
all_positions__secret__icontains=fdata.get('ticket_secret')
|
||||||
).distinct()
|
).distinct()
|
||||||
|
if fdata.get('quota'):
|
||||||
|
quota = fdata['quota']
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(all_positions__item__in=quota.items.all(), all_positions__variation__isnull=True) |
|
||||||
|
Q(all_positions__variation__in=quota.variations.all())
|
||||||
|
).distinct()
|
||||||
for q in self.event.questions.all():
|
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(
|
answers = QuestionAnswer.objects.filter(
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ 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 (
|
||||||
ItemMultipleChoiceField, SizeValidationMixin, SplitDateTimeField,
|
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
|
||||||
SplitDateTimePickerWidget,
|
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
from pretix.helpers.money import change_decimal_field
|
from pretix.helpers.money import change_decimal_field
|
||||||
|
|
||||||
@@ -207,14 +207,20 @@ class QuestionOptionForm(I18nModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class QuotaForm(I18nModelForm):
|
class QuotaForm(I18nModelForm):
|
||||||
|
itemvars = forms.MultipleChoiceField(
|
||||||
|
label=_("Products"),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.instance = kwargs.get('instance', None)
|
self.instance = kwargs.get('instance', None)
|
||||||
self.event = kwargs.get('event')
|
self.event = kwargs.get('event')
|
||||||
items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations')
|
items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations')
|
||||||
|
searchable_selection = kwargs.pop('searchable_selection', None)
|
||||||
self.original_instance = modelcopy(self.instance) if self.instance else None
|
self.original_instance = modelcopy(self.instance) if self.instance else None
|
||||||
initial = kwargs.get('initial', {})
|
initial = kwargs.get('initial', {})
|
||||||
if self.instance and self.instance.pk and 'itemvars' not in initial:
|
if self.instance and self.instance.pk and 'itemvars' not in initial:
|
||||||
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all()] + [
|
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all() if (len(i.variations.all()) == 0)] + [
|
||||||
'{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all()
|
'{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all()
|
||||||
]
|
]
|
||||||
kwargs['initial'] = initial
|
kwargs['initial'] = initial
|
||||||
@@ -231,12 +237,22 @@ class QuotaForm(I18nModelForm):
|
|||||||
else:
|
else:
|
||||||
choices.append(('{}'.format(item.pk), str(item) if item.active else mark_safe(f'<strike class="text-muted">{escape(item)}</strike>')))
|
choices.append(('{}'.format(item.pk), str(item) if item.active else mark_safe(f'<strike class="text-muted">{escape(item)}</strike>')))
|
||||||
|
|
||||||
self.fields['itemvars'] = forms.MultipleChoiceField(
|
if searchable_selection:
|
||||||
label=_('Products'),
|
self.fields['itemvars'].widget = Select2ItemVarMulti(
|
||||||
required=True,
|
attrs={
|
||||||
choices=choices,
|
'data-model-select2': 'generic',
|
||||||
widget=forms.CheckboxSelectMultiple
|
'data-select2-url': reverse('control:event.items.itemvars.select2', kwargs={
|
||||||
)
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': _('No products')
|
||||||
|
},
|
||||||
|
choices=choices,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields['itemvars'].widget = forms.CheckboxSelectMultiple()
|
||||||
|
|
||||||
|
self.fields['itemvars'].choices = choices
|
||||||
|
|
||||||
if self.event.has_subevents:
|
if self.event.has_subevents:
|
||||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||||
@@ -380,7 +396,9 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
'description',
|
'description',
|
||||||
'active',
|
'active',
|
||||||
'available_from',
|
'available_from',
|
||||||
|
'available_from_mode',
|
||||||
'available_until',
|
'available_until',
|
||||||
|
'available_until_mode',
|
||||||
'require_voucher',
|
'require_voucher',
|
||||||
'hide_without_voucher',
|
'hide_without_voucher',
|
||||||
'allow_cancel',
|
'allow_cancel',
|
||||||
@@ -562,6 +580,34 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
)
|
)
|
||||||
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(
|
||||||
|
choices=self.fields['available_from_mode'].choices,
|
||||||
|
option_icons={
|
||||||
|
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||||
|
Item.UNAVAIL_MODE_INFO: 'info'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
|
||||||
|
choices=self.fields['available_until_mode'].choices,
|
||||||
|
option_icons={
|
||||||
|
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||||
|
Item.UNAVAIL_MODE_INFO: 'info'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect(
|
||||||
|
choices=(
|
||||||
|
(True, _("Hide product if unavailable")),
|
||||||
|
(False, _("Show product with info on why it’s unavailable")),
|
||||||
|
),
|
||||||
|
option_icons={
|
||||||
|
True: 'eye-slash',
|
||||||
|
False: 'info'
|
||||||
|
},
|
||||||
|
attrs={'data-checkbox-dependency': '#id_require_voucher'}
|
||||||
|
)
|
||||||
|
|
||||||
if self.instance.hidden_if_available_id:
|
if self.instance.hidden_if_available_id:
|
||||||
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
|
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
|
||||||
self.fields['hidden_if_available'].help_text = format_html(
|
self.fields['hidden_if_available'].help_text = format_html(
|
||||||
@@ -614,6 +660,15 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
|
|
||||||
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
|
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
|
||||||
|
|
||||||
|
self.fields['validity_dynamic_start_choice'] = forms.TypedChoiceField(
|
||||||
|
label=_("Start of validity"),
|
||||||
|
choices=(
|
||||||
|
("False", _("Purchase date")),
|
||||||
|
("True", _("Date chosen by customer")),
|
||||||
|
),
|
||||||
|
coerce=lambda x: x == 'True',
|
||||||
|
)
|
||||||
|
|
||||||
qs = self.event.organizer.membership_types.all()
|
qs = self.event.organizer.membership_types.all()
|
||||||
if qs:
|
if qs:
|
||||||
self.fields['require_membership_types'].queryset = qs
|
self.fields['require_membership_types'].queryset = qs
|
||||||
@@ -647,6 +702,9 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not d.get('require_voucher'):
|
||||||
|
d['hide_without_voucher'] = False
|
||||||
|
|
||||||
if d.get('require_membership') and not d.get('require_membership_types'):
|
if d.get('require_membership') and not d.get('require_membership_types'):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'require_membership_types',
|
'require_membership_types',
|
||||||
@@ -661,7 +719,7 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
if d.get('grant_membership_type'):
|
if d.get('grant_membership_type'):
|
||||||
if not d['grant_membership_type'].transferable and not d['personalized']:
|
if not d['grant_membership_type'].transferable and not d['personalized']:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'personalized' if d['admission'] else 'admission',
|
'personalized' if d.get('admission') else 'admission',
|
||||||
_("Your product grants a non-transferable membership and should therefore be a personalized "
|
_("Your product grants a non-transferable membership and should therefore be a personalized "
|
||||||
"admission ticket. Otherwise customers might not be able to use the membership later. If you "
|
"admission ticket. Otherwise customers might not be able to use the membership later. If you "
|
||||||
"want the membership to be non-personalized, set the membership type to be transferable.")
|
"want the membership to be non-personalized, set the membership type to be transferable.")
|
||||||
@@ -704,7 +762,9 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
'free_price_suggestion',
|
'free_price_suggestion',
|
||||||
'tax_rule',
|
'tax_rule',
|
||||||
'available_from',
|
'available_from',
|
||||||
|
'available_from_mode',
|
||||||
'available_until',
|
'available_until',
|
||||||
|
'available_until_mode',
|
||||||
'require_voucher',
|
'require_voucher',
|
||||||
'require_approval',
|
'require_approval',
|
||||||
'hide_without_voucher',
|
'hide_without_voucher',
|
||||||
@@ -841,6 +901,22 @@ class ItemVariationForm(I18nModelForm):
|
|||||||
|
|
||||||
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
|
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
|
||||||
|
|
||||||
|
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
|
||||||
|
choices=self.fields['available_from_mode'].choices,
|
||||||
|
option_icons={
|
||||||
|
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||||
|
Item.UNAVAIL_MODE_INFO: 'info'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
|
||||||
|
choices=self.fields['available_until_mode'].choices,
|
||||||
|
option_icons={
|
||||||
|
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||||
|
Item.UNAVAIL_MODE_INFO: 'info'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.meta_fields = []
|
self.meta_fields = []
|
||||||
meta_defaults = {}
|
meta_defaults = {}
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
@@ -883,7 +959,9 @@ class ItemVariationForm(I18nModelForm):
|
|||||||
'checkin_attention',
|
'checkin_attention',
|
||||||
'checkin_text',
|
'checkin_text',
|
||||||
'available_from',
|
'available_from',
|
||||||
|
'available_from_mode',
|
||||||
'available_until',
|
'available_until',
|
||||||
|
'available_until_mode',
|
||||||
'sales_channels',
|
'sales_channels',
|
||||||
'hide_without_voucher',
|
'hide_without_voucher',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ from pretix.base.models import (
|
|||||||
TaxRule,
|
TaxRule,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
|
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.control.forms import SplitDateTimeField
|
from pretix.control.forms import SplitDateTimeField
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
@@ -151,10 +152,6 @@ class ForceQuotaConfirmationForm(forms.Form):
|
|||||||
del self.fields['force']
|
del self.fields['force']
|
||||||
|
|
||||||
|
|
||||||
class ConfirmPaymentForm(ForceQuotaConfirmationForm):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ReactivateOrderForm(ForceQuotaConfirmationForm):
|
class ReactivateOrderForm(ForceQuotaConfirmationForm):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -220,10 +217,11 @@ class DenyForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MarkPaidForm(ConfirmPaymentForm):
|
class MarkPaidForm(ForceQuotaConfirmationForm):
|
||||||
send_email = forms.BooleanField(
|
send_email = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Notify customer by email'),
|
label=_('Notify customer by email'),
|
||||||
|
help_text=_('A mail will only be sent if the order is fully paid after this.'),
|
||||||
initial=True
|
initial=True
|
||||||
)
|
)
|
||||||
amount = forms.DecimalField(
|
amount = forms.DecimalField(
|
||||||
@@ -240,9 +238,10 @@ class MarkPaidForm(ConfirmPaymentForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
payment = kwargs.pop('payment', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
change_decimal_field(self.fields['amount'], self.instance.event.currency)
|
change_decimal_field(self.fields['amount'], self.instance.event.currency)
|
||||||
self.fields['amount'].initial = max(Decimal('0.00'), self.instance.pending_sum)
|
self.fields['amount'].initial = max(Decimal('0.00'), payment.amount if payment else self.instance.pending_sum)
|
||||||
|
|
||||||
|
|
||||||
class ExporterForm(forms.Form):
|
class ExporterForm(forms.Form):
|
||||||
@@ -705,6 +704,7 @@ class OrderMailForm(forms.Form):
|
|||||||
)
|
)
|
||||||
self.fields['attach_invoices'].queryset = order.invoices.all()
|
self.fields['attach_invoices'].queryset = order.invoices.all()
|
||||||
self._set_field_placeholders('message', ['event', 'order'])
|
self._set_field_placeholders('message', ['event', 'order'])
|
||||||
|
self._set_field_placeholders('subject', ['event', 'order'])
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionMailForm(OrderMailForm):
|
class OrderPositionMailForm(OrderMailForm):
|
||||||
@@ -720,6 +720,7 @@ class OrderPositionMailForm(OrderMailForm):
|
|||||||
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
|
initial=self.order.event.settings.mail_text_order_custom_mail.localize(self.order.locale),
|
||||||
)
|
)
|
||||||
self._set_field_placeholders('message', ['event', 'order', 'position'])
|
self._set_field_placeholders('message', ['event', 'order', 'position'])
|
||||||
|
self._set_field_placeholders('subject', ['event', 'order'])
|
||||||
|
|
||||||
|
|
||||||
class OrderRefundForm(forms.Form):
|
class OrderRefundForm(forms.Form):
|
||||||
@@ -767,7 +768,7 @@ class OrderRefundForm(forms.Form):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class EventCancelForm(forms.Form):
|
class EventCancelForm(FormPlaceholderMixin, forms.Form):
|
||||||
subevent = forms.ModelChoiceField(
|
subevent = forms.ModelChoiceField(
|
||||||
SubEvent.objects.none(),
|
SubEvent.objects.none(),
|
||||||
label=pgettext_lazy('subevent', 'Date'),
|
label=pgettext_lazy('subevent', 'Date'),
|
||||||
@@ -867,17 +868,6 @@ class EventCancelForm(forms.Form):
|
|||||||
send_waitinglist_subject = forms.CharField()
|
send_waitinglist_subject = forms.CharField()
|
||||||
send_waitinglist_message = forms.CharField()
|
send_waitinglist_message = forms.CharField()
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
|
||||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
|
||||||
ht = format_placeholders_help_text(placeholders, self.event)
|
|
||||||
if self.fields[fn].help_text:
|
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
|
||||||
else:
|
|
||||||
self.fields[fn].help_text = ht
|
|
||||||
self.fields[fn].validators.append(
|
|
||||||
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = kwargs.pop('event')
|
self.event = kwargs.pop('event')
|
||||||
kwargs.setdefault('initial', {})
|
kwargs.setdefault('initial', {})
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
'organizer_link_back',
|
'organizer_link_back',
|
||||||
'organizer_logo_image_large',
|
'organizer_logo_image_large',
|
||||||
'organizer_logo_image_inherit',
|
'organizer_logo_image_inherit',
|
||||||
|
'favicon',
|
||||||
'giftcard_length',
|
'giftcard_length',
|
||||||
'giftcard_expiry_years',
|
'giftcard_expiry_years',
|
||||||
'locales',
|
'locales',
|
||||||
@@ -464,14 +465,6 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||||
'as it will be resized on smaller screens.')
|
'as it will be resized on smaller screens.')
|
||||||
)
|
)
|
||||||
favicon = ExtFileField(
|
|
||||||
label=_('Favicon'),
|
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
|
||||||
required=False,
|
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
|
||||||
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
|
||||||
'We recommend a size of at least 200x200px to accommodate most devices.')
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
is_admin = kwargs.pop('is_admin', False)
|
is_admin = kwargs.pop('is_admin', False)
|
||||||
|
|||||||
@@ -19,18 +19,17 @@
|
|||||||
# 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 bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
|
||||||
from bootstrap3.text import text_value
|
from bootstrap3.text import text_value
|
||||||
from django.forms import CheckboxInput
|
from django.forms import CheckboxInput, CheckboxSelectMultiple, RadioSelect
|
||||||
from django.forms.utils import flatatt
|
from django.forms.utils import flatatt
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import pgettext
|
from django.utils.translation import pgettext
|
||||||
from i18nfield.forms import I18nFormField
|
from i18nfield.forms import I18nFormField
|
||||||
|
|
||||||
from pretix.base.forms.renderers import FieldRenderer, InlineFieldRenderer
|
|
||||||
|
|
||||||
|
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False):
|
||||||
def render_label(content, label_for=None, label_class=None, label_title='', optional=False):
|
|
||||||
"""
|
"""
|
||||||
Render a label with content
|
Render a label with content
|
||||||
"""
|
"""
|
||||||
@@ -41,6 +40,8 @@ def render_label(content, label_for=None, label_class=None, label_title='', opti
|
|||||||
attrs['class'] = label_class
|
attrs['class'] = label_class
|
||||||
if label_title:
|
if label_title:
|
||||||
attrs['title'] = label_title
|
attrs['title'] = label_title
|
||||||
|
if label_id:
|
||||||
|
attrs['id'] = label_id
|
||||||
|
|
||||||
if text_value(content) == ' ':
|
if text_value(content) == ' ':
|
||||||
# Empty label, e.g. checkbox
|
# Empty label, e.g. checkbox
|
||||||
@@ -61,6 +62,7 @@ class ControlFieldRenderer(FieldRenderer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['layout'] = 'horizontal'
|
kwargs['layout'] = 'horizontal'
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.is_group_widget = isinstance(self.widget, (CheckboxSelectMultiple, RadioSelect, )) or (self.is_multi_widget and len(self.widget.widgets) > 1)
|
||||||
|
|
||||||
def add_label(self, html):
|
def add_label(self, html):
|
||||||
label = self.get_label()
|
label = self.get_label()
|
||||||
@@ -73,14 +75,45 @@ class ControlFieldRenderer(FieldRenderer):
|
|||||||
else:
|
else:
|
||||||
required = self.field.field.required
|
required = self.field.field.required
|
||||||
|
|
||||||
|
if self.is_group_widget:
|
||||||
|
label_for = ""
|
||||||
|
label_id = "legend-{}".format(self.field.html_name)
|
||||||
|
else:
|
||||||
|
label_for = self.field.id_for_label
|
||||||
|
label_id = ""
|
||||||
|
|
||||||
html = render_label(
|
html = render_label(
|
||||||
label,
|
label,
|
||||||
label_for=self.field.id_for_label,
|
label_for=label_for,
|
||||||
label_class=self.get_label_class(),
|
label_class=self.get_label_class(),
|
||||||
|
label_id=label_id,
|
||||||
optional=not required and not isinstance(self.widget, CheckboxInput)
|
optional=not required and not isinstance(self.widget, CheckboxInput)
|
||||||
) + html
|
) + html
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
def wrap_label_and_field(self, html):
|
||||||
|
if self.is_group_widget:
|
||||||
|
attrs = ' role="group" aria-labelledby="legend-{}"'.format(self.field.html_name)
|
||||||
|
else:
|
||||||
|
attrs = ''
|
||||||
|
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['layout'] = 'horizontal'
|
||||||
|
kwargs['horizontal_field_class'] = 'col-md-7'
|
||||||
|
self.visibility_field = kwargs['visibility_field']
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def render_visibility_field(self):
|
||||||
|
return self.visibility_field.as_widget(attrs=self.visibility_field.field.widget.attrs)
|
||||||
|
|
||||||
|
def wrap_field(self, html):
|
||||||
|
html = super().wrap_field(html)
|
||||||
|
html += '<div class="col-md-2 text-right">' + self.render_visibility_field() + '</div>'
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
class BulkEditMixin:
|
class BulkEditMixin:
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,8 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
|
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
|
||||||
self.fields['price'].label = str(self.item)
|
self.fields['price'].label = str(self.item)
|
||||||
|
self.available_from_mode = self.item.available_from_mode
|
||||||
|
self.available_until_mode = self.item.available_until_mode
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEventItem
|
model = SubEventItem
|
||||||
@@ -287,6 +289,8 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
|
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
|
||||||
self.fields['price'].label = '{} – {}'.format(str(self.item), self.variation.value)
|
self.fields['price'].label = '{} – {}'.format(str(self.item), self.variation.value)
|
||||||
|
self.available_from_mode = self.variation.available_from_mode
|
||||||
|
self.available_until_mode = self.variation.available_until_mode
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEventItemVariation
|
model = SubEventItemVariation
|
||||||
@@ -356,6 +360,7 @@ class BulkSubEventItemVariationForm(SubEventItemVariationForm):
|
|||||||
class QuotaFormSet(I18nInlineFormSet):
|
class QuotaFormSet(I18nInlineFormSet):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.searchable_selection = kwargs.pop('searchable_selection', None)
|
||||||
self.event = kwargs.pop('event', None)
|
self.event = kwargs.pop('event', None)
|
||||||
self.locales = self.event.settings.get('locales')
|
self.locales = self.event.settings.get('locales')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -368,7 +373,7 @@ class QuotaFormSet(I18nInlineFormSet):
|
|||||||
kwargs['locales'] = self.locales
|
kwargs['locales'] = self.locales
|
||||||
kwargs['event'] = self.event
|
kwargs['event'] = self.event
|
||||||
kwargs['items'] = self.items
|
kwargs['items'] = self.items
|
||||||
kwargs['items'] = self.items
|
kwargs['searchable_selection'] = self.searchable_selection
|
||||||
return super()._construct_form(i, **kwargs)
|
return super()._construct_form(i, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -463,7 +468,8 @@ class RRuleFormSetForm(RRuleForm):
|
|||||||
|
|
||||||
RRuleFormSet = formset_factory(
|
RRuleFormSet = formset_factory(
|
||||||
RRuleFormSetForm,
|
RRuleFormSetForm,
|
||||||
can_order=False, can_delete=True, extra=1
|
min_num=1, validate_min=True,
|
||||||
|
can_order=False, can_delete=True, extra=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
itemvar = FakeChoiceField(
|
itemvar = FakeChoiceField(
|
||||||
label=_("Product"),
|
label=_("Product"),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"This product is added to the user's cart if the voucher is redeemed."
|
"This product is added to the user's cart if the voucher is redeemed. Instead of a specific product, you "
|
||||||
|
"can also select a quota. In this case, all products assigned to this quota can be selected."
|
||||||
),
|
),
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,3 +77,19 @@ class Select2ItemVarQuotaMixin(Select2Mixin):
|
|||||||
|
|
||||||
class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select):
|
class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Select2ItemVarMulti(Select2Mixin, forms.SelectMultiple):
|
||||||
|
def options(self, name, value, attrs=None):
|
||||||
|
# we need this for multi-selection without a queryset for the selection of items and variations
|
||||||
|
for i, v in enumerate(value):
|
||||||
|
yield self.create_option(
|
||||||
|
None,
|
||||||
|
v,
|
||||||
|
dict(self.choices)[v],
|
||||||
|
True,
|
||||||
|
i,
|
||||||
|
subindex=None,
|
||||||
|
attrs=attrs
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from urllib.parse import quote, urljoin, urlparse
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
from django.shortcuts import get_object_or_404, resolve_url
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import get_script_prefix, resolve, reverse
|
from django.urls import get_script_prefix, resolve, reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
@@ -46,6 +46,7 @@ from django_scopes import scope
|
|||||||
|
|
||||||
from pretix.base.models import Event, Organizer
|
from pretix.base.models import Event, Organizer
|
||||||
from pretix.base.models.auth import SuperuserPermissionSet, User
|
from pretix.base.models.auth import SuperuserPermissionSet, User
|
||||||
|
from pretix.helpers.http import redirect_to_url
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||||
)
|
)
|
||||||
@@ -118,7 +119,7 @@ class PermissionMiddleware:
|
|||||||
|
|
||||||
if hasattr(request, 'organizer'):
|
if hasattr(request, 'organizer'):
|
||||||
# If the user is on a organizer's subdomain, he should be redirected to pretix
|
# If the user is on a organizer's subdomain, he should be redirected to pretix
|
||||||
return redirect(urljoin(settings.SITE_URL, request.get_full_path()))
|
return redirect_to_url(urljoin(settings.SITE_URL, request.get_full_path()))
|
||||||
if url_name in self.EXCEPTIONS:
|
if url_name in self.EXCEPTIONS:
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
@@ -132,14 +133,14 @@ class PermissionMiddleware:
|
|||||||
return self._login_redirect(request)
|
return self._login_redirect(request)
|
||||||
except SessionReauthRequired:
|
except SessionReauthRequired:
|
||||||
if url_name not in ('user.reauth', 'auth.logout'):
|
if url_name not in ('user.reauth', 'auth.logout'):
|
||||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||||
|
|
||||||
if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
|
if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
|
||||||
return redirect(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
|
return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
|
||||||
|
|
||||||
if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \
|
if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \
|
||||||
and url_name not in self.EXCEPTIONS_2FA:
|
and url_name not in self.EXCEPTIONS_2FA and not request.user.needs_password_change:
|
||||||
return redirect(reverse('control:user.settings.2fa'))
|
return redirect_to_url(reverse('control:user.settings.2fa'))
|
||||||
|
|
||||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||||
if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-':
|
if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-':
|
||||||
@@ -152,7 +153,7 @@ class PermissionMiddleware:
|
|||||||
k = dict(url.kwargs)
|
k = dict(url.kwargs)
|
||||||
k['organizer'] = ev.organizer.slug
|
k['organizer'] = ev.organizer.slug
|
||||||
k['event'] = ev.slug
|
k['event'] = ev.slug
|
||||||
return redirect(reverse(url.view_name, kwargs=k, args=url.args))
|
return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args))
|
||||||
|
|
||||||
with scope(organizer=None):
|
with scope(organizer=None):
|
||||||
request.event = Event.objects.filter(
|
request.event = Event.objects.filter(
|
||||||
@@ -178,7 +179,7 @@ class PermissionMiddleware:
|
|||||||
"have no permission to administrate it."))
|
"have no permission to administrate it."))
|
||||||
k = dict(url.kwargs)
|
k = dict(url.kwargs)
|
||||||
k['organizer'] = org.slug
|
k['organizer'] = org.slug
|
||||||
return redirect(reverse(url.view_name, kwargs=k, args=url.args))
|
return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args))
|
||||||
|
|
||||||
request.organizer = Organizer.objects.filter(
|
request.organizer = Organizer.objects.filter(
|
||||||
slug=url.kwargs['organizer'],
|
slug=url.kwargs['organizer'],
|
||||||
|
|||||||
@@ -35,10 +35,11 @@
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from pretix.helpers.http import redirect_to_url
|
||||||
|
|
||||||
|
|
||||||
def current_url(request):
|
def current_url(request):
|
||||||
if request.GET:
|
if request.GET:
|
||||||
@@ -135,7 +136,7 @@ def administrator_permission_required():
|
|||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
if not request.user.has_active_staff_session(request.session.session_key):
|
if not request.user.has_active_staff_session(request.session.session_key):
|
||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
return redirect(reverse('control:user.sudo') + '?next=' + quote(current_url(request)))
|
return redirect_to_url(reverse('control:user.sudo') + '?next=' + quote(current_url(request)))
|
||||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||||
return function(request, *args, **kw)
|
return function(request, *args, **kw)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% with id=widget.attrs.id %}<div data-toggle="buttons"{% if id %} id="{{ id }}"{% endif %} class="btn-group btn-group-toggle {{ widget.attrs.class }}">{% for group, options, index in widget.optgroups %}
|
||||||
|
{% for option in options %}
|
||||||
|
{% include option.template_name with widget=option %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>{% endwith %}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} class="btn btn-primary-if-active form-field-boundary {% if widget.attrs.checked %} active{% endif %}" title="{{ widget.label }}" data-toggle="tooltip">
|
||||||
|
{% include "django/forms/widgets/input.html" %} <span class="fa fa-{{ widget.attrs.icon }}"></span></label>
|
||||||
@@ -95,8 +95,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field form.available_from layout="control" %}
|
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field form.available_until layout="control" %}
|
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field form.sales_channels layout="control" %}
|
{% bootstrap_field form.sales_channels layout="control" %}
|
||||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
||||||
{% bootstrap_field form.require_approval layout="control" %}
|
{% bootstrap_field form.require_approval layout="control" %}
|
||||||
@@ -194,7 +194,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field formset.empty_form.available_from layout="control" %}
|
{% bootstrap_field formset.empty_form.available_from visibility_field=formset.empty_form.available_from_mode layout="control_with_visibility" %}
|
||||||
|
{% bootstrap_field formset.empty_form.available_until visibility_field=formset.empty_form.available_until_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field formset.empty_form.available_until layout="control" %}
|
{% bootstrap_field formset.empty_form.available_until layout="control" %}
|
||||||
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}
|
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}
|
||||||
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}
|
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}
|
||||||
|
|||||||
@@ -152,27 +152,28 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Availability" %}</legend>
|
<legend>{% trans "Availability" %}</legend>
|
||||||
{% bootstrap_field form.sales_channels layout="control" %}
|
|
||||||
{% bootstrap_field form.available_from layout="control" %}
|
|
||||||
{% bootstrap_field form.available_until layout="control" %}
|
{% bootstrap_field form.sales_channels layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.max_per_order layout="control" %}
|
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field form.min_per_order layout="control" %}
|
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field form.require_voucher layout="control" %}
|
{% bootstrap_field form.max_per_order layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
{% bootstrap_field form.min_per_order layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.require_bundling layout="control" %}
|
{% bootstrap_field form.require_voucher visibility_field=form.hide_without_voucher layout="control_with_visibility" %}
|
||||||
|
{% bootstrap_field form.require_bundling layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% if form.require_membership %}
|
{% if form.require_membership %}
|
||||||
{% bootstrap_field form.require_membership layout="control" %}
|
{% bootstrap_field form.require_membership layout="control" %}
|
||||||
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
|
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
|
||||||
{% bootstrap_field form.require_membership_types layout="control" %}
|
{% bootstrap_field form.require_membership_types layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.require_membership_hidden layout="control" %}
|
{% bootstrap_field form.require_membership_hidden layout="control" horizontal_field_class="col-md-7" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field form.allow_cancel layout="control" %}
|
{% bootstrap_field form.allow_cancel layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.allow_waitinglist layout="control" %}
|
{% bootstrap_field form.allow_waitinglist layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% if form.hidden_if_available %}
|
{% if form.hidden_if_available %}
|
||||||
{% bootstrap_field form.hidden_if_available layout="control" %}
|
{% bootstrap_field form.hidden_if_available layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field form.hidden_if_item_available layout="control" %}
|
{% bootstrap_field form.hidden_if_item_available layout="control" horizontal_field_class="col-md-7" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% for v in formsets.values %}
|
{% for v in formsets.values %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -209,6 +210,7 @@
|
|||||||
{% bootstrap_field form.validity_fixed_until layout="control" %}
|
{% bootstrap_field form.validity_fixed_until layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
<div data-display-dependency="#{{ form.validity_mode.id_for_label }}" data-display-dependency-value="dynamic">
|
<div data-display-dependency="#{{ form.validity_mode.id_for_label }}" data-display-dependency-value="dynamic">
|
||||||
|
{% bootstrap_field form.validity_dynamic_start_choice layout="control" %}
|
||||||
<div class="form-group metadata-group">
|
<div class="form-group metadata-group">
|
||||||
<label class="col-md-3 control-label">{% trans "Duration" %}</label>
|
<label class="col-md-3 control-label">{% trans "Duration" %}</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@@ -231,14 +233,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% bootstrap_field form.validity_dynamic_start_choice layout="control" %}
|
<div data-display-dependency="#{{ form.validity_dynamic_start_choice.id_for_label }}" data-display-dependency-value="True">
|
||||||
<div data-display-dependency="#{{ form.validity_dynamic_start_choice.id_for_label }}">
|
|
||||||
{% trans "days" as t_days %}
|
{% trans "days" as t_days %}
|
||||||
{% bootstrap_field form.validity_dynamic_start_choice_day_limit addon_after=t_days layout="control" %}
|
{% bootstrap_field form.validity_dynamic_start_choice_day_limit addon_after=t_days layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset id="tab-item-additional-settings">
|
||||||
<legend>{% trans "Additional settings" %}</legend>
|
<legend>{% trans "Additional settings" %}</legend>
|
||||||
{% bootstrap_field form.issue_giftcard layout="control" %}
|
{% bootstrap_field form.issue_giftcard layout="control" %}
|
||||||
{% if form.grant_membership_type %}
|
{% if form.grant_membership_type %}
|
||||||
|
|||||||
@@ -70,6 +70,10 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</em>
|
</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if position.attendee_name %}
|
||||||
|
– <span class="fa fa-user" aria-hidden="true"></span>
|
||||||
|
{{ position.attendee_name }}
|
||||||
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|||||||
@@ -21,7 +21,13 @@
|
|||||||
Do you really want to mark this payment as complete?
|
Do you really want to mark this payment as complete?
|
||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
<input type="hidden" name="status" value="p" />
|
<input type="hidden" name="status" value="p" />
|
||||||
{% bootstrap_form form layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
{% bootstrap_form_errors form %}
|
||||||
|
{% bootstrap_field form.amount layout='horizontal' %}
|
||||||
|
{% bootstrap_field form.payment_date layout='horizontal' %}
|
||||||
|
{% bootstrap_field form.send_email layout='horizontal' %}
|
||||||
|
{% if form.force %}
|
||||||
|
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
||||||
|
{% endif %}
|
||||||
<div class="form-group submit-group">
|
<div class="form-group submit-group">
|
||||||
<a class="btn btn-default btn-lg"
|
<a class="btn btn-default btn-lg"
|
||||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
|
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
|
||||||
{% bootstrap_field sform.giftcard_length layout="control" %}
|
{% bootstrap_field sform.giftcard_length layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset id="tab-organizer-privacy">
|
||||||
<legend>{% trans "Privacy" %}</legend>
|
<legend>{% trans "Privacy" %}</legend>
|
||||||
{% bootstrap_field sform.privacy_url layout="control" %}
|
{% bootstrap_field sform.privacy_url layout="control" %}
|
||||||
<div class="alert alert-legal">
|
<div class="alert alert-legal">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user