mirror of
https://github.com/pretix/pretix.git
synced 2026-10-12 21:04:41 +00:00
Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b97b7466b | ||
|
|
6b7f2ad684 | ||
|
|
54b249acf5 | ||
|
|
0cf2ba763b | ||
|
|
b44c3d707e | ||
|
|
45603c6624 | ||
|
|
ba36d649c6 | ||
|
|
ca90b2af09 | ||
|
|
fe43862fa4 | ||
|
|
411cd764a6 | ||
|
|
6618e3a039 | ||
|
|
b7cd97c33e | ||
|
|
eae3229e68 | ||
|
|
d291cd02e7 | ||
|
|
e5dcb50229 | ||
|
|
9d333bc2cf | ||
|
|
45038d07f3 | ||
|
|
00f0dfec88 | ||
|
|
1975a9d891 | ||
|
|
02b9b6941d | ||
|
|
da2ac6c386 | ||
|
|
b9fbe699ab | ||
|
|
8e3f3cc8c8 | ||
|
|
945f80b288 | ||
|
|
25799da70d | ||
|
|
6b01023d16 | ||
|
|
98644db976 | ||
|
|
814ae7f4d9 | ||
|
|
ae39a1e112 | ||
|
|
61b94f1128 | ||
|
|
dbb586ad5a | ||
|
|
295b0443fb | ||
|
|
31031225d3 | ||
|
|
d600115ea5 | ||
|
|
3d9ebb3072 | ||
|
|
c9667e77c3 | ||
|
|
b2b797f4d3 | ||
|
|
693adbc727 | ||
|
|
8d9840be8d | ||
|
|
73c0c61b6d | ||
|
|
a813269d1f | ||
|
|
d6bc1fa977 | ||
|
|
249edfa2c4 | ||
|
|
0c28151d30 | ||
|
|
5bb3b7e35e | ||
|
|
4119d04fba | ||
|
|
84858bc048 | ||
|
|
e8c091741b | ||
|
|
ebdfd21b0f | ||
|
|
ed0b3cab7f |
@@ -173,7 +173,7 @@ Datetime String in ISO 8601 format ``"2017-12-27T10:00:00Z"``
|
||||
with timezone (normally UTC) ``"2017-12-27T10:00:00.596934Z"``,
|
||||
``"2017-12-27T10:00:00+02:00"``
|
||||
Date String in ISO 8601 format ``2017-12-27``
|
||||
Multi-lingual string Object of strings ``{"en": "red", "de": "rot", "de_Informal": "rot"}``
|
||||
Multi-lingual string Object of strings ``{"en": "red", "de": "rot", "de-informal": "rot"}``
|
||||
Money String with decimal number ``"23.42"``
|
||||
Currency String with ISO 4217 code ``"EUR"``, ``"USD"``
|
||||
Relative datetime *either* String in ISO 8601 ``"2017-12-27T10:00:00.596934Z"``,
|
||||
|
||||
@@ -244,6 +244,59 @@ Endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(exhibitor_id)/leads/(id)
|
||||
|
||||
Returns information on one lead, identified by its exhibitor and lead ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/leads/1 HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"attendee_order": "T0E7E",
|
||||
"attendee_positionid": 1,
|
||||
"rating": 1,
|
||||
"notes": "",
|
||||
"tags": [],
|
||||
"first_upload": "2021-07-06T11:03:31.414491+01:00",
|
||||
"device_name": "DEV1",
|
||||
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
|
||||
"data": [
|
||||
{
|
||||
"id": "attendee_name",
|
||||
"label": "Attendee name",
|
||||
"value": "Peter Miller",
|
||||
"details": [
|
||||
{"label": "Given name", "value": "Peter"},
|
||||
{"label": "Family name", "value": "Miller"},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param exhibitor_id: The ``id`` field of the exhibitor to fetch
|
||||
:param exhibitor_id: The ``id`` field of the lead to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event or exhibitor or lead does not exist **or** you have no permission to view it.
|
||||
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/
|
||||
|
||||
Returns a list of all vouchers connected to an exhibitor. The response contains the same data as described in
|
||||
@@ -1027,4 +1080,4 @@ redemptions list of objects A list of objec
|
||||
:statuscode 200: No error
|
||||
:statuscode 401: Invalid authentication code
|
||||
:statuscode 403: Not permitted to access bulk data
|
||||
:statuscode 404: Voucher not found in system
|
||||
:statuscode 404: Voucher not found in system
|
||||
|
||||
@@ -11,7 +11,8 @@ Core
|
||||
----
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter,
|
||||
register_notification_types, event_notification_sent, organizer_notification_sent,
|
||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators, gift_card_transaction_display,
|
||||
register_text_placeholders, register_mail_placeholders, device_info_updated,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
sphinx==9.1.*
|
||||
sphinx-rtd-theme~=3.1.0
|
||||
sphinxcontrib-httpdomain~=2.0.0
|
||||
sphinxcontrib-httpdomain~=2.1.0
|
||||
sphinxcontrib-images~=1.0.1
|
||||
sphinxcontrib-jquery~=4.1
|
||||
sphinxcontrib-spelling~=8.0.2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-e ../
|
||||
sphinx==9.1.*
|
||||
sphinx-rtd-theme~=3.1.0
|
||||
sphinxcontrib-httpdomain~=2.0.0
|
||||
sphinxcontrib-httpdomain~=2.1.0
|
||||
sphinxcontrib-images~=1.0.1
|
||||
sphinxcontrib-jquery~=4.1
|
||||
sphinxcontrib-spelling~=8.0.2
|
||||
|
||||
Generated
+92
-104
@@ -9,8 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.30",
|
||||
"vue-slicksort": "^2.0.5"
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -41,30 +40,30 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
||||
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
||||
"version": "7.29.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
|
||||
"integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.8"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -74,13 +73,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -379,9 +378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
@@ -1448,53 +1447,53 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
|
||||
"integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.43.tgz",
|
||||
"integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@vue/shared": "3.5.30",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@vue/shared": "3.5.43",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
|
||||
"integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz",
|
||||
"integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
"@vue/compiler-core": "3.5.43",
|
||||
"@vue/shared": "3.5.43"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
|
||||
"integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz",
|
||||
"integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@vue/compiler-core": "3.5.30",
|
||||
"@vue/compiler-dom": "3.5.30",
|
||||
"@vue/compiler-ssr": "3.5.30",
|
||||
"@vue/shared": "3.5.30",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@vue/compiler-core": "3.5.43",
|
||||
"@vue/compiler-dom": "3.5.43",
|
||||
"@vue/compiler-ssr": "3.5.43",
|
||||
"@vue/shared": "3.5.43",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss": "^8.5.28",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
|
||||
"integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz",
|
||||
"integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
"@vue/compiler-dom": "3.5.43",
|
||||
"@vue/shared": "3.5.43"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/eslint-config-typescript": {
|
||||
@@ -1538,53 +1537,51 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
|
||||
"integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.43.tgz",
|
||||
"integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.30"
|
||||
"@vue/shared": "3.5.43"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
|
||||
"integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.43.tgz",
|
||||
"integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
"@vue/reactivity": "3.5.43",
|
||||
"@vue/shared": "3.5.43"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
|
||||
"integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz",
|
||||
"integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.30",
|
||||
"@vue/runtime-core": "3.5.30",
|
||||
"@vue/shared": "3.5.30",
|
||||
"@vue/reactivity": "3.5.43",
|
||||
"@vue/runtime-core": "3.5.43",
|
||||
"@vue/shared": "3.5.43",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
|
||||
"integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.43.tgz",
|
||||
"integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.30"
|
||||
"@vue/compiler-ssr": "3.5.43",
|
||||
"@vue/runtime-dom": "3.5.43",
|
||||
"@vue/shared": "3.5.43"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
|
||||
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.43.tgz",
|
||||
"integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
@@ -3191,9 +3188,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"version": "3.3.20",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
|
||||
"integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3367,9 +3364,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
||||
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
|
||||
"version": "8.5.29",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
|
||||
"integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3386,18 +3383,18 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.16",
|
||||
"nanoid": "^3.3.19",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
"source-map-js": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz",
|
||||
"integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==",
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz",
|
||||
"integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4163,9 +4160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz",
|
||||
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz",
|
||||
"integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -4186,9 +4183,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -4593,16 +4590,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
||||
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
||||
"version": "3.5.43",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.43.tgz",
|
||||
"integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.30",
|
||||
"@vue/compiler-sfc": "3.5.30",
|
||||
"@vue/runtime-dom": "3.5.30",
|
||||
"@vue/server-renderer": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
"@vue/compiler-dom": "3.5.43",
|
||||
"@vue/compiler-sfc": "3.5.43",
|
||||
"@vue/runtime-dom": "3.5.43",
|
||||
"@vue/server-renderer": "3.5.43",
|
||||
"@vue/shared": "3.5.43"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -4650,15 +4647,6 @@
|
||||
"vue-eslint-parser": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-slicksort": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-slicksort/-/vue-slicksort-2.0.5.tgz",
|
||||
"integrity": "sha512-fXz1YrNjhUbJK7o0tMk27mIr4pMAZYLSYvtmLazCtfpvz+zafPCn34ILDL8B7hT7WLVZKreYs6JVe5VWymqmzA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
+1
-2
@@ -27,8 +27,7 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.30",
|
||||
"vue-slicksort": "^2.0.5"
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
+8
-8
@@ -33,15 +33,15 @@ dependencies = [
|
||||
"bleach==6.4.*",
|
||||
"celery==5.6.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=50.0.1",
|
||||
"css-inline==0.21.*",
|
||||
"cryptography>=50.0.2",
|
||||
"css-inline==0.22.*",
|
||||
"defusedcsv>=3.0.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==5.2.*,>=5.2.17",
|
||||
"django-bootstrap3==26.2",
|
||||
"django-compressor==4.6.0",
|
||||
"django-countries==9.1.*",
|
||||
"django-filter==26.1",
|
||||
"django-filter==26.2",
|
||||
"django-formset-js-improved==0.5.0.5",
|
||||
"django-formtools==2.7",
|
||||
"django-hierarkey==2.0.*,>=2.0.2",
|
||||
@@ -83,7 +83,7 @@ dependencies = [
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==3.0",
|
||||
"pycryptodome==3.23.*",
|
||||
"pycryptodome==3.24.*",
|
||||
"pypdf==6.19.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
@@ -94,11 +94,11 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==5.0.*",
|
||||
"requests==2.34.*",
|
||||
"sentry-sdk==2.70.*",
|
||||
"sentry-sdk==2.71.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2026072401",
|
||||
"tlds>=2026093003",
|
||||
"tqdm==4.*",
|
||||
"ua-parser==1.0.*",
|
||||
"vobject==0.9.*",
|
||||
@@ -112,7 +112,7 @@ dev = [
|
||||
"aiohttp==3.14.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.38.*",
|
||||
"fakeredis==2.39.*",
|
||||
"flake8==7.4.*",
|
||||
"freezegun",
|
||||
"isort==9.0.*",
|
||||
@@ -122,7 +122,7 @@ dev = [
|
||||
"pytest-cache",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.*",
|
||||
"pytest-mock==3.15.*",
|
||||
"pytest-mock==3.16.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.8.*",
|
||||
"pytest-playwright",
|
||||
|
||||
@@ -100,10 +100,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:compatquestion-list'),
|
||||
('GET', 'api-v1:datafield-list'),
|
||||
('GET', 'api-v1:questionnaire-list'),
|
||||
('GET', 'api-v1:questionnairechild-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
@@ -145,10 +141,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:compatquestion-list'),
|
||||
('GET', 'api-v1:datafield-list'),
|
||||
('GET', 'api-v1:questionnaire-list'),
|
||||
('GET', 'api-v1:questionnairechild-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
@@ -185,10 +177,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:compatquestion-list'),
|
||||
('GET', 'api-v1:datafield-list'),
|
||||
('GET', 'api-v1:questionnaire-list'),
|
||||
('GET', 'api-v1:questionnairechild-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
from functools import reduce
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class CompatException(APIException):
|
||||
status = 410
|
||||
default_code = 'incompatible_data'
|
||||
|
||||
|
||||
class MergeField(serializers.Field):
|
||||
"""
|
||||
Read-only field that iterates over a list of related objects, picks a value from each, and merges the values
|
||||
in a configurable way (mode parameter).
|
||||
|
||||
:param mode: list: returns a list of all picked values
|
||||
consensus: returns the value only if it is equal on all related objects, otherwise
|
||||
raises or returns an error object
|
||||
union: only supported if picked values are iterables - generates union of all sub-items
|
||||
bool_or: returns a boolean that is true if at least one picked value is truthy
|
||||
"""
|
||||
def __init__(self, related_name, inner_field, mode, **kwargs):
|
||||
self.inner_field = inner_field
|
||||
self.related_name = related_name
|
||||
self.merge_mode = mode
|
||||
assert mode in ('list', 'consensus', 'union', 'bool_or',)
|
||||
kwargs['source'] = '*'
|
||||
kwargs['read_only'] = True
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
self.field_name = field_name
|
||||
self.parent = parent
|
||||
self.inner_field.bind(field_name, parent)
|
||||
|
||||
def get_attribute(self, instance):
|
||||
vals = []
|
||||
for ref in getattr(instance, self.related_name).all():
|
||||
val = self.inner_field.get_attribute(ref)
|
||||
if val is not None:
|
||||
val = self.inner_field.to_representation(val)
|
||||
vals.append(val)
|
||||
if self.merge_mode == 'list':
|
||||
return vals
|
||||
elif self.merge_mode == 'consensus':
|
||||
if len(vals) >= 1 and all(val == vals[0] for val in vals):
|
||||
return vals[0]
|
||||
else:
|
||||
errmes = 'Questionnaire configuration incompatible with old data model due to conflicting values of ' + self.field_name
|
||||
if self.context['ignore_errors']:
|
||||
return {'_error': errmes, '_values': vals}
|
||||
else:
|
||||
raise CompatException({'detail': errmes, '_values': vals})
|
||||
elif self.merge_mode == 'union':
|
||||
return list(set(sum(vals, [])))
|
||||
elif self.merge_mode == 'bool_or':
|
||||
return reduce(lambda a, b: a or bool(b), vals, False)
|
||||
else:
|
||||
raise Exception('Unknown merge mode: ' + self.merge_mode)
|
||||
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
@@ -32,7 +32,6 @@ from rest_framework.exceptions import ValidationError
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
MixedAnswerListSerializer,
|
||||
)
|
||||
from pretix.base.models import SalesChannel, Seat, Voucher
|
||||
from pretix.base.models.orders import CartPosition
|
||||
@@ -44,7 +43,7 @@ class TaxIncludedField(serializers.Field):
|
||||
|
||||
|
||||
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
answers = MixedAnswerListSerializer(child=AnswerSerializer(), source='*')
|
||||
answers = AnswerSerializer(many=True)
|
||||
seat = InlineSeatSerializer()
|
||||
includes_tax = TaxIncludedField(source='*')
|
||||
|
||||
@@ -56,7 +55,7 @@ class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class BaseCartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = MixedAnswerListSerializer(child=AnswerCreateSerializer(required=False), source='*', required=False)
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
includes_tax = serializers.BooleanField(required=False, allow_null=True)
|
||||
|
||||
|
||||
@@ -764,6 +764,14 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'event_calendar_future_only',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'attendee_data_explanation_text',
|
||||
'confirm_texts',
|
||||
'order_email_asked_twice',
|
||||
@@ -933,6 +941,14 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'show_quota_left',
|
||||
'show_dates_on_frontpage',
|
||||
'max_items_per_order',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'ticket_download',
|
||||
'ticket_download_addons',
|
||||
'ticket_download_nonadm',
|
||||
@@ -963,6 +979,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'reusable_media_type_nfc_mf0aes',
|
||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||
'reusable_media_usage_enforced',
|
||||
'system_question_order',
|
||||
'tax_rule_payment',
|
||||
'tax_rule_cancellation',
|
||||
]
|
||||
@@ -987,18 +1004,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
)
|
||||
)
|
||||
|
||||
# Provide fixed values for old configuration fields to ensure apps never use their built-in FakeQuestions, only the
|
||||
# ones we provide from POSCompatQuestionViewSet
|
||||
self.fields['system_question_order'] = serializers.JSONField(read_only=True, default=dict(), source='_invalid')
|
||||
self.fields['attendee_names_asked'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
self.fields['attendee_names_required'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
self.fields['attendee_emails_asked'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
self.fields['attendee_emails_required'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
self.fields['attendee_addresses_asked'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
self.fields['attendee_addresses_required'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
self.fields['attendee_company_asked'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
self.fields['attendee_company_required'] = serializers.BooleanField(read_only=True, default=False, source='_invalid')
|
||||
|
||||
|
||||
class MultiLineStringField(serializers.Field):
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import logging
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
@@ -34,41 +33,26 @@ import logging
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import os.path
|
||||
from decimal import Decimal
|
||||
from itertools import zip_longest
|
||||
|
||||
import rest_framework
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.rest_framework import I18nField
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DrfValidationError
|
||||
from rest_framework.serializers import as_serializer_error
|
||||
|
||||
from pretix.api.compat import MergeField
|
||||
from pretix.api.serializers import (
|
||||
CompatDecimalField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.forms.questions import REQUIRED_NAME_PARTS
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemProgramTime,
|
||||
ItemVariation, ItemVariationMetaValue, Question, QuestionAnswer,
|
||||
QuestionOption, Quota, SalesChannel,
|
||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
SalesChannel,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire, QuestionnaireChild
|
||||
from pretix.base.settings import (
|
||||
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.helpers.i18n import i18n_all_from_gettext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
@@ -277,7 +261,9 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
ItemAddOn.clean_max_min_count(data.get('max_count'), data.get('min_count'))
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
ItemAddOn.clean_max_min_count(full_data.get('max_count'), full_data.get('min_count'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -555,118 +541,18 @@ class LegacyDependencyValueField(serializers.CharField):
|
||||
return [data] if data else []
|
||||
|
||||
|
||||
class POSCompatQuestionSerializer(I18nAwareModelSerializer):
|
||||
id = serializers.IntegerField(source='cooked_id', read_only=True)
|
||||
question = I18nField(source='label', allow_null=True)
|
||||
type = serializers.CharField(source='user_datafield.type', allow_null=True, default=Question.FieldType.STRING)
|
||||
identifier = serializers.SerializerMethodField(read_only=True)
|
||||
show_during_checkin = serializers.BooleanField(source='user_datafield.show_during_checkin', allow_null=True, default=False)
|
||||
ask_during_checkin = serializers.SerializerMethodField(read_only=True)
|
||||
hidden = serializers.SerializerMethodField(read_only=True)
|
||||
print_on_invoice = serializers.BooleanField(source='user_datafield.print_on_invoice', allow_null=True, default=False)
|
||||
valid_number_min = serializers.IntegerField(source='user_datafield.valid_number_min', allow_null=True)
|
||||
valid_number_max = serializers.IntegerField(source='user_datafield.valid_number_max', allow_null=True)
|
||||
valid_date_min = serializers.DateField(source='user_datafield.valid_date_min', allow_null=True)
|
||||
valid_date_max = serializers.DateField(source='user_datafield.valid_date_max', allow_null=True)
|
||||
valid_datetime_min = serializers.DateTimeField(source='user_datafield.valid_datetime_min', allow_null=True)
|
||||
valid_datetime_max = serializers.DateTimeField(source='user_datafield.valid_datetime_max', allow_null=True)
|
||||
valid_string_length_max = serializers.IntegerField(source='user_datafield.valid_string_length_max', allow_null=True)
|
||||
valid_string_length_min = serializers.IntegerField(source='user_datafield.valid_string_length_min', allow_null=True)
|
||||
valid_file_portrait = serializers.BooleanField(source='user_datafield.valid_file_portrait', allow_null=True)
|
||||
dependency_question = serializers.SerializerMethodField('cook_dependency_question_id', read_only=True)
|
||||
dependency_value = serializers.SerializerMethodField('empty', read_only=True)
|
||||
position = serializers.SerializerMethodField(read_only=True)
|
||||
items = serializers.PrimaryKeyRelatedField(source='questionnaire.items', many=True, queryset=Item.objects.none(), allow_null=True, required=False)
|
||||
questionnaire_id = serializers.IntegerField(source='questionnaire.id', read_only=True)
|
||||
|
||||
options = InlineQuestionOptionSerializer(source='user_datafield.options', many=True, required=False, default=[])
|
||||
class QuestionSerializer(I18nAwareModelSerializer):
|
||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait', 'questionnaire_id')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['items'].child_relation.queryset = self.context['event'].items.all() if 'event' in self.context else Item.objects.none()
|
||||
|
||||
def get_attribute(self, instance):
|
||||
if isinstance(instance, QuestionAnswer):
|
||||
position = instance.cartposition if instance.cartposition_id else instance.orderposition
|
||||
print('searching ', repr(instance), repr(position), repr(instance.question), instance.pk, position.item_id, self.context) # TODO remove print
|
||||
qcs = list(QuestionnaireChild.objects.filter(user_datafield_id=instance.question_id, questionnaire__items__in=[position.item_id]))
|
||||
return next((qc for qc in qcs if qc.questionnaire.type == self.context.get('preferred_questionnaire_type', Questionnaire.QuestionnaireType.ORDER_POSITION_SALE)), qcs[0])
|
||||
|
||||
return super().get_attribute(instance)
|
||||
|
||||
def cook_dependency_question_id(self, qc):
|
||||
if qc.dependency_question:
|
||||
return qc.dependency_question.cooked_id
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_ask_during_checkin(self, qc):
|
||||
return qc.questionnaire.type == Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
|
||||
def get_hidden(self, qc):
|
||||
return qc.questionnaire.type == Questionnaire.QuestionnaireType.ORDER_POSITION_HIDDEN
|
||||
|
||||
def get_identifier(self, qc):
|
||||
return qc.user_datafield.identifier if qc.user_datafield else f'@{qc.system_datafield}'
|
||||
|
||||
def empty(self, qc):
|
||||
return None
|
||||
|
||||
def get_position(self, qc):
|
||||
return qc.questionnaire.position * 1000000000 + qc.position
|
||||
|
||||
@staticmethod
|
||||
def mangle_results(event, results):
|
||||
mangled_results = []
|
||||
pos_offset = 0
|
||||
for row in results:
|
||||
row['position'] += pos_offset
|
||||
if row['identifier'] == '@attendee_name_parts':
|
||||
settings = event.settings
|
||||
for k, v, w, *__ in PERSON_NAME_SCHEMES.get(settings.name_scheme)['fields']:
|
||||
part = dict(row)
|
||||
part["identifier"] += ":" + k
|
||||
part["question"] = i18n_all_from_gettext(v, settings.locales)
|
||||
if k == "title" and settings.name_scheme_titles:
|
||||
part["type"] = Question.FieldType.CHOICE
|
||||
part["options"] = [{"id": idx + 1, "identifier": k, "answer": i18n_all_from_gettext(k, settings.locales), "position": idx + 1} for idx, k in
|
||||
enumerate(PERSON_NAME_TITLE_GROUPS.get(settings.name_scheme_titles)[1])]
|
||||
elif k == "salutation":
|
||||
part["type"] = Question.FieldType.CHOICE
|
||||
part["options"] = [{"id": idx + 1, "identifier": k, "answer": i18n_all_from_gettext(v, settings.locales), "position": idx + 1} for idx, (k, v) in
|
||||
enumerate(PERSON_NAME_SALUTATIONS)]
|
||||
part["id"] = QuestionnaireChild.cook_id_from_parts(row['questionnaire_id'], None, part["identifier"][1:])
|
||||
part["required"] = part["required"] and k in REQUIRED_NAME_PARTS
|
||||
mangled_results.append(part)
|
||||
pos_offset += 1
|
||||
row["position"] += 1
|
||||
else:
|
||||
if row['identifier'] == '@country':
|
||||
row['type'] = Question.FieldType.COUNTRYCODE
|
||||
mangled_results.append(row)
|
||||
return mangled_results
|
||||
|
||||
|
||||
class DatafieldSerializer(I18nAwareModelSerializer):
|
||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
internal_name = serializers.CharField(allow_null=True, source='question', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'type', 'options',
|
||||
'show_during_checkin', 'identifier',
|
||||
'hidden', 'print_on_invoice', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait', 'internal_name',)
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
@@ -677,6 +563,14 @@ class DatafieldSerializer(I18nAwareModelSerializer):
|
||||
self.instance.clean_type_change(self.instance.type, value)
|
||||
return value
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if self.instance and 'options' in data:
|
||||
@@ -688,6 +582,21 @@ class DatafieldSerializer(I18nAwareModelSerializer):
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
if dep.ask_during_checkin:
|
||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
||||
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
@@ -712,213 +621,13 @@ class DatafieldSerializer(I18nAwareModelSerializer):
|
||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||
items = validated_data.pop('items', [])
|
||||
|
||||
question = Question.objects.create(**validated_data, container_type=self.context['container_type'])
|
||||
question = Question.objects.create(**validated_data, container_type=Question.ContainerType.ORDERPOSITION)
|
||||
question.items.set(items)
|
||||
for opt_data in options_data:
|
||||
QuestionOption.objects.create(question=question, **opt_data)
|
||||
return question
|
||||
|
||||
|
||||
class CompatDatafieldSerializer(DatafieldSerializer):
|
||||
question = MergeField('referenced_by', I18nField(source='label'), mode='consensus')
|
||||
required = MergeField('referenced_by', serializers.BooleanField(), mode='consensus')
|
||||
items = MergeField('referenced_by', serializers.PrimaryKeyRelatedField(source='questionnaire.items', many=True, queryset=Item.objects.none(), allow_null=True, required=False), mode='union')
|
||||
position = serializers.IntegerField(required=False, read_only=True, default=0)
|
||||
ask_during_checkin = MergeField('referenced_by', serializers.SerializerMethodField(), mode='bool_or')
|
||||
|
||||
dependency_question = MergeField('referenced_by', serializers.IntegerField(source='dependency_question.user_datafield_id', required=False, default=None, allow_null=True), mode='consensus')
|
||||
dependency_values = MergeField('referenced_by', serializers.JSONField(), mode='consensus')
|
||||
dependency_value = MergeField('referenced_by', LegacyDependencyValueField(source='dependency_values'), mode='consensus')
|
||||
help_text = MergeField('referenced_by', I18nField(), mode='consensus')
|
||||
|
||||
referenced_by_questionnaires = MergeField('referenced_by', serializers.IntegerField(source='questionnaire.id'), mode='list')
|
||||
questionnaire_types = MergeField('referenced_by', serializers.CharField(source='questionnaire.type'), mode='list')
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = (
|
||||
'id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait',
|
||||
# new
|
||||
'internal_name', 'referenced_by_questionnaires', 'questionnaire_types',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.fields['items'].inner_field.child_relation.queryset = kwargs['context']['event'].items.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_ask_during_checkin(self, qc):
|
||||
return qc.questionnaire.type == Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
|
||||
|
||||
class DatafieldRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
default_error_messages = {
|
||||
'invalid_choice': _('"{input}" is not a valid choice.')
|
||||
}
|
||||
def to_representation(self, qc):
|
||||
if not qc:
|
||||
return None
|
||||
elif qc.system_datafield:
|
||||
return qc.system_datafield
|
||||
elif qc.user_datafield_id:
|
||||
return qc.user_datafield_id
|
||||
else:
|
||||
return None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) == int:
|
||||
return {'user_datafield': super().to_internal_value(data), 'system_datafield': None}
|
||||
elif type(data) == str:
|
||||
if data not in QuestionnaireChild.SystemQuestion.values:
|
||||
self.fail("invalid_choice", input=data)
|
||||
return {'user_datafield': None, 'system_datafield': data}
|
||||
elif data is None:
|
||||
return {'user_datafield': None, 'system_datafield': None}
|
||||
else:
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return self.source == '*'
|
||||
|
||||
|
||||
class RenderedMarkdownField(serializers.CharField):
|
||||
def to_representation(self, value):
|
||||
return rich_text(value)
|
||||
|
||||
|
||||
class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
||||
datafield = DatafieldRelatedField(allow_null=True, source='*', queryset=Question.objects.none())
|
||||
dependency_question = DatafieldRelatedField(allow_null=True, required=False, queryset=Question.objects.none())
|
||||
rendered_help_text = RenderedMarkdownField(read_only=True, source='help_text')
|
||||
cooked_id = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ('datafield', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values', 'rendered_help_text', 'cooked_id')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["datafield"].queryset = self.context["event"].questions.all()
|
||||
self.fields["dependency_question"].queryset = self.context["event"].questions.all()
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value['user_datafield']:
|
||||
if value['user_datafield'].type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
return value
|
||||
|
||||
|
||||
class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
items = serializers.PrimaryKeyRelatedField(many=True, queryset=Item.objects.none(), allow_null=True, required=False)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Questionnaire
|
||||
fields = ('id', 'type', 'internal_name', 'items', 'position', 'all_sales_channels', 'limit_sales_channels', 'children')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.fields['children'] = InlineQuestionnaireChildSerializer(many=True, required=True, context=kwargs['context'], partial=False)
|
||||
self.fields['items'].child_relation.queryset = kwargs['context']['event'].items.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = kwargs['context']['event'].organizer.sales_channels.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
try:
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
except rest_framework.exceptions.ValidationError as e:
|
||||
# we already have invalid state in the database, what should we do? hope it gets better after saving?
|
||||
logger.exception("Invalid state in database, ignoring some validations!")
|
||||
else:
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
# raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
#if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
#Question.clean_items(event, full_data.get('items') or [])
|
||||
|
||||
if (full_data.get('type') == Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
and any(c['dependency_question'] for c in full_data['children'])):
|
||||
raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
if (not full_data.get('type').startswith('P')
|
||||
and any(c['system_datafield'] for c in full_data['children'])):
|
||||
raise ValidationError('System data fields are only supported on order positions.')
|
||||
|
||||
system_fields = set(c['system_datafield'] for c in full_data['children'])
|
||||
if ('zipcode' in system_fields or 'state' in system_fields or 'street' in system_fields or 'city' in system_fields) and 'country' not in system_fields:
|
||||
raise ValidationError('The system data fields "street", "zip code", "city", and "state" can only be used in combination with the "country" field.')
|
||||
|
||||
return data
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
children_data = validated_data.pop('children') if 'children' in validated_data else []
|
||||
questionnaire = super().create(validated_data)
|
||||
self.set_children(questionnaire, children_data)
|
||||
try:
|
||||
Questionnaire.check_constraints(questionnaire.event)
|
||||
except ValidationError as e:
|
||||
raise DrfValidationError(as_serializer_error(e))
|
||||
return questionnaire
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
children_data = validated_data.pop('children', None)
|
||||
questionnaire = super().update(instance, validated_data)
|
||||
if children_data is not None:
|
||||
self.set_children(questionnaire, children_data)
|
||||
try:
|
||||
Questionnaire.check_constraints(questionnaire.event)
|
||||
except ValidationError as e:
|
||||
raise DrfValidationError(as_serializer_error(e))
|
||||
return questionnaire
|
||||
|
||||
def set_children(self, questionnaire, new_data):
|
||||
result = {}
|
||||
child_serializer = self.fields['children'].child
|
||||
existing_children = questionnaire.children.all()
|
||||
def q(qc):
|
||||
return qc['user_datafield'] or qc['system_datafield']
|
||||
for i, d in enumerate(new_data):
|
||||
d['questionnaire'] = questionnaire
|
||||
d['position'] = i + 1
|
||||
d.setdefault('required', False)
|
||||
d.setdefault('help_text', None)
|
||||
d.setdefault('dependency_question', None)
|
||||
d.setdefault('dependency_values', None)
|
||||
for existing, update_data in zip_longest(existing_children, new_data):
|
||||
if update_data:
|
||||
if update_data.get('dependency_question'):
|
||||
try:
|
||||
update_data['dependency_question'] = result[q(update_data['dependency_question'])]
|
||||
except KeyError:
|
||||
raise ValidationError('A question can only depend on a previous question from the same questionnaire.')
|
||||
if existing:
|
||||
result[q(update_data)] = child_serializer.update(existing, update_data)
|
||||
else:
|
||||
result[q(update_data)] = child_serializer.create(update_data)
|
||||
else:
|
||||
existing.delete()
|
||||
return result
|
||||
|
||||
|
||||
class QuotaSerializer(I18nAwareModelSerializer):
|
||||
available = serializers.BooleanField(read_only=True)
|
||||
available_number = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -40,7 +40,6 @@ from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.serializers import ListSerializer
|
||||
|
||||
from pretix.api.auth.utils import get_session_key_for_api_request
|
||||
from pretix.api.serializers import CompatDecimalField, CompatibleJSONField
|
||||
@@ -48,7 +47,7 @@ from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, POSCompatQuestionSerializer,
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -58,8 +57,8 @@ from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||
QuestionAnswer, QuestionnaireChild, ReusableMedium, SalesChannel, Seat,
|
||||
SubEvent, TaxRule, Voucher,
|
||||
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
@@ -75,8 +74,7 @@ from pretix.base.services.pricing import (
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, ROUNDING_MODES,
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, ROUNDING_MODES,
|
||||
)
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
@@ -231,95 +229,6 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'name', 'seat_guid', 'zone_name', 'row_name', 'row_label', 'seat_label', 'seat_number')
|
||||
|
||||
|
||||
class MixedAnswerListSerializer(ListSerializer):
|
||||
def __init__(self, instance=None, *args, **kwargs):
|
||||
self.parent_instance = instance
|
||||
super().__init__(instance.answers if instance else None, *args, **kwargs)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
return dictionary
|
||||
|
||||
def to_representation(self, instance):
|
||||
answers = super().to_representation(instance.answers)
|
||||
answers_dict = {answer["question"]: answer for answer in answers}
|
||||
for qc in QuestionnaireChild.objects.filter(questionnaire__items__id=instance.item_id):
|
||||
if qc.user_datafield_id:
|
||||
if answer := answers_dict.get(qc.user_datafield_id):
|
||||
answer = dict(answer)
|
||||
answer["question"] = qc.cooked_id
|
||||
answer["questionnaire_id"] = qc.questionnaire_id
|
||||
answer["user_datafield_id"] = qc.user_datafield_id
|
||||
answers.append(answer)
|
||||
elif (sys_answer := instance.get_system_answer(qc.system_datafield)) is not None:
|
||||
if qc.system_datafield == 'attendee_name_parts':
|
||||
name_parts = instance.attendee_name_parts
|
||||
settings = self.context['event'].settings
|
||||
for k, v, w, *__ in PERSON_NAME_SCHEMES.get(settings.name_scheme)['fields']:
|
||||
if k in name_parts:
|
||||
options = []
|
||||
options_ident = []
|
||||
try:
|
||||
if k == "salutation":
|
||||
salutation_keys = [key for key, value in PERSON_NAME_SALUTATIONS]
|
||||
options = [salutation_keys.index(name_parts[k]) + 1]
|
||||
options_ident = [name_parts[k]]
|
||||
elif k == "title" and settings.name_scheme_titles:
|
||||
options = [PERSON_NAME_TITLE_GROUPS.get(settings.name_scheme_titles)[1].index(name_parts[k]) + 1]
|
||||
options_ident = [name_parts[k]]
|
||||
except ValueError:
|
||||
pass
|
||||
answers.append({
|
||||
"question": QuestionnaireChild.cook_id_from_parts(qc.questionnaire_id, None, f'{qc.system_datafield}:{k}'),
|
||||
"answer": name_parts[k],
|
||||
"question_identifier": f'@{qc.system_datafield}:{k}',
|
||||
"options": options,
|
||||
"option_identifiers": options_ident,
|
||||
"questionnaire_id": qc.questionnaire_id,
|
||||
"system_datafield": f'{qc.system_datafield}:{k}',
|
||||
})
|
||||
answers.append({
|
||||
"question": qc.cooked_id,
|
||||
"answer": sys_answer,
|
||||
"question_identifier": f'@{qc.system_datafield}',
|
||||
"options": [],
|
||||
"option_identifiers": [],
|
||||
"questionnaire_id": qc.questionnaire_id,
|
||||
"system_datafield": qc.system_datafield,
|
||||
})
|
||||
return answers
|
||||
|
||||
def to_internal_value(self, data):
|
||||
result = {}
|
||||
answers = []
|
||||
attendee_name_parts = {}
|
||||
settings = self.context['event'].settings
|
||||
for answer in data["answers"]:
|
||||
try:
|
||||
qid, dfid, sys_df = QuestionnaireChild.uncook_id(answer["question"])
|
||||
except ValueError:
|
||||
if type(answer["question"]) is str and answer["question"].lstrip("@") in QuestionnaireChild.SYSTEM_DATAFIELD_NUMBERS:
|
||||
qid, dfid, sys_df = None, None, answer["question"].lstrip("@")
|
||||
else:
|
||||
raise ValidationError('Invalid question ID')
|
||||
if dfid:
|
||||
answer["question"] = dfid
|
||||
answers.append(answer)
|
||||
elif sys_df == "country":
|
||||
result[sys_df] = Country(answer["answer"])
|
||||
elif sys_df == "attendee_name_parts:salutation":
|
||||
attendee_name_parts['salutation'] = PERSON_NAME_SALUTATIONS[answer["options"][0] - 1][0]
|
||||
elif sys_df == "attendee_name_parts:title" and settings.name_scheme_titles:
|
||||
attendee_name_parts['title'] = PERSON_NAME_TITLE_GROUPS.get(settings.name_scheme_titles)[1][answer["options"][0] - 1]
|
||||
elif sys_df.startswith("attendee_name_parts:"):
|
||||
attendee_name_parts[sys_df[len("attendee_name_parts:"):]] = answer["answer"]
|
||||
else:
|
||||
result[sys_df] = answer["answer"]
|
||||
result["answers"] = super().to_internal_value(answers)
|
||||
if attendee_name_parts:
|
||||
result["attendee_name_parts"] = attendee_name_parts
|
||||
return result
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||
@@ -682,7 +591,7 @@ class OrderPositionPluginDataField(serializers.Field):
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
checkins = InlineCheckinSerializer(many=True, read_only=True)
|
||||
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||
answers = MixedAnswerListSerializer(child=AnswerSerializer(), source='*')
|
||||
answers = AnswerSerializer(many=True)
|
||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*', read_only=True)
|
||||
@@ -783,7 +692,6 @@ class AttendeeNamePartsField(serializers.Field):
|
||||
|
||||
class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
require_attention = RequireAttentionField(source='*')
|
||||
answers = AnswerSerializer(many=True)
|
||||
attendee_name = AttendeeNameField(source='*')
|
||||
attendee_name_parts = AttendeeNamePartsField(source='*')
|
||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
||||
@@ -813,7 +721,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = POSCompatQuestionSerializer(read_only=True) # TODO(questionnaires)
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
|
||||
if 'addons' in self.context['expand']:
|
||||
# Experimental feature, undocumented on purpose for now in case we need to remove it again
|
||||
@@ -1133,7 +1041,7 @@ class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
answers = MixedAnswerListSerializer(child=AnswerCreateSerializer(required=False), source='*', required=False)
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
@@ -30,8 +30,7 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField,
|
||||
MixedAnswerListSerializer, OrderFeeCreateSerializer,
|
||||
OrderPositionCreateSerializer,
|
||||
OrderFeeCreateSerializer, OrderPositionCreateSerializer,
|
||||
)
|
||||
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
||||
from pretix.base.services.orders import OrderChangeManager, OrderError
|
||||
@@ -42,7 +41,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', queryset=Order.objects.none(), required=True, allow_null=False)
|
||||
answers = MixedAnswerListSerializer(child=AnswerCreateSerializer(required=False), source='*', required=False)
|
||||
answers = AnswerCreateSerializer(many=True, required=False)
|
||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||
secret = serializers.CharField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
@@ -155,7 +154,7 @@ class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
|
||||
|
||||
|
||||
class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
|
||||
answers = MixedAnswerListSerializer(child=AnswerSerializer(), source='*')
|
||||
answers = AnswerSerializer(many=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.templatetags.static import static
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from i18nfield.rest_framework import I18nField
|
||||
@@ -220,12 +219,10 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
|
||||
class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||
type = serializers.CharField(default="api")
|
||||
icon_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ('identifier', 'type', 'label', 'position', 'icon', 'icon_url')
|
||||
read_only_fields = ('icon',)
|
||||
fields = ('identifier', 'type', 'label', 'position')
|
||||
|
||||
def validate_type(self, value):
|
||||
if (not self.instance or not self.instance.pk) and value != "api":
|
||||
@@ -249,10 +246,6 @@ class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
return value
|
||||
|
||||
def get_icon_url(self, instance):
|
||||
if '.' in instance.icon:
|
||||
return static(instance.icon)
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||
|
||||
@@ -80,10 +80,7 @@ event_router.register(r'subevents', event.SubEventViewSet)
|
||||
event_router.register(r'clone', event.CloneEventViewSet)
|
||||
event_router.register(r'items', item.ItemViewSet)
|
||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||
event_router.register(r'questions', item.CompatDatafieldViewSet, basename='compatquestion')
|
||||
event_router.register(r'datafields', item.DatafieldViewSet, basename='datafield')
|
||||
event_router.register(r'orderdatafields', item.OrderDatafieldViewSet, basename='orderdatafield')
|
||||
event_router.register(r'questionnaires', item.QuestionnaireViewSet)
|
||||
event_router.register(r'questions', item.QuestionViewSet)
|
||||
event_router.register(r'discounts', discount.DiscountViewSet)
|
||||
event_router.register(r'quotas', item.QuotaViewSet)
|
||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
|
||||
@@ -55,7 +55,7 @@ from pretix.api.serializers.checkin import (
|
||||
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
|
||||
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
|
||||
)
|
||||
from pretix.api.serializers.item import POSCompatQuestionSerializer
|
||||
from pretix.api.serializers.item import QuestionSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
CheckinListOrderPositionSerializer, CheckinSerializer,
|
||||
FailedCheckinSerializer,
|
||||
@@ -65,8 +65,7 @@ from pretix.api.views.order import OrderPositionFilter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
||||
Question, Questionnaire, QuestionnaireChild, ReusableMedium,
|
||||
RevokedTicketSecret, TeamAPIToken,
|
||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.orders import PrintLog
|
||||
from pretix.base.permissions import AnyPermissionOf
|
||||
@@ -490,7 +489,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'pdf_data': pdf_data and (
|
||||
user if user and user.is_authenticated else auth
|
||||
).has_event_permission(request.organizer, event, 'event.orders:read', request),
|
||||
'preferred_questionnaire_type': Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN,
|
||||
}
|
||||
|
||||
common_checkin_args = dict(
|
||||
@@ -799,19 +797,16 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
# 5. Pre-validate all incoming answers, handle file upload
|
||||
given_answers = {}
|
||||
if answers_data:
|
||||
for qc in QuestionnaireChild.objects.filter(
|
||||
questionnaire__items__in=[op.item], questionnaire__type=Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
):
|
||||
if qc.user_datafield_id and (str(qc.user_datafield_id) in answers_data or str(qc.cooked_id) in answers_data):
|
||||
for q in op.item.questions.filter(ask_during_checkin=True):
|
||||
if str(q.pk) in answers_data:
|
||||
try:
|
||||
ans = answers_data.get(str(qc.user_datafield_id), answers_data[str(qc.cooked_id)])
|
||||
if qc.user_datafield.type == Question.TYPE_FILE:
|
||||
if ans:
|
||||
given_answers[qc.user_datafield] = _handle_file_upload(ans, user, auth)
|
||||
if q.type == Question.TYPE_FILE:
|
||||
if answers_data[str(q.pk)]:
|
||||
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
|
||||
else:
|
||||
given_answers[qc.user_datafield] = None
|
||||
given_answers[q] = None
|
||||
else:
|
||||
given_answers[qc.user_datafield] = qc.user_datafield.clean_answer(ans)
|
||||
given_answers[q] = q.clean_answer(answers_data[str(q.pk)])
|
||||
except (ValidationError, BaseValidationError):
|
||||
pass
|
||||
|
||||
@@ -872,13 +867,14 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
else:
|
||||
perform_checkin(**checkin_args)
|
||||
except RequiredQuestionsError as e:
|
||||
qs = POSCompatQuestionSerializer(context={'event': op.order.event})
|
||||
return Response({
|
||||
'status': 'incomplete',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'questions': POSCompatQuestionSerializer.mangle_results(op.order.event, (qs.to_representation(q) for q in e.questions)), # TODO(questionnaires)
|
||||
'questions': [
|
||||
QuestionSerializer(q).data for q in e.questions
|
||||
],
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
except RequiredMediaExchangeError as e:
|
||||
@@ -977,7 +973,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
ctx['expand'] = self.request.query_params.getlist('expand')
|
||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
||||
ctx['preferred_questionnaire_type'] = Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
return ctx
|
||||
|
||||
def get_filterset_kwargs(self):
|
||||
|
||||
+16
-314
@@ -32,9 +32,6 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Mapping
|
||||
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
@@ -47,27 +44,21 @@ from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.compat import CompatException
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.item import (
|
||||
CompatDatafieldSerializer, DatafieldSerializer, ItemAddOnSerializer,
|
||||
ItemBundleSerializer, ItemCategorySerializer, ItemProgramTimeSerializer,
|
||||
ItemSerializer, ItemVariationSerializer, POSCompatQuestionSerializer,
|
||||
QuestionnaireSerializer, QuestionOptionSerializer, QuotaSerializer,
|
||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
||||
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Item, ItemAddOn, ItemBundle, ItemCategory,
|
||||
ItemProgramTime, ItemVariation, Question, QuestionOption, Quota,
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
||||
ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire, QuestionnaireChild
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
@@ -467,152 +458,27 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class CompatDatafieldFilter(FilterSet):
|
||||
identifier = django_filters.CharFilter(field_name='question__identifier')
|
||||
ask_during_checkin = django_filters.BooleanFilter(method='ask_during_checkin_qs')
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ['required']
|
||||
|
||||
def ask_during_checkin_qs(self, qs, name, value):
|
||||
return qs.filter(
|
||||
questionnaire__type=Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN if value else Questionnaire.QuestionnaireType.ORDER_POSITION_SALE
|
||||
)
|
||||
|
||||
|
||||
class DeprecationWarningMixin:
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
if self.deprecation_warning and isinstance(getattr(response, 'data', None), Mapping) and not 'x-deprecated' in response.headers:
|
||||
response.data = {
|
||||
'deprecation_warning': self.deprecation_warning,
|
||||
**response.data
|
||||
}
|
||||
response.headers['x-deprecated'] = self.deprecation_warning
|
||||
return super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
|
||||
class POSCompatQuestionViewSet(DeprecationWarningMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = POSCompatQuestionSerializer
|
||||
queryset = QuestionnaireChild.objects.none()
|
||||
permission = None
|
||||
sales_channel_ident = None
|
||||
|
||||
def __init__(self, sales_channel_ident, *args, **kwargs):
|
||||
self.sales_channel_ident = sales_channel_ident
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def deprecation_warning(self):
|
||||
return (
|
||||
f'This API endpoint is deprecated and will be removed in the future. '
|
||||
f'Results are generated on-the-fly, count and page size may be inaccurate. '
|
||||
f'Filtered for sales_channel "{self.sales_channel_ident}"'
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return QuestionnaireChild.objects.filter(
|
||||
Q(questionnaire__all_sales_channels=True) | Q(questionnaire__limit_sales_channels__identifier=self.sales_channel_ident),
|
||||
questionnaire__event=self.request.event,
|
||||
# legacy compat only for position-level questions
|
||||
questionnaire__type__startswith='P',
|
||||
).prefetch_related(
|
||||
'user_datafield', 'user_datafield__options', 'questionnaire'
|
||||
).order_by('questionnaire__position', 'position', 'id')
|
||||
|
||||
def list(self, request, **kwargs):
|
||||
resp = super().list(request, **kwargs)
|
||||
resp.data['results'] = POSCompatQuestionSerializer.mangle_results(self.request.event, resp.data['results'])
|
||||
return resp
|
||||
|
||||
def get_object(self):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
|
||||
qid, dfid, sys_df = QuestionnaireChild.uncook_id(self.kwargs[lookup_url_kwarg])
|
||||
print(self.kwargs[lookup_url_kwarg], qid, dfid, sys_df)
|
||||
if qid and sys_df:
|
||||
obj = get_object_or_404(queryset, questionnaire__pk=qid, system_datafield=sys_df)
|
||||
elif qid and dfid:
|
||||
obj = get_object_or_404(queryset, questionnaire__pk=qid, user_datafield__pk=dfid)
|
||||
else:
|
||||
obj = get_object_or_404(queryset, user_datafield__pk=dfid)
|
||||
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, obj)
|
||||
|
||||
return obj
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.question.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.question.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.question.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class DatafieldFilter(FilterSet):
|
||||
class QuestionFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ['identifier']
|
||||
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||
|
||||
|
||||
class BaseDatafieldViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = DatafieldSerializer
|
||||
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = QuestionSerializer
|
||||
queryset = Question.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
filterset_class = DatafieldFilter
|
||||
ordering_fields = ('id')
|
||||
ordering = ('id')
|
||||
filterset_class = QuestionFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'event.items:write'
|
||||
container_type = Question.ContainerType.ORDERPOSITION
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.filter(container_type=self.container_type).prefetch_related('options')
|
||||
|
||||
def get_object(self):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
|
||||
qid, dfid, sys_df = QuestionnaireChild.uncook_id(self.kwargs[lookup_url_kwarg])
|
||||
self.via_questionnaire_id = qid
|
||||
obj = get_object_or_404(queryset, pk=dfid)
|
||||
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, obj)
|
||||
|
||||
return obj
|
||||
return self.request.event.questions.filter(
|
||||
# the container_type parameter is undocumented, this API is going to change in a later release
|
||||
container_type=self.request.GET.get('container_type', Question.ContainerType.ORDERPOSITION),
|
||||
).prefetch_related('options').all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
@@ -627,7 +493,6 @@ class BaseDatafieldViewSet(viewsets.ModelViewSet):
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['container_type'] = self.container_type
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
@@ -650,110 +515,6 @@ class BaseDatafieldViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class DatafieldViewSet(ConditionalListView, BaseDatafieldViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class OrderDatafieldViewSet(ConditionalListView, BaseDatafieldViewSet):
|
||||
container_type = Question.ContainerType.ORDER
|
||||
|
||||
|
||||
def ordered_union(*lists):
|
||||
from graphlib import TopologicalSorter
|
||||
items_set = {item for lst in lists for __ignore, item in lst}
|
||||
dependencies = {item: set() for item in items_set}
|
||||
for lst in lists:
|
||||
for i in range(1, len(lst)):
|
||||
current_item = lst[i][1]
|
||||
previous_item = lst[i - 1][1]
|
||||
dependencies[current_item].add(previous_item)
|
||||
ts = TopologicalSorter(dependencies)
|
||||
try:
|
||||
return list(ts.static_order())
|
||||
except ValueError:
|
||||
raise CompatException("Questionnaire configuration incompatible with old data model due to conflicting field orders.")
|
||||
|
||||
|
||||
class CompatListMixin:
|
||||
deprecation_warning = (
|
||||
f'This API endpoint is deprecated and will be removed in the future. It only works as long as '
|
||||
f'your event\'s questionnaire configuration is compatible with the old data model. '
|
||||
f'Please switch to the .../datafields/ and .../questionnaires/ endpoints.'
|
||||
)
|
||||
|
||||
@property
|
||||
def ignore_errors(self):
|
||||
return self.request.query_params.get('ignore_errors', '') == 'true'
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['ignore_errors'] = self.ignore_errors
|
||||
return ctx
|
||||
|
||||
def do_the_topo_sort(self, queryset):
|
||||
products = {}
|
||||
for df in queryset:
|
||||
for qc in df.referenced_by.all():
|
||||
for item_id in qc.questionnaire.items.values_list('pk', flat=True):
|
||||
k = (qc.questionnaire.type, item_id)
|
||||
products[k] = products.get(k, []) + [((qc.questionnaire.position, qc.position), df)]
|
||||
for q in products.values():
|
||||
q.sort()
|
||||
try:
|
||||
result = ordered_union(*products.values())
|
||||
for i, d in enumerate(result):
|
||||
d.position = i
|
||||
except ValueError:
|
||||
if self.ignore_errors:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
return result
|
||||
|
||||
def list(self, request, **kwargs):
|
||||
logger.info('Returning compatible datafield list on %s/%s', request.organizer.slug, request.event.slug)
|
||||
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
queryset = self.do_the_topo_sort(queryset)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
resultlist = serializer.data
|
||||
return Response({
|
||||
'next': None, 'previous': None, 'count': len(resultlist),
|
||||
'results': resultlist,
|
||||
})
|
||||
|
||||
|
||||
class CompatDatafieldViewSet(ConditionalListView, DeprecationWarningMixin, CompatListMixin, BaseDatafieldViewSet):
|
||||
serializer_class = CompatDatafieldSerializer
|
||||
pagination_class = None
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related('referenced_by', 'referenced_by__questionnaire')
|
||||
|
||||
def _check_defer(self, mode, request, *args, **kwargs):
|
||||
sales_channel_ident = request.query_params.get('sales_channel', None)
|
||||
if sales_channel_ident is None and isinstance(request.auth, Device):
|
||||
sales_channel_ident = 'pretixpos' if request.auth.software_brand.startswith("pretixPOS") else 'web'
|
||||
if sales_channel_ident:
|
||||
logger.info('Returning app compatible question %s for %s (device: %r %s %s) on %s/%s', mode,sales_channel_ident, request.auth, getattr(request.auth, 'software_brand', ''), getattr(request.auth, 'software_version', ''), request.organizer.slug, request.event.slug)
|
||||
return POSCompatQuestionViewSet.as_view(
|
||||
{'get': mode}, sales_channel_ident=sales_channel_ident
|
||||
)(*self.args, request=request._request, **self.kwargs)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if response := self._check_defer('list', request, *args, **kwargs):
|
||||
return response
|
||||
|
||||
logger.info('Returning compatible datafield list on %s/%s', request.organizer.slug, request.event.slug)
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
if response := self._check_defer('retrieve', request, *args, **kwargs):
|
||||
return response
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = QuestionOptionSerializer
|
||||
queryset = QuestionOption.objects.none()
|
||||
@@ -805,65 +566,6 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class QuestionnaireFilter(FilterSet):
|
||||
sales_channel = django_filters.CharFilter(method='sales_channel_qs')
|
||||
|
||||
class Meta:
|
||||
model = Questionnaire
|
||||
fields = ['type', 'items', 'sales_channel']
|
||||
|
||||
def sales_channel_qs(self, qs, name, value):
|
||||
return qs.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
|
||||
class QuestionnaireViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = QuestionnaireSerializer
|
||||
queryset = Questionnaire.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
filterset_class = QuestionnaireFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questionnaires.prefetch_related('children').all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.questionnaire.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ class OrderViewSetMixin:
|
||||
[request.event],
|
||||
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'),
|
||||
to_attr='meta_values_cached'),
|
||||
'questionnaires',
|
||||
'questions',
|
||||
'item_meta_properties',
|
||||
)
|
||||
return Prefetch(
|
||||
@@ -287,9 +287,7 @@ class OrderViewSetMixin:
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'item', 'variation',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question')
|
||||
# TODO(questionnaires) .order_by('question__position')
|
||||
),
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||
'seat',
|
||||
)
|
||||
)
|
||||
@@ -1685,7 +1683,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
@action(detail=True, methods=['POST'])
|
||||
def confirm(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
force = request.data.get('force', False)
|
||||
force = request.data.get('force', False) if request.data else False
|
||||
send_mail = request.data.get('send_email', True) if request.data else True
|
||||
|
||||
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
|
||||
|
||||
+148
-154
@@ -36,6 +36,7 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
@@ -49,8 +50,7 @@ from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import ProgrammingError
|
||||
from django.db.models import Prefetch, Q, QuerySet
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Select, widgets
|
||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||
from django.utils.formats import date_format
|
||||
@@ -82,10 +82,7 @@ from pretix.base.i18n import (
|
||||
from pretix.base.invoicing.transmission import (
|
||||
get_transmission_types, transmission_types,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, Item, Question, Questionnaire, QuestionnaireChild,
|
||||
QuestionOption,
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
|
||||
from pretix.base.models.tax import ask_for_vat_id
|
||||
from pretix.base.services.tax import (
|
||||
VATIDFinalError, VATIDTemporaryError, normalize_vat_id, validate_vat_id,
|
||||
@@ -650,22 +647,49 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
FakeQuestion = namedtuple(
|
||||
'FakeQuestion', 'id question position required help_text container_type', defaults=('', Question.ContainerType.ORDERPOSITION)
|
||||
)
|
||||
|
||||
|
||||
def get_fake_attendee_questions(settings):
|
||||
fq = []
|
||||
sqo = settings.system_question_order
|
||||
|
||||
if settings.attendee_names_asked:
|
||||
fq.append(FakeQuestion('attendee_name_parts', _('Attendee name'), sqo.get('attendee_name_parts', 0), settings.attendee_names_required))
|
||||
|
||||
if settings.attendee_emails_asked:
|
||||
fq.append(FakeQuestion('attendee_email', _('Attendee email'), sqo.get('attendee_email', 0), settings.attendee_emails_required))
|
||||
|
||||
if settings.attendee_company_asked:
|
||||
fq.append(FakeQuestion('company', _('Company'), sqo.get('company', 0), settings.attendee_company_required))
|
||||
|
||||
if settings.attendee_addresses_asked:
|
||||
fq.append(FakeQuestion('street', _('Street'), sqo.get('street', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('zipcode', _('ZIP code'), sqo.get('zipcode', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('city', _('City'), sqo.get('city', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('state', _('State'), sqo.get('country', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('country', _('Country'), sqo.get('country', 0), settings.attendee_addresses_required))
|
||||
return fq
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This is the base form class responsible for asking order- or ticket-related questions.
|
||||
"""
|
||||
address_validation = False
|
||||
|
||||
def build_user_question_field(self, request, event, answerlist, qc, datafield):
|
||||
def build_user_question_field(self, request, event, answerlist, q):
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
answers = [a for a in answerlist if a.question_id == datafield.id]
|
||||
answers = [a for a in answerlist if a.question_id == q.id]
|
||||
if answers:
|
||||
initial = answers[0]
|
||||
else:
|
||||
initial = None
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
required = qc.required and not self.all_optional
|
||||
if datafield.type == Question.TYPE_BOOLEAN:
|
||||
required = q.required and not self.all_optional
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
if required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
# itself.
|
||||
@@ -679,105 +703,107 @@ class BaseQuestionsForm(forms.Form):
|
||||
initialbool = False
|
||||
|
||||
field = forms.BooleanField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initialbool, widget=widget,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_NUMBER:
|
||||
elif q.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_value=datafield.valid_number_min or Decimal('0.00'),
|
||||
max_value=datafield.valid_number_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
min_value=q.valid_number_min or Decimal('0.00'),
|
||||
max_value=q.valid_number_max,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_STRING:
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_length=datafield.valid_string_length_min,
|
||||
max_length=datafield.valid_string_length_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
min_length=q.valid_string_length_min,
|
||||
max_length=q.valid_string_length_max,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_TEXT:
|
||||
elif q.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_length=datafield.valid_string_length_min,
|
||||
max_length=datafield.valid_string_length_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
min_length=q.valid_string_length_min,
|
||||
max_length=q.valid_string_length_max,
|
||||
help_text=rich_text(q.help_text),
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_COUNTRYCODE:
|
||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||
field = CountryField(
|
||||
countries=CachedCountries,
|
||||
blank=True, null=True, blank_label=' ',
|
||||
).formfield(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
widget=forms.Select,
|
||||
empty_label=' ',
|
||||
initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None),
|
||||
initial=initial.answer if initial else (
|
||||
guess_country_from_request(request, event) if required else None),
|
||||
)
|
||||
elif datafield.type == Question.TYPE_CHOICE:
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
queryset=datafield.options,
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
queryset=q.options,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
widget=forms.Select,
|
||||
to_field_name='identifier',
|
||||
empty_label='',
|
||||
initial=initial.options.first() if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
field = forms.ModelMultipleChoiceField(
|
||||
queryset=datafield.options,
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
queryset=q.options,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
to_field_name='identifier',
|
||||
widget=QuestionCheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_FILE:
|
||||
if datafield.valid_file_portrait:
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
if q.valid_file_portrait:
|
||||
field = PortraitImageField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.file if initial else None,
|
||||
widget=PortraitImageWidget(answer=initial, request=request, attrs={'data-portrait-photo': 'true'}),
|
||||
widget=PortraitImageWidget(answer=initial, request=request,
|
||||
attrs={'data-portrait-photo': 'true'}),
|
||||
)
|
||||
else:
|
||||
field = ExtFileField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(answer=initial, request=request),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_DATE:
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
attrs = {}
|
||||
if datafield.valid_date_min:
|
||||
attrs['data-min'] = datafield.valid_date_min.isoformat()
|
||||
if datafield.valid_date_max:
|
||||
attrs['data-max'] = datafield.valid_date_max.isoformat()
|
||||
help_text = qc.help_text
|
||||
if q.valid_date_min:
|
||||
attrs['data-min'] = q.valid_date_min.isoformat()
|
||||
if q.valid_date_max:
|
||||
attrs['data-max'] = q.valid_date_max.isoformat()
|
||||
help_text = q.help_text
|
||||
if not help_text:
|
||||
if datafield.valid_date_min and datafield.valid_date_max:
|
||||
if q.valid_date_min and q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date between {min} and {max}.'),
|
||||
min=date_format(datafield.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(datafield.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_date_min:
|
||||
elif q.valid_date_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no earlier than {min}.'),
|
||||
min=date_format(datafield.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_date_max:
|
||||
elif q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no later than {max}.'),
|
||||
max=date_format(datafield.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
@@ -787,16 +813,16 @@ class BaseQuestionsForm(forms.Form):
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.DateField(
|
||||
label=escape(qc.label), required=required,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(help_text),
|
||||
initial=_initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
)
|
||||
if datafield.valid_date_min:
|
||||
field.validators.append(MinDateValidator(datafield.valid_date_min))
|
||||
if datafield.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(datafield.valid_date_max))
|
||||
elif datafield.type == Question.TYPE_TIME:
|
||||
if q.valid_date_min:
|
||||
field.validators.append(MinDateValidator(q.valid_date_min))
|
||||
if q.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(q.valid_date_max))
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).time()
|
||||
@@ -805,29 +831,29 @@ class BaseQuestionsForm(forms.Form):
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.TimeField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=_initial,
|
||||
widget=TimePickerWidget(without_seconds=True),
|
||||
)
|
||||
elif datafield.type == Question.TYPE_DATETIME:
|
||||
help_text = qc.help_text
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
help_text = q.help_text
|
||||
if not help_text:
|
||||
if datafield.valid_datetime_min and datafield.valid_datetime_max:
|
||||
if q.valid_datetime_min and q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time between {min} and {max}.'),
|
||||
min=date_format(datafield.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(datafield.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_datetime_min:
|
||||
elif q.valid_datetime_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no earlier than {min}.'),
|
||||
min=date_format(datafield.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_datetime_max:
|
||||
elif q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no later than {max}.'),
|
||||
max=date_format(datafield.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
|
||||
if initial and initial.answer:
|
||||
@@ -839,20 +865,20 @@ class BaseQuestionsForm(forms.Form):
|
||||
_initial = None
|
||||
|
||||
field = SplitDateTimeField(
|
||||
label=escape(qc.label), required=required,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(help_text),
|
||||
initial=_initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=datafield.valid_datetime_min,
|
||||
max_date=datafield.valid_datetime_max
|
||||
min_date=q.valid_datetime_min,
|
||||
max_date=q.valid_datetime_max
|
||||
),
|
||||
)
|
||||
if datafield.valid_datetime_min:
|
||||
field.validators.append(MinDateTimeValidator(datafield.valid_datetime_min))
|
||||
if datafield.valid_datetime_max:
|
||||
field.validators.append(MaxDateTimeValidator(datafield.valid_datetime_max))
|
||||
elif datafield.type == Question.TYPE_PHONENUMBER:
|
||||
if q.valid_datetime_min:
|
||||
field.validators.append(MinDateTimeValidator(q.valid_datetime_min))
|
||||
if q.valid_datetime_max:
|
||||
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
|
||||
elif q.type == Question.TYPE_PHONENUMBER:
|
||||
if initial:
|
||||
try:
|
||||
initial = PhoneNumber().from_string(initial.answer)
|
||||
@@ -865,40 +891,28 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial = "+{}.".format(phone_prefix)
|
||||
|
||||
field = PhoneNumberField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
|
||||
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
|
||||
# the future.
|
||||
initial=initial,
|
||||
widget=WrappedPhoneNumberPrefixWidget()
|
||||
)
|
||||
else:
|
||||
raise ProgrammingError('Invalid question type')
|
||||
field.datafield = datafield
|
||||
field.question = q
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
|
||||
if qc.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = f"question_{qc.dependency_question.user_datafield_id}" if qc.dependency_question.user_datafield_id else qc.dependency_question.system_datafield
|
||||
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(qc.dependency_values))
|
||||
if datafield.type != 'M':
|
||||
field.widget.attrs['required'] = qc.required and not self.all_optional
|
||||
field._required = qc.required and not self.all_optional
|
||||
if q.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
|
||||
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values))
|
||||
if q.type != 'M':
|
||||
field.widget.attrs['required'] = q.required and not self.all_optional
|
||||
field._required = q.required and not self.all_optional
|
||||
field.required = False
|
||||
|
||||
return field
|
||||
|
||||
def build_text_block(self, request, event, qc):
|
||||
return forms.CharField( # TODO(questionnaires): use a less hacky way to format this field
|
||||
required=False,
|
||||
label="",
|
||||
help_text=rich_text((f"#### {qc.label}\n" if qc.label else "") + str(qc.help_text)),
|
||||
disabled=True,
|
||||
widget=QuestionCheckboxSelectMultiple,
|
||||
)
|
||||
|
||||
def check_user_questions(self, d):
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
@@ -950,7 +964,8 @@ class OrderLevelQuestionsForm(BaseQuestionsForm):
|
||||
"""
|
||||
Takes two additional keyword arguments:
|
||||
|
||||
:param container: The checkout session or order the form should be for
|
||||
:param checkoutsession: The checkout session the form should be for
|
||||
:param order: The order the form should be for
|
||||
:param event: The event this belongs to
|
||||
"""
|
||||
request = kwargs.pop('request', None)
|
||||
@@ -959,35 +974,14 @@ class OrderLevelQuestionsForm(BaseQuestionsForm):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
questionnaires = Questionnaire.objects.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=container.sales_channel.identifier),
|
||||
event=event, type=Questionnaire.QuestionnaireType.ORDER_SALE,
|
||||
).order_by('position').prefetch_related(
|
||||
Prefetch('children', QuestionnaireChild.objects.prefetch_related(
|
||||
Prefetch('user_datafield', Question.objects.prefetch_related(
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
))
|
||||
),
|
||||
to_attr='childlist')
|
||||
)
|
||||
questions = Question.objects.filter(
|
||||
event=event, container_type=Question.ContainerType.ORDER,
|
||||
ask_during_checkin=False, hidden=False,
|
||||
).order_by('position')
|
||||
answerlist = container.answers.prefetch_related('options')
|
||||
|
||||
idx = 0
|
||||
for questionnaire in questionnaires:
|
||||
for child in getattr(questionnaire, 'childlist', questionnaire.children.all()):
|
||||
if child.user_datafield:
|
||||
df = child.user_datafield
|
||||
self.fields['question_%s' % df.id] = self.build_user_question_field(request, event, answerlist, child, df)
|
||||
else:
|
||||
self.fields['text_%d' % idx] = self.build_text_block(request, event, child)
|
||||
idx += 1
|
||||
for q in questions:
|
||||
self.fields['question_%s' % q.id] = self.build_user_question_field(request, event, answerlist, q)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
@@ -1014,7 +1008,6 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
orderpos = self.orderpos = kwargs.pop('orderpos', None)
|
||||
pos = cartpos or orderpos
|
||||
item = pos.item
|
||||
questionnaires = pos.item.relevant_questionnaires
|
||||
event = kwargs.pop('event')
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
|
||||
@@ -1024,17 +1017,18 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||
self.fields['requested_valid_from'] = self.build_requested_valid_from_field(event, pos, item)
|
||||
|
||||
idx = 0
|
||||
for questionnaire in questionnaires:
|
||||
for child in getattr(questionnaire, 'childlist', questionnaire.children.all()):
|
||||
if child.user_datafield:
|
||||
df = child.user_datafield
|
||||
self.fields['question_%s' % df.id] = self.build_user_question_field(request, event, pos.answerlist, child, df)
|
||||
elif child.system_datafield:
|
||||
self.fields[child.system_datafield] = self.build_system_question_field(request, event, pos, child)
|
||||
else:
|
||||
self.fields['text_%d' % idx] = self.build_text_block(request, event, child)
|
||||
idx += 1
|
||||
questions = []
|
||||
if item.ask_attendee_data:
|
||||
questions += get_fake_attendee_questions(event.settings)
|
||||
questions += pos.item.questions_to_ask
|
||||
|
||||
questions.sort(key=lambda q: q.position)
|
||||
|
||||
for q in questions:
|
||||
if isinstance(q, FakeQuestion):
|
||||
self.fields[q.id] = self.build_system_question_field(request, event, pos, q)
|
||||
else:
|
||||
self.fields['question_%s' % q.id] = self.build_user_question_field(request, event, pos.answerlist, q)
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=pos)
|
||||
data = pos.meta_info_data
|
||||
@@ -1099,21 +1093,21 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
)
|
||||
|
||||
def build_system_question_field(self, request, event, pos, qc):
|
||||
field_name = qc.system_datafield
|
||||
field_name = qc.id
|
||||
if field_name == 'attendee_name_parts':
|
||||
return NamePartsFormField(
|
||||
max_length=255,
|
||||
required=qc.required and not self.all_optional,
|
||||
scheme=event.settings.name_scheme,
|
||||
titles=event.settings.name_scheme_titles,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.attendee_name_parts,
|
||||
)
|
||||
if field_name == 'attendee_email':
|
||||
return forms.EmailField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.attendee_email,
|
||||
widget=forms.EmailInput(
|
||||
@@ -1125,7 +1119,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if field_name == 'company':
|
||||
return forms.CharField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
max_length=255,
|
||||
initial=pos.company,
|
||||
@@ -1134,7 +1128,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if field_name == 'street':
|
||||
return forms.CharField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
@@ -1147,7 +1141,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
return forms.CharField(
|
||||
required=False,
|
||||
max_length=30,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.zipcode,
|
||||
widget=forms.TextInput(attrs={
|
||||
@@ -1157,7 +1151,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if field_name == 'city':
|
||||
return forms.CharField(
|
||||
required=False,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
max_length=255,
|
||||
initial=pos.city,
|
||||
@@ -1171,7 +1165,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=country,
|
||||
widget=forms.Select(attrs={
|
||||
@@ -1199,7 +1193,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
field = forms.ChoiceField(
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
required=False,
|
||||
choices=c,
|
||||
|
||||
@@ -71,8 +71,8 @@ class EmailTransmissionType(TransmissionType):
|
||||
|
||||
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
|
||||
return {
|
||||
"transmission_email_other": bool(transmission_info.get("transmission_email_address")),
|
||||
"transmission_email_address": transmission_info.get("transmission_email_address"),
|
||||
"transmission_email_other": bool((transmission_info or {}).get("transmission_email_address")),
|
||||
"transmission_email_address": (transmission_info or {}).get("transmission_email_address"),
|
||||
}
|
||||
|
||||
def form_data_to_transmission_info(self, form_data: dict) -> dict:
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
# Generated by Django 4.2.29 on 2026-03-19 14:24
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from itertools import chain, groupby
|
||||
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.fields
|
||||
from pretix.base.i18n import language
|
||||
|
||||
FakeQuestion = namedtuple(
|
||||
'FakeQuestion', 'id question position required'
|
||||
)
|
||||
|
||||
|
||||
def get_fake_questions(settings):
|
||||
def b(s):
|
||||
return s == 'True'
|
||||
fq = []
|
||||
sqo = json.loads(settings.get('system_question_order', '{}'))
|
||||
_ = LazyI18nString.from_gettext
|
||||
|
||||
if b(settings.get('attendee_names_asked', 'True')):
|
||||
fq.append(FakeQuestion('attendee_name_parts', _('Attendee name'), sqo.get('attendee_name_parts', 0), b(settings.get('attendee_names_required'))))
|
||||
|
||||
if b(settings.get('attendee_emails_asked')):
|
||||
fq.append(FakeQuestion('attendee_email', _('Attendee email'), sqo.get('attendee_email', 0), b(settings.get('attendee_emails_required'))))
|
||||
|
||||
if b(settings.get('attendee_company_asked')):
|
||||
fq.append(FakeQuestion('company', _('Company'), sqo.get('company', 0), b(settings.get('attendee_company_required'))))
|
||||
|
||||
if b(settings.get('attendee_addresses_asked')):
|
||||
fq.append(FakeQuestion('street', _('Street'), sqo.get('street', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('zipcode', _('ZIP code'), sqo.get('zipcode', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('city', _('City'), sqo.get('city', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('state', _('State'), sqo.get('country', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('country', _('Country'), sqo.get('country', 0), b(settings.get('attendee_addresses_required'))))
|
||||
return fq
|
||||
|
||||
|
||||
def migrate_questions_forward(apps, schema_editor):
|
||||
from django.utils.translation import (
|
||||
get_supported_language_variant, gettext as _,
|
||||
)
|
||||
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
Item = apps.get_model("pretixbase", "Item")
|
||||
Question = apps.get_model("pretixbase", "Question")
|
||||
Questionnaire = apps.get_model("pretixbase", "Questionnaire")
|
||||
QuestionnaireChild = apps.get_model("pretixbase", "QuestionnaireChild")
|
||||
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||
|
||||
def create_grouped_item_questionnaires(event, children, label_prefix, questionnaire_type, personalized_items=None):
|
||||
# group by item, creating a unique questionnaire per item
|
||||
item_questionnaires = (([t[3] for t in children], item_id) for item_id, children in
|
||||
groupby(children, key=lambda t: t[0]))
|
||||
|
||||
# group again, merging all questionnaires with identical children
|
||||
merged_questionnaires = groupby(sorted(item_questionnaires, key=lambda t: [q.id for q in t[0]]),
|
||||
key=lambda t: t[0])
|
||||
for children, iterator in merged_questionnaires:
|
||||
items = [item for _c, item in iterator]
|
||||
|
||||
internal_name = label_prefix + ', '.join(str(iname or name) for (id, iname, name) in items)
|
||||
if all(i.personalized for i in items) and len(items) == len(personalized_items) and questionnaire_type == 'PS':
|
||||
# if we have a questionnaire for exactly all personalized items, we just call it "Attendee data"
|
||||
# (same as the default questionnaire for new events)
|
||||
internal_name = _('Attendee data')
|
||||
|
||||
# create questionnaires and children
|
||||
questionnaire = Questionnaire.objects.create(
|
||||
event=event, type=questionnaire_type, position=0, all_sales_channels=True,
|
||||
internal_name=internal_name
|
||||
)
|
||||
questionnaire.items.set([id for (id, iname, name) in items])
|
||||
deps = {}
|
||||
for position, child in enumerate(children):
|
||||
if isinstance(child, FakeQuestion):
|
||||
QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
system_datafield=child.id,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
)
|
||||
else:
|
||||
deps[child.id] = QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
user_datafield=child,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
help_text=child.help_text,
|
||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
||||
dependency_values=child.dependency_values,
|
||||
)
|
||||
|
||||
for event in Event.objects.iterator():
|
||||
# get relevant settings
|
||||
settings = {
|
||||
setting.key: setting.value for setting in EventSettingsStore.objects.filter(object_id=event.id, key__in=(
|
||||
'system_question_order', 'attendee_names_asked', 'attendee_names_required', 'attendee_emails_asked', 'attendee_emails_required',
|
||||
'attendee_company_asked', 'attendee_company_required', 'attendee_addresses_asked', 'attendee_addresses_required', 'locale',
|
||||
))
|
||||
}
|
||||
with language(get_supported_language_variant(settings.get('locale', 'en'))):
|
||||
# [P] get all ticket-level questions (user-defined and system provided), along with the products for which they're asked
|
||||
questions = event.questions.filter(container_type='P')
|
||||
|
||||
# [PS] presale
|
||||
personalized_items = event.items.filter(personalized=True).values_list('id', 'internal_name', 'name')
|
||||
children = sorted(chain((
|
||||
(item, q.position, 0, q)
|
||||
for q in get_fake_questions(settings)
|
||||
for item in personalized_items
|
||||
), (
|
||||
(item, q.position, q.id, q)
|
||||
for q in questions.filter(hidden=False, ask_during_checkin=False)
|
||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
||||
)), key=lambda t: (t[0], t[1]))
|
||||
|
||||
create_grouped_item_questionnaires(event, children, '', 'PS', personalized_items)
|
||||
|
||||
# [PC] checkin
|
||||
children = sorted(chain((
|
||||
(item, q.position, q.id, q)
|
||||
for q in questions.filter(ask_during_checkin=True)
|
||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
||||
)), key=lambda t: (t[0], t[1]))
|
||||
|
||||
create_grouped_item_questionnaires(event, children, _('Check-in questions for '), 'PC')
|
||||
|
||||
# [PH] hidden
|
||||
children = sorted(chain((
|
||||
(item, q.position, q.id, q)
|
||||
for q in questions.filter(hidden=True, ask_during_checkin=False)
|
||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
||||
)), key=lambda t: (t[0], t[1]))
|
||||
|
||||
create_grouped_item_questionnaires(event, children, _('Hidden questions for '), 'PH')
|
||||
|
||||
# [O] get all order-level questions
|
||||
questions = list(event.questions.filter(container_type='O', hidden=False).order_by('position'))
|
||||
if questions:
|
||||
# create questionnaires and children
|
||||
questionnaire = Questionnaire.objects.create(
|
||||
event=event, type='OS', position=0, all_sales_channels=True,
|
||||
internal_name=_('Per-order questions'),
|
||||
)
|
||||
deps = {}
|
||||
for position, child in enumerate(questions):
|
||||
deps[child.id] = QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
user_datafield=child,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
help_text=child.help_text,
|
||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
||||
dependency_values=child.dependency_values,
|
||||
)
|
||||
|
||||
|
||||
def migrate_questions_backward(apps, schema_editor):
|
||||
pass # as long as we don't delete the old columns, this is a no op. after that, it gets complicated...
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0311_fix_unshredded_invoices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Questionnaire',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('internal_name', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=5)),
|
||||
('position', models.PositiveIntegerField(default=0)),
|
||||
('all_sales_channels', models.BooleanField(default=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questionnaires', to='pretixbase.event')),
|
||||
('items', models.ManyToManyField(related_name='questionnaires', to='pretixbase.item')),
|
||||
('limit_sales_channels', models.ManyToManyField(to='pretixbase.saleschannel')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='QuestionnaireChild',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('position', models.PositiveIntegerField(default=0)),
|
||||
('system_datafield', models.CharField(max_length=25, null=True)),
|
||||
('required', models.BooleanField(default=False)),
|
||||
('label', i18nfield.fields.I18nTextField()),
|
||||
('help_text', i18nfield.fields.I18nTextField(null=True)),
|
||||
('dependency_values', pretix.base.models.fields.MultiStringField(default=[])),
|
||||
('dependency_question', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dependent_questions', to='pretixbase.questionnairechild')),
|
||||
('questionnaire', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pretixbase.questionnaire')),
|
||||
('user_datafield', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='references', to='pretixbase.question')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.RunPython(
|
||||
migrate_questions_forward,
|
||||
migrate_questions_backward,
|
||||
),
|
||||
# TODO(questionnaires) remove old columns from Question model
|
||||
]
|
||||
@@ -37,8 +37,8 @@ from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
|
||||
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
|
||||
Questionnaire, QuestionnaireChild, QuestionOption, Quota, SubEventItem,
|
||||
SubEventItemVariation, itempicture_upload_to,
|
||||
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
|
||||
itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .mail import OutgoingMail
|
||||
|
||||
@@ -737,9 +737,6 @@ class Event(EventMixin, LoggedModel):
|
||||
self.settings.mail_send_order_approved_attendee = True
|
||||
self.settings.mail_send_order_approved_free_attendee = True
|
||||
self.settings.mail_send_download_reminder_attendee = True
|
||||
from . import Questionnaire
|
||||
q = self.questionnaires.create(internal_name=str(_('Attendee data')), type=Questionnaire.QuestionnaireType.ORDER_POSITION_SALE, position=0)
|
||||
q.children.create(label=str(_('Attendee name')), system_datafield='attendee_name_parts', position=0)
|
||||
|
||||
@property
|
||||
def social_image(self):
|
||||
@@ -1429,10 +1426,6 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
return issues
|
||||
|
||||
@property
|
||||
def sales_channels(self):
|
||||
return self.organizer.sales_channels if self.all_sales_channels else self.limit_sales_channels
|
||||
|
||||
def get_users_with_any_permission(self):
|
||||
"""
|
||||
Returns a queryset of users who have any permission to this event.
|
||||
|
||||
+52
-241
@@ -41,7 +41,6 @@ import warnings
|
||||
from collections import Counter, OrderedDict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal, DecimalException
|
||||
from itertools import groupby
|
||||
from typing import Optional, Tuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -1566,14 +1565,12 @@ class ItemBundle(models.Model):
|
||||
raise ValidationError(_('The count needs to be equal to or greater than zero.'))
|
||||
|
||||
|
||||
class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
class Question(LoggedModel):
|
||||
"""
|
||||
A question is a data field that can be used to extend an order or a ticket by custom
|
||||
information, e.g. "Attendee age". To be actually useful, questions need to be added to
|
||||
one or multiple Questionnaires. The answers may be found in QuestionAnswers, attached
|
||||
to Orders, OrderPositions or CartPositions.
|
||||
|
||||
A question can allow one of several input types, currently:
|
||||
A question is an input field that can be used to extend a ticket by custom information,
|
||||
e.g. "Attendee age". The answers are found next to the position. The answers may be found
|
||||
in QuestionAnswers, attached to OrderPositions/CartPositions. A question can allow one of
|
||||
several input types, currently:
|
||||
|
||||
* a number (``TYPE_NUMBER``)
|
||||
* a one-line string (``TYPE_STRING``)
|
||||
@@ -1593,7 +1590,7 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
:param required: Whether answering this question is required for submitting an order including
|
||||
items associated with this question.
|
||||
:type required: bool
|
||||
:param items: TO BE REMOVED
|
||||
:param items: A set of ``Items`` objects that this question should be applied to
|
||||
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
||||
:type ask_during_checkin: bool
|
||||
:param show_during_checkin: Whether to show the answer to this question during check-in.
|
||||
@@ -1610,22 +1607,6 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
class ContainerType(models.TextChoices):
|
||||
ORDER = "O", _("Order")
|
||||
ORDERPOSITION = "P", _("Order position")
|
||||
|
||||
class FieldType(models.TextChoices):
|
||||
NUMBER = "N", _("Number")
|
||||
STRING = "S", _("Text (one line)")
|
||||
TEXT = "T", _("Multiline text")
|
||||
BOOLEAN = "B", _("Yes/No")
|
||||
CHOICE = "C", _("Choose one from a list")
|
||||
CHOICE_MULTIPLE = "M", _("Choose multiple from a list")
|
||||
FILE = "F", _("File upload")
|
||||
DATE = "D", _("Date")
|
||||
TIME = "H", _("Time")
|
||||
DATETIME = "W", _("Date and time")
|
||||
COUNTRYCODE = "CC", _("Country code (ISO 3166-1 alpha-2)")
|
||||
PHONENUMBER = "TEL", _("Phone number")
|
||||
|
||||
# compat
|
||||
TYPE_NUMBER = "N"
|
||||
TYPE_STRING = "S"
|
||||
TYPE_TEXT = "T"
|
||||
@@ -1638,8 +1619,20 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
TYPE_DATETIME = "W"
|
||||
TYPE_COUNTRYCODE = "CC"
|
||||
TYPE_PHONENUMBER = "TEL"
|
||||
TYPE_CHOICES = FieldType.choices
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_NUMBER, _("Number")),
|
||||
(TYPE_STRING, _("Text (one line)")),
|
||||
(TYPE_TEXT, _("Multiline text")),
|
||||
(TYPE_BOOLEAN, _("Yes/No")),
|
||||
(TYPE_CHOICE, _("Choose one from a list")),
|
||||
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
|
||||
(TYPE_FILE, _("File upload")),
|
||||
(TYPE_DATE, _("Date")),
|
||||
(TYPE_TIME, _("Time")),
|
||||
(TYPE_DATETIME, _("Date and time")),
|
||||
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
|
||||
(TYPE_PHONENUMBER, _("Phone number")),
|
||||
)
|
||||
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
|
||||
ASK_DURING_CHECKIN_UNSUPPORTED = []
|
||||
SHOW_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE]
|
||||
@@ -1655,8 +1648,8 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
verbose_name=_("Asked on"),
|
||||
default=ContainerType.ORDERPOSITION,
|
||||
)
|
||||
question = I18nTextField( # TODO(questionnaires) : to be renamed to 'internal_name'
|
||||
verbose_name=_("Internal name"),
|
||||
question = I18nTextField(
|
||||
verbose_name=_("Question")
|
||||
)
|
||||
identifier = models.CharField(
|
||||
max_length=190,
|
||||
@@ -1670,46 +1663,42 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
),
|
||||
],
|
||||
)
|
||||
#help_text = I18nTextField(
|
||||
# # TODO(questionnaires) : to be removed
|
||||
# verbose_name=_("Help text"),
|
||||
# help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
# null=True, blank=True,
|
||||
#)
|
||||
help_text = I18nTextField(
|
||||
verbose_name=_("Help text"),
|
||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=5,
|
||||
choices=FieldType.choices,
|
||||
choices=TYPE_CHOICES,
|
||||
verbose_name=_("Question type")
|
||||
)
|
||||
tbd_required = models.BooleanField( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Required question"),
|
||||
db_column="required",
|
||||
verbose_name=_("Required question")
|
||||
)
|
||||
# items = models.ManyToManyField( # TODO(questionnaires) : to be removed, -> Questionnaire
|
||||
# Item,
|
||||
# related_name='questions',
|
||||
# verbose_name=_("Products"),
|
||||
# blank=True,
|
||||
# help_text=_('This question will be asked to buyers of the selected products')
|
||||
# )
|
||||
tbd_position = models.PositiveIntegerField( # TODO(questionnaires) : to be removed, -> Questionnaire + QuestionnaireChild
|
||||
items = models.ManyToManyField(
|
||||
Item,
|
||||
related_name='questions',
|
||||
verbose_name=_("Products"),
|
||||
blank=True,
|
||||
help_text=_('This question will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position"),
|
||||
db_column="position",
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
tbd_ask_during_checkin = models.BooleanField( # TODO(questionnaires) : to be removed
|
||||
ask_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False,
|
||||
db_column="ask_during_checkin",
|
||||
default=False
|
||||
)
|
||||
show_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Show answer during check-in'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
hidden = models.BooleanField( # to be removed
|
||||
hidden = models.BooleanField(
|
||||
verbose_name=_('Hidden question'),
|
||||
help_text=_('This question will only show up in the backend.'),
|
||||
default=False
|
||||
@@ -1718,10 +1707,10 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
verbose_name=_('Print answer on invoices'),
|
||||
default=False
|
||||
)
|
||||
dependency_question = models.ForeignKey( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
dependency_question = models.ForeignKey(
|
||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_values = MultiStringField(default=[]) # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
dependency_values = MultiStringField(default=[])
|
||||
valid_number_min = models.DecimalField(decimal_places=6, max_digits=30, null=True, blank=True,
|
||||
verbose_name=_('Minimum value'),
|
||||
help_text=_('Currently not supported in our apps and during check-in'))
|
||||
@@ -1760,9 +1749,9 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Data field")
|
||||
verbose_name_plural = _("Data fields")
|
||||
ordering = ('question', 'id')
|
||||
verbose_name = _("Question")
|
||||
verbose_name_plural = _("Questions")
|
||||
ordering = ('position', 'id')
|
||||
unique_together = (('event', 'identifier'),)
|
||||
|
||||
def __str__(self):
|
||||
@@ -1800,16 +1789,15 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
return self.id
|
||||
return self.position, self.id
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
def clean_answer(self, answer):
|
||||
# TODO(questionnaires) - we don't have Question.required any more - do we actually need this check? if yes, pass in required from caller
|
||||
# if self.required:
|
||||
# if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
|
||||
# raise ValidationError(_('An answer to this question is required to proceed.'))
|
||||
if self.required:
|
||||
if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
|
||||
raise ValidationError(_('An answer to this question is required to proceed.'))
|
||||
if not answer:
|
||||
if self.type == Question.TYPE_BOOLEAN:
|
||||
return False
|
||||
@@ -1914,7 +1902,7 @@ class Question(LoggedModel): # TODO(questionnaires) - rename to Datafield ?
|
||||
return answer
|
||||
|
||||
@staticmethod
|
||||
def clean_items(event, items): # TODO(questionnaires) : remove method / move to qc
|
||||
def clean_items(event, items):
|
||||
for item in items:
|
||||
if event != item.event:
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
@@ -1995,183 +1983,6 @@ class QuestionOption(models.Model):
|
||||
ordering = ('position', 'id')
|
||||
|
||||
|
||||
class Questionnaire(LoggedModel):
|
||||
class QuestionnaireType(models.TextChoices):
|
||||
ORDER_SALE = "OS", _("Order-wide, before purchase")
|
||||
ORDER_POSITION_SALE = "PS", _("Per product, before purchase")
|
||||
ORDER_POSITION_ATTENDEE_ONLY = "PA", _("Per product, via attendee link")
|
||||
ORDER_POSITION_CHECKIN = "PC", _("Per product, at check-in")
|
||||
ORDER_POSITION_HIDDEN = "PH", _("Per product, hidden")
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
related_name="questionnaires",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
internal_name = models.CharField(
|
||||
verbose_name=_("Internal name"),
|
||||
max_length=255,
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=5,
|
||||
choices=QuestionnaireType.choices,
|
||||
verbose_name=_("Questionnaire type")
|
||||
)
|
||||
items = models.ManyToManyField(
|
||||
Item,
|
||||
related_name='questionnaires',
|
||||
verbose_name=_("Products"),
|
||||
blank=True,
|
||||
help_text=_('This questionnaire will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels the product is sold on"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
||||
'selected here but not on product level, the variation will not be available.'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('position', 'id')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.internal_name
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
return self.position, self.id
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
@staticmethod
|
||||
def check_constraints(event):
|
||||
errors = set()
|
||||
all_questionnaires = Questionnaire.objects.filter(event=event).prefetch_related('children', 'items')
|
||||
all_sales_channels = event.sales_channels.all()
|
||||
for (sales_channel, type, item_id, sdf, udf), fields in groupby(sorted(
|
||||
(sales_channel, q.type, item.id, c.system_datafield or '', c.user_datafield_id or 0, q, item)
|
||||
for q in all_questionnaires
|
||||
for sales_channel in (q.limit_sales_channels if not q.all_sales_channels else all_sales_channels).values_list('pk', flat=True)
|
||||
for item in q.items.all()
|
||||
for c in q.children.all()
|
||||
), key=lambda d: (d[0], d[1], d[2], d[3], d[4])):
|
||||
fields = list(fields)
|
||||
if len(fields) > 1:
|
||||
sales_channel, type, item_id, sdf, udf, q, item = fields[0]
|
||||
questionnaires = set(q for sc, type, item, sdf, udf, q, item in fields)
|
||||
df_label = _('System data field') + f' "{sdf}"' if sdf else _('User-defined data field') + f' "{str(udf)}"'
|
||||
if len(questionnaires) == 1:
|
||||
errors.add(_('{datafield} added twice to questionnaire "{questionnaire}"').format(
|
||||
datafield=df_label, questionnaire=next(str(q) for q in questionnaires)
|
||||
))
|
||||
else:
|
||||
errors.add(_('{datafield} added twice to the same item "{item}" via questionnaires "{questionnaires}"').format(
|
||||
datafield=df_label, item=str(item), questionnaires='" and "'.join(str(q) for q in questionnaires)
|
||||
))
|
||||
if errors:
|
||||
raise ValidationError(list(errors))
|
||||
|
||||
|
||||
class QuestionnaireChild(LoggedModel):
|
||||
class SystemQuestion(models.TextChoices):
|
||||
ATTENDEE_NAME_PARTS = 'attendee_name_parts', _('Attendee name')
|
||||
ATTENDEE_EMAIL = 'attendee_email', _('Attendee email')
|
||||
COMPANY = 'company', _('Company')
|
||||
STREET = 'street', _('Street')
|
||||
ZIPCODE = 'zipcode', _('ZIP code')
|
||||
CITY = 'city', _('City')
|
||||
STATE = 'state', _('State')
|
||||
COUNTRY = 'country', _('Country')
|
||||
|
||||
# _name_schemes_all_fields = sorted(list(set(k for scheme in PERSON_NAME_SCHEMES.values() for k, v, w, *__ in scheme['fields'])))
|
||||
_name_schemes_all_fields = ['calling_name', 'degree', 'family_name', 'full_name', 'given_name',
|
||||
'latin_transcription', 'middle_name', 'salutation', 'title']
|
||||
SYSTEM_DATAFIELD_NUMBERS = [
|
||||
SystemQuestion.ATTENDEE_NAME_PARTS.value,
|
||||
SystemQuestion.ATTENDEE_EMAIL.value,
|
||||
SystemQuestion.COMPANY.value,
|
||||
SystemQuestion.STREET.value,
|
||||
SystemQuestion.ZIPCODE.value,
|
||||
SystemQuestion.CITY.value,
|
||||
SystemQuestion.STATE.value,
|
||||
SystemQuestion.COUNTRY.value,
|
||||
] + [
|
||||
'attendee_name_parts:' + field
|
||||
for field in _name_schemes_all_fields
|
||||
]
|
||||
|
||||
questionnaire = models.ForeignKey(
|
||||
Questionnaire,
|
||||
related_name="children",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
user_datafield = models.ForeignKey(
|
||||
Question,
|
||||
related_name="referenced_by",
|
||||
on_delete=models.CASCADE,
|
||||
null=True, blank=True,
|
||||
)
|
||||
system_datafield = models.CharField(
|
||||
max_length=25,
|
||||
choices=SystemQuestion.choices,
|
||||
null=True, blank=True,
|
||||
)
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Required question")
|
||||
)
|
||||
label = I18nTextField(
|
||||
verbose_name=_("Question")
|
||||
)
|
||||
help_text = I18nTextField(
|
||||
verbose_name=_("Help text"),
|
||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
dependency_question = models.ForeignKey(
|
||||
'QuestionnaireChild', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_values = MultiStringField(default=[])
|
||||
|
||||
class Meta:
|
||||
ordering = ('position', 'id')
|
||||
|
||||
@property
|
||||
def cooked_id(self):
|
||||
return QuestionnaireChild.cook_id_from_parts(self.questionnaire_id, self.user_datafield_id, self.system_datafield)
|
||||
|
||||
@staticmethod
|
||||
def cook_id_from_parts(qid, dfid, sys_df):
|
||||
df_id = dfid + 100 if dfid else QuestionnaireChild.SYSTEM_DATAFIELD_NUMBERS.index(sys_df)
|
||||
assert qid < 1000000000
|
||||
return qid * 1000000000 + df_id
|
||||
|
||||
@staticmethod
|
||||
def uncook_id(id):
|
||||
id = int(id)
|
||||
if id > 1000000000:
|
||||
qid, dfid = divmod(id, 1000000000)
|
||||
if dfid >= 100:
|
||||
return qid, dfid - 100, None
|
||||
else:
|
||||
return qid, None, QuestionnaireChild.SYSTEM_DATAFIELD_NUMBERS[dfid]
|
||||
else:
|
||||
return None, id, None
|
||||
|
||||
|
||||
class Quota(LoggedModel):
|
||||
"""
|
||||
A quota is a "pool of tickets". It is there to limit the number of items
|
||||
|
||||
@@ -95,9 +95,7 @@ from ._transactions import (
|
||||
)
|
||||
from .base import LockModel, LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
from .items import (
|
||||
Item, ItemVariation, Question, QuestionnaireChild, QuestionOption, Quota,
|
||||
)
|
||||
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -920,7 +918,7 @@ class Order(LockModel, LoggedModel):
|
||||
positions = list(
|
||||
self.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||
).select_related('item').prefetch_related('item__questionnaires')
|
||||
).select_related('item').prefetch_related('item__questions')
|
||||
)
|
||||
if not self.event.settings.allow_modifications_after_checkin:
|
||||
for cp in positions:
|
||||
@@ -931,7 +929,7 @@ class Order(LockModel, LoggedModel):
|
||||
return True
|
||||
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||
for cp in positions:
|
||||
if (cp.item.ask_attendee_data and ask_names) or cp.item.questionnaires.all(): # TODO(questionnaires) : filter to only QuestionnaireType.ORDER_POSITION_SALE
|
||||
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
|
||||
return True
|
||||
|
||||
return False # nothing there to modify
|
||||
@@ -1451,12 +1449,6 @@ class QuestionAnswer(models.Model):
|
||||
else:
|
||||
return self.answer
|
||||
|
||||
def to_dependency_values(self):
|
||||
if self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
return [o.identifier for o in self.options.all()]
|
||||
elif self.question.type in (Question.TYPE_BOOLEAN, Question.TYPE_COUNTRYCODE):
|
||||
return self.answer
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.orderposition and self.cartposition:
|
||||
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
||||
@@ -1599,92 +1591,55 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
def meta_info_data(self, d):
|
||||
self.meta_info = json.dumps(d)
|
||||
|
||||
def cache_answers(self, questionnaire_type, sales_channel):
|
||||
def cache_answers(self, all=True):
|
||||
"""
|
||||
Creates a new property on the object:
|
||||
questions: a list of Question objects, extended by an 'answer' property
|
||||
Creates two properties on the object.
|
||||
(1) answ: a dictionary of question.id → answer string
|
||||
(2) questions: a list of Question objects, extended by an 'answer' property
|
||||
"""
|
||||
self.answ = {}
|
||||
for a in getattr(self, 'answerlist', self.answers.all()): # use prefetch_related cache from get_cart
|
||||
self.answ[a.question_id] = a
|
||||
|
||||
# We need to clone our question objects, otherwise we will override the cached
|
||||
# answers of other items in the same cart if the question objects have been
|
||||
# selected via prefetch_related
|
||||
if hasattr(self.item, 'relevant_questionnaires'):
|
||||
children = list(copy.copy(qc) for qq in self.item.relevant_questionnaires for qc in qq.childlist)
|
||||
if not all:
|
||||
if hasattr(self.item, 'questions_to_ask'):
|
||||
questions = list(copy.copy(q) for q in self.item.questions_to_ask)
|
||||
else:
|
||||
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False,
|
||||
hidden=False))
|
||||
else:
|
||||
children = QuestionnaireChild.objects.filter(questionnaire__event_id=self.item.event_id, questionnaire__items__in=[self.item.id])
|
||||
if questionnaire_type is not None:
|
||||
children = children.filter(questionnaire__type=questionnaire_type)
|
||||
if sales_channel is not None:
|
||||
children = children.filter(Q(questionnaire__all_sales_channels=True) | Q(questionnaire__limit_sales_channels=sales_channel))
|
||||
questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||
|
||||
children = list(children)
|
||||
|
||||
qc_cache = {
|
||||
q.pk: q for q in children
|
||||
question_cache = {
|
||||
q.pk: q for q in questions
|
||||
}
|
||||
|
||||
def qc_is_visible(parentid, qvals):
|
||||
if parentid not in qc_cache:
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in question_cache:
|
||||
return False
|
||||
parentqc = qc_cache[parentid]
|
||||
if parentqc.dependency_question_id and not qc_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||
return False
|
||||
answer_values = self.get_dependency_answer_values(parentqc)
|
||||
return answer_values and any(qval in answer_values for qval in qvals)
|
||||
if parentid not in self.answ:
|
||||
return False
|
||||
return (
|
||||
('True' in qvals and self.answ[parentid].answer == 'True')
|
||||
or ('False' in qvals and self.answ[parentid].answer == 'False')
|
||||
or (any(qval in [o.identifier for o in self.answ[parentid].options.all()] for qval in qvals))
|
||||
)
|
||||
|
||||
self.questions = []
|
||||
for qc in children:
|
||||
try:
|
||||
qc.answer = self.answer_cache[qc.user_datafield_id or qc.system_datafield]
|
||||
except KeyError:
|
||||
continue
|
||||
if not qc.dependency_question_id or qc_is_visible(qc.dependency_question_id, qc.dependency_values):
|
||||
self.questions.append(qc)
|
||||
return self.questions
|
||||
|
||||
@cached_property
|
||||
def answer_cache(self):
|
||||
return {
|
||||
"attendee_name_parts": self.attendee_name,
|
||||
"attendee_email": self.attendee_email,
|
||||
"company": self.company,
|
||||
"street": self.street,
|
||||
"zipcode": self.zipcode,
|
||||
"city": self.city,
|
||||
"state": self.state,
|
||||
"country": str(self.country),
|
||||
**{aw.question_id: aw for aw in getattr(self, 'answerlist', self.answers.all())},
|
||||
}
|
||||
|
||||
def get_dependency_answer_values(self, qc):
|
||||
if qc.user_datafield_id:
|
||||
if qc.user_datafield_id not in self.answer_cache:
|
||||
return None
|
||||
answer = self.answer_cache[qc.user_datafield_id]
|
||||
return answer.to_dependency_values()
|
||||
elif qc.system_datafield:
|
||||
return [self.get_system_answer(qc.system_datafield)]
|
||||
else:
|
||||
raise ValueError('Questionnaire child without datafield has no answer')
|
||||
|
||||
def get_system_answer(self, system_datafield_name):
|
||||
if system_datafield_name == 'attendee_name_parts':
|
||||
return self.attendee_name
|
||||
elif system_datafield_name == 'attendee_email':
|
||||
return self.attendee_email
|
||||
elif system_datafield_name == 'company':
|
||||
return self.company
|
||||
elif system_datafield_name == 'street':
|
||||
return self.street
|
||||
elif system_datafield_name == 'zipcode':
|
||||
return self.zipcode
|
||||
elif system_datafield_name == 'city':
|
||||
return self.city
|
||||
elif system_datafield_name == 'state':
|
||||
return self.state
|
||||
elif system_datafield_name == 'country':
|
||||
return str(self.country)
|
||||
else:
|
||||
raise ValueError('Unknown system question name')
|
||||
for q in questions:
|
||||
if q.id in self.answ:
|
||||
q.answer = self.answ[q.id]
|
||||
q.answer.question = q # cache object
|
||||
else:
|
||||
q.answer = ""
|
||||
if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values):
|
||||
self.questions.append(q)
|
||||
|
||||
@property
|
||||
def net_price(self):
|
||||
@@ -1697,21 +1652,20 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
else self.variation.quotas.filter(subevent=self.subevent))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
def ensure_updated(fieldname):
|
||||
if 'update_fields' in kwargs:
|
||||
kwargs['update_fields'] = {fieldname}.union(kwargs['update_fields'])
|
||||
|
||||
if 'attendee_name_parts' in kwargs.get('update_fields', set()):
|
||||
ensure_updated('attendee_name_cached')
|
||||
update_fields = kwargs.get('update_fields', set())
|
||||
if 'attendee_name_parts' in update_fields:
|
||||
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields'])
|
||||
|
||||
name = self.attendee_name
|
||||
if name != self.attendee_name_cached:
|
||||
self.attendee_name_cached = name
|
||||
ensure_updated('attendee_name_cached')
|
||||
if 'update_fields' in kwargs:
|
||||
kwargs['update_fields'] = {'attendee_name_cached'}.union(kwargs['update_fields'])
|
||||
|
||||
if self.attendee_name_parts is None:
|
||||
self.attendee_name_parts = {}
|
||||
ensure_updated('attendee_name_parts')
|
||||
if 'update_fields' in kwargs:
|
||||
kwargs['update_fields'] = {'attendee_name_parts'}.union(kwargs['update_fields'])
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@@ -2787,7 +2741,7 @@ class OrderPosition(AbstractPosition):
|
||||
positions = list(
|
||||
self.order.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||
).select_related('item').prefetch_related('item__questionnaires')
|
||||
).select_related('item').prefetch_related('item__questions')
|
||||
)
|
||||
if not self.event.settings.allow_modifications_after_checkin:
|
||||
for cp in positions:
|
||||
@@ -2797,7 +2751,7 @@ class OrderPosition(AbstractPosition):
|
||||
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||
for cp in positions:
|
||||
if cp.pk == self.pk or cp.addon_to_id == self.pk:
|
||||
if (cp.item.ask_attendee_data and ask_names) or cp.item.questionnaires.all(): # TODO(questionnaires) : filter to only QuestionnaireType.ORDER_POSITION_SALE
|
||||
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
|
||||
return True
|
||||
|
||||
return False # nothing there to modify
|
||||
|
||||
@@ -42,7 +42,7 @@ from django.conf import settings
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -312,6 +312,25 @@ class Organizer(LoggedModel):
|
||||
)
|
||||
i += 1
|
||||
|
||||
def get_users_with_permission(self, permission):
|
||||
"""
|
||||
Returns a queryset of users who have a specific permission to this organizer.
|
||||
|
||||
:return: Iterable of User
|
||||
"""
|
||||
from .auth import User
|
||||
|
||||
if permission:
|
||||
qs = Team.objects.with_organizer_permission(permission)
|
||||
else:
|
||||
qs = Team.objects.all()
|
||||
|
||||
team_with_perm = qs.filter(
|
||||
members__pk=OuterRef('pk'),
|
||||
organizer=self,
|
||||
)
|
||||
return User.objects.filter(Exists(team_with_perm))
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
import logging
|
||||
from collections import OrderedDict, namedtuple
|
||||
from itertools import groupby
|
||||
from typing import Optional, Union
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.models import Event, LogEntry
|
||||
from pretix.base.models import Event, LogEntry, Organizer
|
||||
from pretix.base.signals import register_notification_types
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.urls import mainreverse_absolute
|
||||
@@ -57,7 +58,8 @@ class Notification:
|
||||
"""
|
||||
Represents a notification that is sent/shown to a user. A notification consists of:
|
||||
|
||||
* one ``event`` reference
|
||||
* one ``event`` reference (can be ``Ǹone``)
|
||||
* one ``organizer`` reference
|
||||
* one ``title`` text that is shown e.g. in the email subject or in a headline
|
||||
* optionally one ``detail`` text that may or may not be shown depending on the notification method
|
||||
* optionally one ``url`` that should be absolute and point to the context of an notification (e.g. an order)
|
||||
@@ -67,8 +69,12 @@ class Notification:
|
||||
each consisting of a button label and an absolute URL to point to.
|
||||
"""
|
||||
|
||||
def __init__(self, event: Event, title: str, detail: str=None, url: str=None):
|
||||
def __init__(self, event: Optional[Event], title: str, detail: Optional[str]=None, url: Optional[str]=None, organizer: Optional[Organizer]=None):
|
||||
assert event or organizer, "You need to pass either event or organizer"
|
||||
if event and organizer:
|
||||
assert event.organizer_id == organizer.id, "Organizer and event mismatch"
|
||||
self.title = title
|
||||
self.organizer = organizer or event.organizer
|
||||
self.event = event
|
||||
self.detail = detail
|
||||
self.url = url
|
||||
@@ -91,8 +97,16 @@ class Notification:
|
||||
|
||||
|
||||
class NotificationType:
|
||||
def __init__(self, event: Event = None):
|
||||
self.event = event
|
||||
def __init__(self, event_or_organizer: Union[Event, Organizer] = None):
|
||||
if isinstance(event_or_organizer, Event):
|
||||
self.event = event_or_organizer
|
||||
self.organizer = event_or_organizer.organizer
|
||||
elif isinstance(event_or_organizer, Organizer):
|
||||
self.event = None
|
||||
self.organizer = event_or_organizer
|
||||
else:
|
||||
self.event = None
|
||||
self.organizer = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<NotificationType: {}>'.format(self.action_type)
|
||||
@@ -121,31 +135,39 @@ class NotificationType:
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def is_event_level(self) -> bool:
|
||||
"""
|
||||
Return `True` if this notification type can be configured per event (the default).
|
||||
"""
|
||||
return True
|
||||
|
||||
def build_notification(self, logentry: LogEntry) -> Notification:
|
||||
"""
|
||||
This is the main function that you should override. It is supposed to turn a log entry
|
||||
object into a notification object that can then be rendered e.g. into an email.
|
||||
"""
|
||||
return Notification(
|
||||
logentry.event,
|
||||
logentry.display()
|
||||
event=logentry.event,
|
||||
title=logentry.display(),
|
||||
organizer=logentry.organizer,
|
||||
)
|
||||
|
||||
|
||||
def get_all_notification_types(event=None):
|
||||
def get_all_notification_types(event_or_organizer=None):
|
||||
global _ALL_TYPES
|
||||
|
||||
if event is None and _ALL_TYPES:
|
||||
if event_or_organizer is None and _ALL_TYPES:
|
||||
return _ALL_TYPES
|
||||
|
||||
types = OrderedDict()
|
||||
for recv, ret in register_notification_types.send(event):
|
||||
for recv, ret in register_notification_types.send(event_or_organizer):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types[r.action_type] = r
|
||||
else:
|
||||
types[ret.action_type] = ret
|
||||
if event is None:
|
||||
if event_or_organizer is None:
|
||||
_ALL_TYPES = types
|
||||
return types
|
||||
|
||||
@@ -181,6 +203,7 @@ class ParametrizedOrderNotificationType(NotificationType):
|
||||
|
||||
n = Notification(
|
||||
event=logentry.event,
|
||||
organizer=logentry.organizer,
|
||||
title=self._title.format(order=order, event=logentry.event),
|
||||
url=order_url
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.money import DecimalTextInput
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
from pretix.presale.views import get_cart_positions
|
||||
from pretix.presale.views import get_cart
|
||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1165,7 +1165,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||
from .services.cart import get_fees
|
||||
|
||||
cart = get_cart_positions(request)
|
||||
cart = get_cart(request)
|
||||
|
||||
try:
|
||||
fees = get_fees(event=request.event, request=request,
|
||||
@@ -1433,7 +1433,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
for p in cs.get('payments', [])
|
||||
if p.get('info_data', {}).get('gift_card')
|
||||
]
|
||||
positions = get_cart_positions(request)
|
||||
positions = get_cart(request)
|
||||
testmode = self.event.testmode
|
||||
else:
|
||||
used_cards = []
|
||||
|
||||
@@ -1410,7 +1410,6 @@ class CartManager:
|
||||
for k, v in self._widget_data.items():
|
||||
if not k.startswith('question-'):
|
||||
continue
|
||||
# TODO(questionnaires) - switch to QuestionnaireChild
|
||||
q = cp.item.questions.filter(ask_during_checkin=False, identifier__iexact=k[9:]).first()
|
||||
if q:
|
||||
try:
|
||||
|
||||
@@ -55,7 +55,7 @@ from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
|
||||
OrderPosition, Questionnaire, QuestionnaireChild, QuestionOption,
|
||||
OrderPosition, QuestionOption,
|
||||
)
|
||||
from pretix.base.signals import checkin_created, periodic_task
|
||||
from pretix.helpers import OF_SELF
|
||||
@@ -897,14 +897,14 @@ def _save_answers(op, answers, given_answers):
|
||||
written = False
|
||||
for q, a in given_answers.items():
|
||||
if not a:
|
||||
if q.id in answers:
|
||||
if q in answers:
|
||||
written = True
|
||||
answers[q.id].delete()
|
||||
answers[q].delete()
|
||||
else:
|
||||
continue
|
||||
if isinstance(a, QuestionOption):
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
qa.answer = str(a.answer)
|
||||
qa.save()
|
||||
written = True
|
||||
@@ -913,8 +913,8 @@ def _save_answers(op, answers, given_answers):
|
||||
qa = _create_answer(question=q, answer=str(a.answer))
|
||||
qa.options.add(a)
|
||||
elif isinstance(a, list):
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
qa.answer = ", ".join([str(o) for o in a])
|
||||
qa.save()
|
||||
written = True
|
||||
@@ -923,8 +923,8 @@ def _save_answers(op, answers, given_answers):
|
||||
qa = _create_answer(question=q, answer=", ".join([str(o) for o in a]))
|
||||
qa.options.add(*a)
|
||||
elif isinstance(a, File):
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
else:
|
||||
qa = _create_answer(question=q, answer=str(a))
|
||||
qa.file.save(os.path.basename(a.name), a, save=False)
|
||||
@@ -932,8 +932,8 @@ def _save_answers(op, answers, given_answers):
|
||||
qa.save()
|
||||
written = True
|
||||
else:
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
qa.answer = str(a)
|
||||
qa.save()
|
||||
else:
|
||||
@@ -1033,19 +1033,17 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
|
||||
# Do this outside of transaction so it is saved even if the checkin fails for some other reason
|
||||
checkin_questions = list(
|
||||
QuestionnaireChild.objects.filter(
|
||||
questionnaire__event=clist.event, questionnaire__type=Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN,
|
||||
questionnaire__items__in=[op.item_id],
|
||||
).select_related('user_datafield', 'questionnaire')
|
||||
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
|
||||
)
|
||||
require_answers = []
|
||||
if type != Checkin.TYPE_EXIT and checkin_questions:
|
||||
answers = {a.question: a for a in op.answers.all()}
|
||||
for q in checkin_questions:
|
||||
if q.user_datafield not in given_answers and q.user_datafield_id not in op.answer_cache:
|
||||
if q not in given_answers and q not in answers:
|
||||
require_answers.append(q)
|
||||
|
||||
if not simulate:
|
||||
_save_answers(op, op.answer_cache, given_answers)
|
||||
_save_answers(op, answers, given_answers)
|
||||
|
||||
with conditional_atomic(not simulate):
|
||||
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||
|
||||
@@ -34,7 +34,9 @@ from pretix.base.models import (
|
||||
from pretix.base.notifications import Notification, get_all_notification_types
|
||||
from pretix.base.services.mail import mail_send_task
|
||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
||||
from pretix.base.signals import notification
|
||||
from pretix.base.signals import (
|
||||
event_notification_sent, organizer_notification_sent,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.celery import get_task_priority
|
||||
from pretix.helpers.urls import mainreverse_absolute
|
||||
@@ -47,40 +49,47 @@ def notify(logentry_ids: list):
|
||||
logentry_ids = [logentry_ids]
|
||||
|
||||
qs = LogEntry.all.select_related(
|
||||
'event', 'event__organizer'
|
||||
'event', 'event__organizer', 'organizer'
|
||||
).order_by(
|
||||
'action_type', 'event_id',
|
||||
'action_type', 'event_id', 'organizer_id',
|
||||
).filter(id__in=logentry_ids)
|
||||
|
||||
_event, _at, notify_specific, notify_global = None, None, None, None
|
||||
_event, _organizer, _at, notify_specific, notify_global = None, None, None, None, None
|
||||
for logentry in qs:
|
||||
if not logentry.event:
|
||||
break # Ignore, we only have event-related notifications right now
|
||||
if not logentry.event and not logentry.organizer:
|
||||
break # Ignore, we only have event- or organizer-related notifications right now
|
||||
|
||||
notification_type = logentry.notification_type
|
||||
|
||||
if not notification_type:
|
||||
break # No suitable plugin
|
||||
|
||||
if _event != logentry.event or _at != logentry.action_type or notify_global is None:
|
||||
if _event != logentry.event or _organizer != logentry.organizer or _at != logentry.action_type or notify_global is None:
|
||||
_event = logentry.event
|
||||
_organizer = logentry.organizer
|
||||
_at = logentry.action_type
|
||||
|
||||
event_or_organizer = logentry.event or logentry.organizer
|
||||
# All users that have the permission to get the notification
|
||||
users = logentry.event.get_users_with_permission(
|
||||
users = event_or_organizer.get_users_with_permission(
|
||||
notification_type.required_permission
|
||||
).filter(notifications_send=True, is_active=True)
|
||||
|
||||
if logentry.user:
|
||||
users = users.exclude(pk=logentry.user.pk)
|
||||
|
||||
# Get all notification settings, both specific to this event as well as global
|
||||
notify_specific = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event=logentry.event,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
if logentry.event:
|
||||
notify_specific = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event=logentry.event,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
else:
|
||||
notify_specific = {}
|
||||
notify_global = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
@@ -106,7 +115,10 @@ def notify(logentry_ids: list):
|
||||
priority=get_task_priority("notifications", logentry.organizer_id),
|
||||
)
|
||||
|
||||
notification.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
|
||||
if logentry.event:
|
||||
event_notification_sent.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
|
||||
elif logentry.organizer:
|
||||
organizer_notification_sent.send(logentry.organizer, logentry_id=logentry.id, notification_type=notification_type.action_type)
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, acks_late=True, max_retries=9, default_retry_delay=900)
|
||||
@@ -158,13 +170,20 @@ def send_notification_mail(notification: Notification, user: User):
|
||||
body_plain = tpl_plain.render(ctx)
|
||||
|
||||
guid = uuid.uuid4()
|
||||
settings_holder = notification.event or notification.organizer
|
||||
prefix = settings_holder.settings.mail_prefix
|
||||
if not prefix:
|
||||
if notification.event:
|
||||
prefix = notification.event.slug.upper()
|
||||
elif notification.organizer:
|
||||
prefix = notification.organizer.name
|
||||
m = OutgoingMail.objects.create(
|
||||
guid=guid,
|
||||
user=user,
|
||||
to=[user.email],
|
||||
subject='[{}] {}: {}'.format(
|
||||
settings.PRETIX_INSTANCE_NAME,
|
||||
notification.event.settings.mail_prefix or notification.event.slug.upper(),
|
||||
prefix,
|
||||
notification.title
|
||||
),
|
||||
body_plain=body_plain,
|
||||
|
||||
@@ -2474,7 +2474,11 @@ class OrderChangeManager:
|
||||
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
|
||||
'fee': fee.pk,
|
||||
'old_price': fee.value,
|
||||
'new_price': op.value.gross
|
||||
'new_price': op.value.gross,
|
||||
'old_tax_rate': fee.tax_rate,
|
||||
'new_tax_rate': op.value.rate,
|
||||
'old_tax_value': fee.tax_value,
|
||||
'new_tax_value': op.value.tax,
|
||||
})
|
||||
fee.value = op.value.gross
|
||||
fee._calculate_tax()
|
||||
@@ -2484,9 +2488,13 @@ class OrderChangeManager:
|
||||
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'old_price': position.price,
|
||||
'addon_to': position.addon_to_id,
|
||||
'new_price': op.price.gross
|
||||
'old_price': position.price,
|
||||
'new_price': op.price.gross,
|
||||
'old_tax_rate': position.tax_rate,
|
||||
'new_tax_rate': op.price.rate,
|
||||
'old_tax_value': position.tax_value,
|
||||
'new_tax_value': op.price.tax,
|
||||
})
|
||||
position.price = op.price.gross
|
||||
position.price_includes_rounding_correction = Decimal("0.00")
|
||||
@@ -2501,25 +2509,43 @@ class OrderChangeManager:
|
||||
elif isinstance(op, self.TaxRuleOperation):
|
||||
if isinstance(op.position, OrderPosition):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
|
||||
log = {
|
||||
'position': position.pk,
|
||||
'positionid': position.positionid,
|
||||
'addon_to': position.addon_to_id,
|
||||
'old_taxrule': position.tax_rule.pk if position.tax_rule else None,
|
||||
'new_taxrule': op.tax_rule.pk
|
||||
})
|
||||
'new_taxrule': op.tax_rule.pk,
|
||||
'old_price': position.price,
|
||||
'old_tax_rate': position.tax_rate,
|
||||
'old_tax_value': position.tax_value,
|
||||
}
|
||||
position._calculate_tax(op.tax_rule)
|
||||
position.save()
|
||||
log.update({
|
||||
'new_price': position.price,
|
||||
'new_tax_rate': position.tax_rate,
|
||||
'new_tax_value': position.tax_value,
|
||||
})
|
||||
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data=log)
|
||||
elif isinstance(op.position, OrderFee):
|
||||
fee = fee_cache.setdefault(op.position.pk, op.position)
|
||||
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
|
||||
log = {
|
||||
'fee': fee.pk,
|
||||
'fee_type': fee.fee_type,
|
||||
'old_taxrule': fee.tax_rule.pk if fee.tax_rule else None,
|
||||
'new_taxrule': op.tax_rule.pk
|
||||
})
|
||||
'new_taxrule': op.tax_rule.pk,
|
||||
'old_price': fee.value,
|
||||
'old_tax_rate': fee.tax_rate,
|
||||
'old_tax_value': fee.tax_value,
|
||||
}
|
||||
fee._calculate_tax(op.tax_rule)
|
||||
fee.save()
|
||||
log.update({
|
||||
'new_price': fee.value,
|
||||
'new_tax_rate': fee.tax_rate,
|
||||
'new_tax_value': fee.tax_value,
|
||||
})
|
||||
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data=log)
|
||||
elif isinstance(op, self.CancelFeeOperation):
|
||||
fee = fee_cache.setdefault(op.fee.pk, op.fee)
|
||||
self.order.log_action('pretix.event.order.changed.cancelfee', user=self.user, auth=self.auth, data={
|
||||
|
||||
+101
-14
@@ -379,12 +379,99 @@ DEFAULTS = {
|
||||
|
||||
)
|
||||
},
|
||||
# 'system_question_order': { # TODO(questionnaires) - remove this
|
||||
# 'default': {},
|
||||
# 'type': dict,
|
||||
# 'serializer_class': serializers.DictField,
|
||||
# 'serializer_kwargs': lambda: dict(read_only=True, allow_empty=True),
|
||||
# },
|
||||
'system_question_order': {
|
||||
'default': {},
|
||||
'type': dict,
|
||||
'serializer_class': serializers.DictField,
|
||||
'serializer_kwargs': lambda: dict(read_only=True, allow_empty=True),
|
||||
},
|
||||
'attendee_names_asked': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for attendee names"),
|
||||
help_text=_("Ask for a name for all personalized tickets."),
|
||||
)
|
||||
},
|
||||
'attendee_names_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require attendee names"),
|
||||
help_text=_("Require customers to fill in the names of all attendees."),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_names_asked'}),
|
||||
)
|
||||
},
|
||||
'attendee_emails_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for email addresses per ticket"),
|
||||
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
|
||||
"only to that email address. If you enable this option, the system will additionally ask for "
|
||||
"individual email addresses for every personalized ticket. This might be useful if you want to "
|
||||
"obtain individual addresses for every attendee even in case of group orders. However, "
|
||||
"pretix will send the order confirmation by default only to the one primary email address, not to "
|
||||
"the per-attendee addresses. You can however enable this in the email settings."),
|
||||
)
|
||||
},
|
||||
'attendee_emails_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require email addresses per ticket"),
|
||||
help_text=_("Require customers to fill in individual email addresses for all personalized tickets. See the "
|
||||
"above option for more details. One email address for the order confirmation will always be "
|
||||
"required regardless of this setting."),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
|
||||
)
|
||||
},
|
||||
'attendee_company_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for company per ticket"),
|
||||
)
|
||||
},
|
||||
'attendee_company_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require company per ticket"),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_company_asked'}),
|
||||
)
|
||||
},
|
||||
'attendee_addresses_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for postal addresses per ticket"),
|
||||
)
|
||||
},
|
||||
'attendee_addresses_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require postal addresses per ticket"),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_addresses_asked'}),
|
||||
)
|
||||
},
|
||||
'order_email_asked_twice': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -4085,14 +4172,14 @@ def validate_event_settings(event, settings_dict):
|
||||
raise ValidationError({
|
||||
'locale': _('Your default locale must also be enabled for your event (see box above).')
|
||||
})
|
||||
# if settings_dict.get('attendee_names_required') and not settings_dict.get('attendee_names_asked'):
|
||||
# raise ValidationError({
|
||||
# 'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
|
||||
# })
|
||||
# if settings_dict.get('attendee_emails_required') and not settings_dict.get('attendee_emails_asked'):
|
||||
# raise ValidationError({
|
||||
# 'attendee_emails_required': _('You have to ask for attendee emails if you want to make them required.')
|
||||
# })
|
||||
if settings_dict.get('attendee_names_required') and not settings_dict.get('attendee_names_asked'):
|
||||
raise ValidationError({
|
||||
'attendee_names_required': _('You cannot require specifying attendee names if you do not ask for them.')
|
||||
})
|
||||
if settings_dict.get('attendee_emails_required') and not settings_dict.get('attendee_emails_asked'):
|
||||
raise ValidationError({
|
||||
'attendee_emails_required': _('You have to ask for attendee emails if you want to make them required.')
|
||||
})
|
||||
if settings_dict.get('invoice_address_required') and not settings_dict.get('invoice_address_asked'):
|
||||
raise ValidationError({
|
||||
'invoice_address_required': _('You have to ask for invoice addresses if you want to make them required.')
|
||||
|
||||
@@ -605,15 +605,14 @@ subclass of pretix.base.ticketoutput.BaseTicketOutput
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_notification_types = EventPluginSignal()
|
||||
register_notification_types = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known notification types. Receivers should return an
|
||||
instance of a subclass of pretix.base.notifications.NotificationType or a list of such
|
||||
instances.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event,
|
||||
however for this signal, the ``sender`` **may also be None** to allow creating the general
|
||||
notification settings!
|
||||
When called for actually sending notifications, ``sender`` will be the event or organizer,
|
||||
depending on context.
|
||||
"""
|
||||
|
||||
register_event_permission_groups = GlobalSignal()
|
||||
@@ -628,7 +627,7 @@ This signal is sent out to get all known permissions. Receivers should return an
|
||||
instance of pretix.base.permissions.PermissionGroup or a list of such instances.
|
||||
"""
|
||||
|
||||
notification = EventPluginSignal()
|
||||
event_notification_sent = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``logentry_id``, ``notification_type``
|
||||
|
||||
@@ -637,6 +636,15 @@ This signal is sent out when a notification is sent.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
organizer_notification_sent = OrganizerPluginSignal()
|
||||
"""
|
||||
Arguments: ``logentry_id``, ``notification_type``
|
||||
|
||||
This signal is sent out when a notification is sent.
|
||||
|
||||
As with all organizer-plugin signals, the ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
register_sales_channel_types = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known sales channels types. Receivers should return an
|
||||
|
||||
@@ -27,7 +27,7 @@ from decimal import Decimal
|
||||
from django import forms
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch, Q, QuerySet
|
||||
from django.db.models import Prefetch, QuerySet
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
@@ -37,7 +37,7 @@ from pretix.base.forms.questions import (
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer,
|
||||
Questionnaire, QuestionnaireChild, QuestionOption,
|
||||
QuestionOption,
|
||||
)
|
||||
from pretix.base.models.customers import AttendeeProfile
|
||||
from pretix.base.models.orders import CheckoutSession, Order
|
||||
@@ -119,8 +119,8 @@ class BaseQuestionsViewMixin:
|
||||
override_sets = self.get_question_override_sets(cr, idx)
|
||||
for overrides in override_sets:
|
||||
for question_name, question_field in form.fields.items():
|
||||
if hasattr(question_field, 'datafield'):
|
||||
src = overrides.get(question_field.datafield.identifier)
|
||||
if hasattr(question_field, 'question'):
|
||||
src = overrides.get(question_field.question.identifier)
|
||||
else:
|
||||
src = overrides.get(question_name)
|
||||
if not src:
|
||||
@@ -184,7 +184,7 @@ class BaseQuestionsViewMixin:
|
||||
field, v,
|
||||
checkoutsession=checkoutsession,
|
||||
order=order,
|
||||
question=field.datafield,
|
||||
question=field.question,
|
||||
)
|
||||
|
||||
for form in self.forms:
|
||||
@@ -246,7 +246,7 @@ class BaseQuestionsViewMixin:
|
||||
field, v,
|
||||
cartposition=cartposition,
|
||||
orderposition=orderposition,
|
||||
question=field.datafield,
|
||||
question=field.question,
|
||||
)
|
||||
|
||||
answer_dict = self._build_answer_dict(field, answer, k)
|
||||
@@ -331,8 +331,8 @@ class BaseQuestionsViewMixin:
|
||||
'field_name': k,
|
||||
'field_label': str(field.label),
|
||||
'value': answer_value,
|
||||
'question_type': field.datafield.type,
|
||||
'question_identifier': field.datafield.identifier,
|
||||
'question_type': field.question.type,
|
||||
'question_identifier': field.question.identifier,
|
||||
}
|
||||
|
||||
|
||||
@@ -348,37 +348,27 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
qqs = self.request.event.questionnaires.all()
|
||||
qqs = self.request.event.questions.all()
|
||||
if self.only_user_visible:
|
||||
qqs = qqs.filter(type=Questionnaire.QuestionnaireType.ORDER_POSITION_SALE)
|
||||
else:
|
||||
qqs = qqs.filter(type__startswith='P')
|
||||
qqs = qqs.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.order.sales_channel.identifier)
|
||||
)
|
||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False, container_type=Question.ContainerType.ORDERPOSITION)
|
||||
return list(self.order.positions.select_related(
|
||||
'item', 'variation'
|
||||
).prefetch_related(
|
||||
Prefetch('answers',
|
||||
QuestionAnswer.objects.prefetch_related('options'),
|
||||
to_attr='answerlist'),
|
||||
Prefetch('item__questionnaires',
|
||||
Prefetch('item__questions',
|
||||
qqs.prefetch_related(
|
||||
Prefetch('children', QuestionnaireChild.objects.prefetch_related(
|
||||
Prefetch('user_datafield', Question.objects.prefetch_related(
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
))
|
||||
),
|
||||
to_attr='childlist')
|
||||
),
|
||||
to_attr='relevant_questionnaires')
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
))
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -26,7 +26,6 @@ from django.core import signing
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,7 +60,6 @@ def redir_view(request):
|
||||
u = urllib.parse.urlparse(url)
|
||||
return render(request, 'pretixbase/redirect.html', {
|
||||
'hostname': u.hostname,
|
||||
'bold_hostname': format_html("<strong>{}</strong>", u.hostname),
|
||||
'url': url,
|
||||
})
|
||||
|
||||
|
||||
@@ -639,6 +639,14 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
||||
'event_calendar_future_only',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_data_explanation_text',
|
||||
'order_phone_asked',
|
||||
'order_phone_required',
|
||||
|
||||
@@ -60,13 +60,12 @@ from pretix.base.models import (
|
||||
Item, ItemCategory, ItemProgramTime, ItemVariation, Question,
|
||||
QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import (
|
||||
ItemAddOn, ItemBundle, ItemMetaValue, Questionnaire,
|
||||
)
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||
from pretix.base.signals import item_copy_data
|
||||
from pretix.control.forms import (
|
||||
ButtonGroupRadioSelect, ExtFileField, SalesChannelCheckboxSelectMultiple,
|
||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField,
|
||||
SalesChannelCheckboxSelectMultiple, SplitDateTimeField,
|
||||
SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -154,10 +153,53 @@ class QuestionForm(I18nModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.container_type != Question.ContainerType.ORDERPOSITION:
|
||||
if self.instance.container_type == Question.ContainerType.ORDERPOSITION:
|
||||
self.fields['items'].queryset = self.instance.event.items.all()
|
||||
self.fields['items'].required = True
|
||||
else:
|
||||
del self.fields['items']
|
||||
del self.fields['ask_during_checkin']
|
||||
del self.fields['show_during_checkin']
|
||||
del self.fields['print_on_invoice']
|
||||
self.fields['dependency_question'].widget.attrs['data-container-type'] = self.instance.container_type
|
||||
self.fields['dependency_question'].queryset = self.instance.event.questions.filter(
|
||||
type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE),
|
||||
ask_during_checkin=False,
|
||||
container_type=self.instance.container_type,
|
||||
)
|
||||
if self.instance.pk:
|
||||
self.fields['dependency_question'].queryset = self.fields['dependency_question'].queryset.exclude(
|
||||
pk=self.instance.pk
|
||||
)
|
||||
self.fields['identifier'].required = False
|
||||
self.fields['dependency_values'].required = False
|
||||
self.fields['help_text'].widget.attrs['rows'] = 3
|
||||
|
||||
def clean_dependency_values(self):
|
||||
val = self.data.getlist('dependency_values')
|
||||
return val
|
||||
|
||||
def clean_dependency_question(self):
|
||||
dep = val = self.cleaned_data.get('dependency_question')
|
||||
if dep:
|
||||
if dep.ask_during_checkin:
|
||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
||||
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
return val
|
||||
|
||||
def clean_ask_during_checkin(self):
|
||||
val = self.cleaned_data.get('ask_during_checkin')
|
||||
|
||||
if val and self.cleaned_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
return val
|
||||
|
||||
def clean_show_during_checkin(self):
|
||||
val = self.cleaned_data.get('show_during_checkin')
|
||||
@@ -191,10 +233,16 @@ class QuestionForm(I18nModelForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'question',
|
||||
'help_text',
|
||||
'type',
|
||||
'required',
|
||||
'ask_during_checkin',
|
||||
'show_during_checkin',
|
||||
'hidden',
|
||||
'identifier',
|
||||
'items',
|
||||
'dependency_question',
|
||||
'dependency_values',
|
||||
'print_on_invoice',
|
||||
'valid_number_min',
|
||||
'valid_number_max',
|
||||
@@ -211,11 +259,17 @@ class QuestionForm(I18nModelForm):
|
||||
'valid_datetime_max': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'valid_date_min': DatePickerWidget(),
|
||||
'valid_date_max': DatePickerWidget(),
|
||||
'items': forms.CheckboxSelectMultiple(
|
||||
attrs={'class': 'scrolling-multiple-choice'}
|
||||
),
|
||||
'dependency_values': forms.SelectMultiple,
|
||||
'help_text': I18nMarkdownTextarea,
|
||||
}
|
||||
field_classes = {
|
||||
'valid_datetime_min': SplitDateTimeField,
|
||||
'valid_datetime_max': SplitDateTimeField,
|
||||
'items': ItemMultipleChoiceField,
|
||||
'dependency_question': SafeModelChoiceField,
|
||||
}
|
||||
|
||||
|
||||
@@ -393,20 +447,13 @@ class ItemCreateForm(I18nModelForm):
|
||||
help_text=_('Select this option e.g. for t-shirts that come in multiple sizes. '
|
||||
'You can select the variations in the next step.'),
|
||||
required=False)
|
||||
questionnaires = SafeModelMultipleChoiceField(
|
||||
queryset=Questionnaire.objects.none(),
|
||||
required=False,
|
||||
label=_('Questionnaires'),
|
||||
widget=forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs['event']
|
||||
self.user = kwargs.pop('user')
|
||||
kwargs.setdefault('initial', {})
|
||||
kwargs['initial'].setdefault('admission', True)
|
||||
kwargs['initial'].setdefault('personalized', True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['category'].queryset = self.instance.event.categories.all()
|
||||
@@ -422,8 +469,6 @@ class ItemCreateForm(I18nModelForm):
|
||||
)
|
||||
self.fields['category'].widget.choices = self.fields['category'].choices
|
||||
|
||||
self.fields['questionnaires'].queryset = self.instance.event.questionnaires.all()
|
||||
|
||||
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
|
||||
change_decimal_field(self.fields['default_price'], self.instance.event.currency)
|
||||
self.fields['copy_from'] = forms.ModelChoiceField(
|
||||
@@ -530,15 +575,10 @@ class ItemCreateForm(I18nModelForm):
|
||||
self.instance.picture.save(os.path.basename(src.picture.name), src.picture)
|
||||
|
||||
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||
if not self.instance.admission:
|
||||
self.instance.personalized = False
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
instance.questionnaires.clear()
|
||||
instance.questionnaires.add(*self.cleaned_data['questionnaires'])
|
||||
try:
|
||||
Questionnaire.check_constraints(self.event)
|
||||
except ValidationError as exc:
|
||||
raise ValidationError({'questionnaires': exc})
|
||||
|
||||
if not self.event.has_subevents and not self.cleaned_data.get('has_variations'):
|
||||
if self.cleaned_data.get('quota_option') == self.EXISTING and self.cleaned_data.get('quota_add_existing') is not None:
|
||||
quota = self.cleaned_data.get('quota_add_existing')
|
||||
@@ -662,14 +702,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
required=False,
|
||||
)
|
||||
questionnaires = SafeModelMultipleChoiceField(
|
||||
queryset=Questionnaire.objects.none(),
|
||||
required=False,
|
||||
label=_('Questionnaires'),
|
||||
widget=forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -765,9 +797,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
)
|
||||
self.fields['category'].widget.choices = self.fields['category'].choices
|
||||
|
||||
self.fields['questionnaires'].queryset = self.instance.event.questionnaires.all()
|
||||
self.initial['questionnaires'] = self.instance.questionnaires.values_list('pk', flat=True)
|
||||
|
||||
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
|
||||
|
||||
self.fields['validity_dynamic_start_choice'] = forms.TypedChoiceField(
|
||||
@@ -862,16 +891,6 @@ class ItemUpdateForm(I18nModelForm):
|
||||
|
||||
return d
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
instance.questionnaires.clear()
|
||||
instance.questionnaires.add(*self.cleaned_data['questionnaires'])
|
||||
try:
|
||||
Questionnaire.check_constraints(self.event)
|
||||
except ValidationError as exc:
|
||||
raise ValidationError({'questionnaires': exc})
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
localized_fields = '__all__'
|
||||
@@ -883,6 +902,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'all_sales_channels',
|
||||
'limit_sales_channels',
|
||||
'admission',
|
||||
'personalized',
|
||||
'description',
|
||||
'picture',
|
||||
'default_price',
|
||||
@@ -1177,10 +1197,6 @@ class ItemAddOnForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['addon_category'].queryset = self.event.categories.all()
|
||||
self.fields['addon_category'].help_text = format_html('<a href="{}" data-iframe-dialog="true" data-iframe-dialog-target="<[name$=addon_category]" target="_blank">{}</a>', reverse('control:event.items.categories.add', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}), _("Create a new category"))
|
||||
self.fields['addon_category'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
|
||||
@@ -66,7 +66,7 @@ class ProcessForm(forms.Form):
|
||||
widget=forms.Select(
|
||||
attrs={'data-static': 'true'}
|
||||
),
|
||||
help_text=conditional_escape(c.help_text),
|
||||
help_text=conditional_escape(c.help_text or ""),
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
|
||||
@@ -364,7 +364,7 @@ class TeamForm(forms.ModelForm):
|
||||
for opt in pg.options
|
||||
],
|
||||
label=pg.label,
|
||||
help_text=conditional_escape(pg.help_text),
|
||||
help_text=conditional_escape(pg.help_text or ""),
|
||||
initial=initial,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
@@ -389,7 +389,7 @@ class TeamForm(forms.ModelForm):
|
||||
for opt in pg.options
|
||||
],
|
||||
label=pg.label,
|
||||
help_text=conditional_escape(pg.help_text),
|
||||
help_text=conditional_escape(pg.help_text or ""),
|
||||
initial=initial,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
@@ -866,9 +866,6 @@ class OrganizerPluginStateLogEntryType(LogEntryType):
|
||||
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
|
||||
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
|
||||
'pretix.event.question.option.changed': _('An answer option has been changed.'),
|
||||
'pretix.event.questionnaire.added': _('A questionnaire has been created.'),
|
||||
'pretix.event.questionnaire.deleted': _('A questionnaire has been deleted.'),
|
||||
'pretix.event.questionnaire.changed': _('A questionnaire has been changed.'),
|
||||
'pretix.event.permissions.added': _('A user has been added to the event team.'),
|
||||
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
|
||||
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
|
||||
|
||||
@@ -182,12 +182,12 @@ def get_event_navigation(request: HttpRequest):
|
||||
'active': 'event.items.categories' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Questionnaires'),
|
||||
'url': reverse('control:event.items.questionnaires', kwargs={
|
||||
'label': _('Questions'),
|
||||
'url': reverse('control:event.items.questions', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.questionnaires' in url.url_name or 'event.items.questions' in url.url_name,
|
||||
'active': 'event.items.questions' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Discounts'),
|
||||
|
||||
@@ -94,6 +94,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>{% trans "Attendee data (once per personalized ticket)" %}</h4>
|
||||
|
||||
{% bootstrap_field sform.attendee_names_asked_required layout="control" %}
|
||||
{% bootstrap_field sform.attendee_emails_asked_required layout="control" %}
|
||||
{% bootstrap_field sform.attendee_company_asked_required layout="control" %}
|
||||
{% bootstrap_field sform.attendee_addresses_asked_required layout="control" %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3">
|
||||
{% trans "Custom fields" %}
|
||||
@@ -102,27 +109,11 @@
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
target="_blank">
|
||||
{% trans "Manage questionnaires" %}
|
||||
{% trans "Manage questions" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>{% trans "Attendee data (once per personalized ticket)" %}</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3">
|
||||
</label>
|
||||
<div class="col-md-9 static-form-row">
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
target="_blank">
|
||||
{% trans "Manage questionnaires" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
|
||||
|
||||
<h4>{% trans "Form settings" %}</h4>
|
||||
|
||||
@@ -64,7 +64,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.questionnaires layout="control" %}
|
||||
<div class="form-group" data-display-dependency="#admission_on">
|
||||
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
|
||||
<div class="col-md-9">
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
|
||||
<span class="fa fa-fw fa-id-card-o"></span>
|
||||
<strong>{% trans "Personalized ticket" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
When this ticket is purchased, the system will ask for a name or other details according
|
||||
to your event settings.
|
||||
{% endblocktrans %}
|
||||
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
|
||||
<br>
|
||||
<span class="text-warning">
|
||||
<span class="fa fa-warning" aria-hidden="true"></span>
|
||||
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
|
||||
</span>
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
|
||||
<span class="fa fa-fw fa-circle-o"></span>
|
||||
<strong>{% trans "Non-personalized ticket" %}</strong>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
The system will not ask for a name or other attendee details. This only affects
|
||||
system-provided fields, you can still add your own questions.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.category layout="control" %}
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -72,7 +72,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.questionnaires layout="control" %}
|
||||
<div class="form-group" data-display-dependency="#admission_on">
|
||||
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
|
||||
<div class="col-md-9">
|
||||
{% for e in form.errors.personalized %}
|
||||
<div class="alert alert-danger has-error">
|
||||
{{ e }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
|
||||
<span class="fa fa-fw fa-id-card-o"></span>
|
||||
<strong>{% trans "Personalized ticket" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
When this ticket is purchased, the system will ask for a name or other details according
|
||||
to your event settings.
|
||||
{% endblocktrans %}
|
||||
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
|
||||
<br>
|
||||
<span class="text-warning">
|
||||
<span class="fa fa-warning" aria-hidden="true"></span>
|
||||
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
|
||||
</span>
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
|
||||
<span class="fa fa-fw fa-file-text-o"></span>
|
||||
<strong>{% trans "Non-personalized ticket" %}</strong>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
The system will not ask for a name or other attendee details. This only affects
|
||||
system-provided fields, you can still add your own questions.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.description layout="control" %}
|
||||
{% bootstrap_field form.picture layout="control" %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<h1>{% trans "Delete question" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete the data field <strong>{{ question }}</strong>?{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}Are you sure you want to delete the question <strong>{{ question }}</strong>?{% endblocktrans %}</p>
|
||||
{% if dependent|length > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
<p>{% blocktrans %}All answers to the question given by the buyers of the following products will be <strong>lost</strong>.{% endblocktrans %}
|
||||
|
||||
@@ -5,124 +5,158 @@
|
||||
{% load escapejson %}
|
||||
{% block title %}
|
||||
{% if question %}
|
||||
{% blocktrans with name=question.question %}Data field: {{ name }}{% endblocktrans %}
|
||||
{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Create new data field" %}
|
||||
{% trans "Question" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
{% if question %}
|
||||
<h1>{% blocktrans with name=question.question %}Data field: {{ name }}{% endblocktrans %}</h1>
|
||||
<h1>{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Create new data field" %}</h1>
|
||||
<h1>{% trans "Question" %}</h1>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
<div id="valid-number">
|
||||
{% bootstrap_field form.valid_number_min layout="control" %}
|
||||
{% bootstrap_field form.valid_number_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-date">
|
||||
{% bootstrap_field form.valid_date_min layout="control" %}
|
||||
{% bootstrap_field form.valid_date_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-datetime">
|
||||
{% bootstrap_field form.valid_datetime_min layout="control" %}
|
||||
{% bootstrap_field form.valid_datetime_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-string">
|
||||
{% bootstrap_field form.valid_string_length_min layout="control" %}
|
||||
{% bootstrap_field form.valid_string_length_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-file">
|
||||
{% bootstrap_field form.valid_file_portrait layout="control" %}
|
||||
</div>
|
||||
<div id="answer-options">
|
||||
<h3>{% trans "Answer options" %}</h3>
|
||||
<noscript>
|
||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||
</noscript>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% blocktrans trimmed with id=form.instance.identifier %}
|
||||
Answer option {{ id }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
{% if form.items %}
|
||||
{% bootstrap_field form.items layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
<div class="alert alert-info alert-required-boolean">
|
||||
{% blocktrans trimmed %}
|
||||
If you mark a Yes/No question as required, it means that the user has to select Yes and No is not
|
||||
accepted. If you want to allow both options, do not make this field required.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% trans "New answer option" %}
|
||||
</span>
|
||||
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
|
||||
<div id="valid-number">
|
||||
{% bootstrap_field form.valid_number_min layout="control" %}
|
||||
{% bootstrap_field form.valid_number_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-date">
|
||||
{% bootstrap_field form.valid_date_min layout="control" %}
|
||||
{% bootstrap_field form.valid_date_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-datetime">
|
||||
{% bootstrap_field form.valid_datetime_min layout="control" %}
|
||||
{% bootstrap_field form.valid_datetime_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-string">
|
||||
{% bootstrap_field form.valid_string_length_min layout="control" %}
|
||||
{% bootstrap_field form.valid_string_length_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-file">
|
||||
{% bootstrap_field form.valid_file_portrait layout="control" %}
|
||||
</div>
|
||||
<div id="answer-options">
|
||||
<h3>{% trans "Answer options" %}</h3>
|
||||
<noscript>
|
||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||
</noscript>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% blocktrans trimmed with id=form.instance.identifier %}
|
||||
Answer option {{ id }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% trans "New answer option" %}
|
||||
</span>
|
||||
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced" %}</legend>
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% if form.ask_during_checkin %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% if form.show_during_checkin %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% if form.print_on_invoice %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_dependency_question">
|
||||
{% trans "Question dependency" %}
|
||||
<br><span class="optional">{% trans "Optional" context "form" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.dependency_question layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<script type="text/plain" id="dependency_value_val">{{ form.instance.dependency_values|escapejson_dumps }}</script>
|
||||
{% bootstrap_field form.dependency_values layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% if form.show_during_checkin %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% if form.print_on_invoice %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load icon %}
|
||||
{% load compress %}
|
||||
{% load vite %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Questionnaires" %}
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% trans "Questionnaires" %}
|
||||
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default pull-right">
|
||||
{% icon "wrench" %} {% trans "Manage data fields" %}
|
||||
</a>
|
||||
</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Questionaires allow your attendees to fill in additional data about their ticket. If you provide food, one
|
||||
example might be to ask your users about dietary requirements.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
TODO(questionnaires) : add more specific explanation of the questionnaire concept.
|
||||
</p>
|
||||
|
||||
{{ request.event.settings.locales|json_script:"event_locales" }}
|
||||
{{ questionnaire_type_choices|json_script:"questionnaire_type_choices" }}
|
||||
{{ question_type_choices|json_script:"question_type_choices" }}
|
||||
{{ system_question_choices|json_script:"system_question_choices" }}
|
||||
|
||||
{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=0 as datafield_edit_url %}
|
||||
{{ datafield_edit_url|json_script:"datafield_edit_url" }}
|
||||
|
||||
{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=0 as datafield_view_url %}
|
||||
{{ datafield_view_url|json_script:"datafield_view_url" }}
|
||||
|
||||
{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug as datafield_create_url %}
|
||||
{{ datafield_create_url|json_script:"datafield_create_url" }}
|
||||
|
||||
<div id="questionnaires-editor">
|
||||
<!-- Vue app mount point -->
|
||||
</div>
|
||||
|
||||
{% vite_hmr %}
|
||||
{% vite_asset "src/pretix/static/pretixcontrol/js/ui/questionnaires/index.ts" %}
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Data fields" %}{% endblock %}
|
||||
{% block title %}{% trans "Questions" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Data fields" %}</h1>
|
||||
<h1>{% trans "Questions" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Questions allow your attendees to fill in additional data about their ticket. If you provide food, one
|
||||
@@ -11,40 +11,63 @@
|
||||
</p>
|
||||
{% csrf_token %}
|
||||
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new order-level data field" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new per-ticket data field" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if request.event.settings.feature_flag_order_level_questions %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new per-ticket question" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new order-level question" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% trans "Per-order data fields" %}</h2>
|
||||
<p>{% trans "These data fields can be used on orders." %}</p>
|
||||
<h2>{% trans "Per-ticket questions" %}</h2>
|
||||
<p>{% trans "These questions are asked for every ticket, so possibly multiple times in the same order." %}</p>
|
||||
{% else %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Internal name" %}</th>
|
||||
<th>{% trans "Question" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<th class="action-col-2"></th>
|
||||
{% endif %}
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in questions %}{% if q.container_type == "O" %}
|
||||
<tr>
|
||||
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P">
|
||||
{% for q in questions %}{% if q.container_type == "P" %}
|
||||
<tr data-dnd-id="{{ q.id }}">
|
||||
<td>
|
||||
<strong>
|
||||
{% if q.pk %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||
{% endif %}
|
||||
{{ q.question }}
|
||||
{% if q.pk %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</strong><br>
|
||||
<small class="text-muted">{{ q.identifier }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ q.get_type_display }}
|
||||
{% if q.pk %}
|
||||
{{ q.get_type_display }}
|
||||
{% else %}
|
||||
{% trans "System question" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.required %}
|
||||
@@ -61,8 +84,26 @@
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk %}
|
||||
<ul>
|
||||
{% for item in q.items.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<small>{% trans "All personalized products" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<td class="dnd-container">
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right flip">
|
||||
{% if q.pk %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
@@ -80,61 +121,85 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>{% trans "Per-ticket data fields" %}</h2>
|
||||
<p>{% trans "These data field can be used on individual tickets." %}</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Internal name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in questions %}{% if q.container_type == "P" %}
|
||||
{% if request.event.settings.feature_flag_order_level_questions %}
|
||||
<h2>
|
||||
{% trans "Per-order questions" %}
|
||||
<small><span class="label label-info" title="
|
||||
{% trans "This functionality is in active development and expected to change significantly over the coming months." %}
|
||||
{% trans "Per-order questions are currently not supported and will not be displayed in pretixPOS." %}
|
||||
" data-toggle="tooltip">
|
||||
<span class="fa fa-flask" aria-hidden="true"></span>
|
||||
{% trans "Experimental feature" %}
|
||||
</span></small>
|
||||
</h2>
|
||||
<p>{% trans "These questions are asked once per order." %}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||
{{ q.question }}
|
||||
</a>
|
||||
</strong><br>
|
||||
<small class="text-muted">{{ q.identifier }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ q.get_type_display }}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.required %}
|
||||
<span class="fa fa-exclamation-circle text-muted" data-toggle="tooltip" title="{% trans "Required question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk and q.ask_during_checkin %}
|
||||
<span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk and q.hidden %}
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<th>{% trans "Question" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<th class="action-col-2"></th>
|
||||
{% endif %}
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
{% endif %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</thead>
|
||||
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O">
|
||||
{% for q in questions %}{% if q.container_type == "O" %}
|
||||
<tr data-dnd-id="{{ q.id }}">
|
||||
<td>
|
||||
<strong>
|
||||
{{ q.question }}
|
||||
</strong><br>
|
||||
<small class="text-muted">{{ q.identifier }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk %}
|
||||
{{ q.get_type_display }}
|
||||
{% else %}
|
||||
{% trans "System question" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.required %}
|
||||
<span class="fa fa-exclamation-circle text-muted" data-toggle="tooltip" title="{% trans "Required question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk and q.ask_during_checkin %}
|
||||
<span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk and q.hidden %}
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<td class="dnd-container">
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right flip">
|
||||
{% if q.pk %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if 'event.settings.general:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% trans "Please wait..." %}</h1>
|
||||
{{ notify_info|json_script:"notify_info" }}
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/notify_parent.js" %}"></script>
|
||||
{% endblock %}
|
||||
@@ -20,10 +20,8 @@
|
||||
{% endif %}
|
||||
{% elif question.type == "M" %}
|
||||
{{ answer.to_string_i18n|rich_text_snippet }}
|
||||
{% elif question.type %}
|
||||
{{ answer.to_string_i18n|rich_text_snippet }}
|
||||
{% else %}
|
||||
{{ answer|linebreaksbr }}
|
||||
{{ answer.to_string_i18n|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
|
||||
@@ -610,9 +610,60 @@
|
||||
{% endif %}
|
||||
{% if line.has_questions %}
|
||||
<dl>
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
|
||||
<dt>{% trans "Attendee name" %}</dt>
|
||||
<dd>{% if line.attendee_name %}{{ line.attendee_name_all_components }}{% else %}
|
||||
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
|
||||
<dt>{% trans "Attendee email" %}</dt>
|
||||
<dd>
|
||||
{% if line.attendee_email %}
|
||||
{{ line.attendee_email }}
|
||||
{% if not line.addon_to %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
|
||||
class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee company" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee address" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.street or line.zipcode or line.city or line.country %}
|
||||
{{ line.street|default_if_none:""|linebreaksbr }}<br>
|
||||
{{ line.zipcode|default_if_none:"" }} {{ line.city|default_if_none:"" }}<br>
|
||||
{% if line.state %}{{ line.state_for_address }}<br>{% endif %}
|
||||
{{ line.country.name|default_if_none:"" }}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% for q in line.questions %}
|
||||
<dt>
|
||||
{{ q.label }}
|
||||
{{ q.question }}
|
||||
{% if q.ask_during_checkin %}
|
||||
<span class="fa fa-qrcode text-muted"
|
||||
data-toggle="tooltip"
|
||||
@@ -621,49 +672,7 @@
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd>
|
||||
|
||||
{# {% if q.answer %}#}
|
||||
{# {% if q.answer.file %}#}
|
||||
{# <span class="fa fa-file"></span>#}
|
||||
{# <a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">#}
|
||||
{# {{ q.answer.file_name }}#}
|
||||
{# </a>#}
|
||||
{# <span class="label label-danger" data-toggle="tooltip"#}
|
||||
{# title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">#}
|
||||
{# {% trans "UNSAFE" %}#}
|
||||
{# </span>#}
|
||||
{# {% if q.answer.is_image %}#}
|
||||
{# <br>#}
|
||||
{# <a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}" data-lightbox="order"#}
|
||||
{# class="answer-thumb">#}
|
||||
{# <img src="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">#}
|
||||
{# </a>#}
|
||||
{# {% endif %}#}
|
||||
{# {% elif q.type == "M" %}#}
|
||||
{# {{ q.answer.to_string_i18n|rich_text_snippet }}#}
|
||||
{# {% elif q.type %}#}
|
||||
{# {{ q.answer.to_string_i18n|linebreaksbr }}#}
|
||||
{# {% else %}<!-- TODO(questionnaires): proper separation of QuestionAnswer objects and system answers...... -->#}
|
||||
{# {{ q.answer|linebreaksbr }}#}
|
||||
{# {% endif %}#}
|
||||
{# {% else %}#}
|
||||
{# <em>{% trans "not answered" %}</em>#}
|
||||
{# {% endif %}#}
|
||||
|
||||
{% include "pretixcontrol/order/fragment_question_answer.html" with request=request question=q answer=q.answer %}
|
||||
{% if q.system_datafield == "attendee_email" and not line.addon_to %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
|
||||
class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% for q in line.additional_fields %}
|
||||
|
||||
@@ -343,6 +343,7 @@ urlpatterns = [
|
||||
name='event.items.categories.edit'),
|
||||
re_path(r'^categories/add$', item.CategoryCreate.as_view(), name='event.items.categories.add'),
|
||||
re_path(r'^questions/$', item.QuestionList.as_view(), name='event.items.questions'),
|
||||
re_path(r'^questions/reorder$', item.reorder_questions, name='event.items.questions.reorder'),
|
||||
re_path(r'^questions/(?P<question>\d+)/delete$', item.QuestionDelete.as_view(),
|
||||
name='event.items.questions.delete'),
|
||||
re_path(r'^questions/(?P<question>\d+)/$', item.QuestionView.as_view(),
|
||||
@@ -350,7 +351,6 @@ urlpatterns = [
|
||||
re_path(r'^questions/(?P<question>\d+)/change$', item.QuestionUpdate.as_view(),
|
||||
name='event.items.questions.edit'),
|
||||
re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
||||
re_path(r'^questionnaires/$', item.QuestionnairesEditor.as_view(), name='event.items.questionnaires'),
|
||||
re_path(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
|
||||
re_path(r'^quotas/bulk_action$', item.QuotaBulkAction.as_view(), name='event.items.quotas.bulkaction'),
|
||||
re_path(r'^quotas/bulk_edit$', item.QuotaBulkUpdateView.as_view(), name='event.items.quotas.bulkedit'),
|
||||
|
||||
@@ -22,12 +22,9 @@
|
||||
import collections.abc
|
||||
import warnings
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import (
|
||||
EmptyPage, PageNotAnInteger, UnorderedObjectListWarning,
|
||||
)
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import edit
|
||||
|
||||
@@ -41,21 +38,7 @@ class EventBasedFormMixin:
|
||||
return kwargs
|
||||
|
||||
|
||||
class TellParentFormMixin:
|
||||
|
||||
def form_valid(self, form):
|
||||
result = super().form_valid(form)
|
||||
if self.request.GET.get('notify_parent') and isinstance(result, HttpResponseRedirect):
|
||||
message_store = messages.get_messages(self.request)
|
||||
msgs = [{'level': msg.level_tag, 'message': msg.message} for msg in message_store]
|
||||
return render(self.request, 'pretixcontrol/notify_parent.html', {
|
||||
'notify_info': {'object': self.object.pk, 'object_str': str(self.object), 'redirect_url': result.url, 'messages': msgs},
|
||||
})
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
class CreateView(EventBasedFormMixin, TellParentFormMixin, edit.CreateView):
|
||||
class CreateView(EventBasedFormMixin, edit.CreateView):
|
||||
"""
|
||||
Like Django's default CreateView, but passes the optional event
|
||||
argument to the form. This is necessary for I18nModelForms to work
|
||||
@@ -64,7 +47,7 @@ class CreateView(EventBasedFormMixin, TellParentFormMixin, edit.CreateView):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateView(EventBasedFormMixin, TellParentFormMixin, edit.UpdateView):
|
||||
class UpdateView(EventBasedFormMixin, edit.UpdateView):
|
||||
"""
|
||||
Like Django's default UpdateView, but passes the optional event
|
||||
argument to the form. This is necessary for I18nModelForms to work
|
||||
|
||||
@@ -1670,7 +1670,6 @@ class QuickSetupView(EventPermissionRequiredMixin, FormView):
|
||||
user=self.request.user)
|
||||
|
||||
subevent = self.request.event.subevents.first()
|
||||
questionnaire = self.request.event.questionnaires.first()
|
||||
for i, f in enumerate(self.formset):
|
||||
if f in self.formset.deleted_forms or not f.has_changed():
|
||||
continue
|
||||
@@ -1686,8 +1685,6 @@ class QuickSetupView(EventPermissionRequiredMixin, FormView):
|
||||
position=i,
|
||||
all_sales_channels=True,
|
||||
)
|
||||
if questionnaire:
|
||||
item.questionnaires.add(questionnaire)
|
||||
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
|
||||
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
|
||||
quota = self.request.event.quotas.create(
|
||||
|
||||
@@ -39,7 +39,7 @@ from itertools import groupby
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.db import models, transaction
|
||||
from django.db.models import (
|
||||
@@ -56,7 +56,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import FormView, ListView, TemplateView, View
|
||||
from django.views.generic import FormView, ListView, View
|
||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
from django_countries.fields import Country
|
||||
|
||||
@@ -65,15 +65,14 @@ from pretix.api.serializers.item import (
|
||||
ItemVariationSerializer,
|
||||
)
|
||||
from pretix.base.forms import I18nFormSet
|
||||
from pretix.base.forms.questions import get_fake_attendee_questions
|
||||
from pretix.base.models import (
|
||||
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry,
|
||||
OrderPosition, Question, QuestionAnswer, QuestionnaireChild,
|
||||
QuestionOption, Quota, SeatCategoryMapping, Voucher,
|
||||
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
||||
SeatCategoryMapping, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
ItemAddOn, ItemBundle, ItemMetaValue, Questionnaire,
|
||||
)
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tickets import invalidate_cache
|
||||
from pretix.base.signals import quota_availability
|
||||
@@ -95,7 +94,6 @@ from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...helpers import GroupConcat
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from ...helpers.i18n import i18n_all_from_gettext
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
@@ -439,7 +437,66 @@ class QuestionList(ListView):
|
||||
template_name = 'pretixcontrol/items/questions.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.all()
|
||||
return self.request.event.questions.prefetch_related('items')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
questions = get_fake_attendee_questions(self.request.event.settings)
|
||||
|
||||
questions += list(ctx['questions'])
|
||||
questions.sort(key=lambda q: q.position)
|
||||
ctx['questions'] = questions
|
||||
return ctx
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@event_permission_required("event.items:write")
|
||||
@require_http_methods(["POST"])
|
||||
def reorder_questions(request, organizer, event):
|
||||
try:
|
||||
ids = json.loads(request.body.decode('utf-8'))['ids']
|
||||
except (JSONDecodeError, KeyError, ValueError):
|
||||
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
||||
|
||||
qs = request.event.questions.filter(container_type=request.GET['container_type'])
|
||||
|
||||
# filter system_questions - normal questions are int/digit, system_questions strings
|
||||
custom_question_ids = [i for i in ids if i.isdigit()]
|
||||
input_questions = list(qs.filter(id__in=custom_question_ids))
|
||||
|
||||
if len(input_questions) != len(custom_question_ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
|
||||
if len(input_questions) != qs.count():
|
||||
raise Http404(_("Not all objects have been selected."))
|
||||
|
||||
for q in input_questions:
|
||||
pos = ids.index(str(q.pk))
|
||||
if pos != q.position: # Save unneccessary UPDATE queries
|
||||
q.position = pos
|
||||
q.save(update_fields=['position'])
|
||||
q.log_action(
|
||||
'pretix.event.question.reordered', user=request.user, data={
|
||||
'position': pos,
|
||||
}
|
||||
)
|
||||
|
||||
if request.GET['container_type'] == Question.ContainerType.ORDERPOSITION:
|
||||
system_question_order = {}
|
||||
for s in ('attendee_name_parts', 'attendee_email', 'company', 'street', 'zipcode', 'city', 'country'):
|
||||
if s in ids:
|
||||
system_question_order[s] = ids.index(s)
|
||||
else:
|
||||
system_question_order[s] = -1
|
||||
request.event.settings.system_question_order = system_question_order
|
||||
request.event.log_action(
|
||||
'pretix.event.settings', user=request.user, data={
|
||||
'system_question_order': system_question_order,
|
||||
}
|
||||
)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
@@ -458,7 +515,7 @@ class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
# TODO(questionnaires) context['dependent'] = list(self.get_object().items.all())
|
||||
context['dependent'] = list(self.get_object().items.all())
|
||||
context['edit_url'] = reverse('control:event.items.questions.edit', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
@@ -557,7 +614,7 @@ class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView
|
||||
question=self.object, orderposition__isnull=False,
|
||||
)
|
||||
qs = qs.filter(orderposition__in=opqs)
|
||||
op_cnt = 0 # TODO opqs.filter(item__in=self.object.items.all()).count()
|
||||
op_cnt = opqs.filter(item__in=self.object.items.all()).count()
|
||||
|
||||
if self.object.type == Question.TYPE_FILE:
|
||||
qs = [
|
||||
@@ -602,7 +659,7 @@ class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
# TODO(questionnaires) ctx['items'] = self.object.items.exists()
|
||||
ctx['items'] = self.object.items.exists()
|
||||
ctx['has_subevents'] = self.request.event.has_subevents
|
||||
stats = self.get_answer_statistics()
|
||||
ctx['stats'], ctx['total'] = stats
|
||||
@@ -709,22 +766,6 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
||||
return ret
|
||||
|
||||
|
||||
def textchoices_to_json(choices, event):
|
||||
return [(c.name, c.value, i18n_all_from_gettext(c.label, event.settings.locales)) for c in choices]
|
||||
|
||||
|
||||
class QuestionnairesEditor(EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/items/questionnaires.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['questionnaire_type_choices'] = textchoices_to_json(Questionnaire.QuestionnaireType, self.request.event)
|
||||
ctx['system_question_choices'] = textchoices_to_json(QuestionnaireChild.SystemQuestion, self.request.event)
|
||||
ctx['question_type_choices'] = textchoices_to_json(Question.FieldType, self.request.event)
|
||||
return ctx
|
||||
|
||||
|
||||
class QuotaQueryMixin:
|
||||
|
||||
@cached_property
|
||||
@@ -1330,10 +1371,9 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
|
||||
initial['tax_rule'] = trs[0]
|
||||
|
||||
if self.copy_from:
|
||||
fields = ('name', 'internal_name', 'category', 'admission', 'default_price', 'tax_rule')
|
||||
fields = ('name', 'internal_name', 'category', 'admission', 'personalized', 'default_price', 'tax_rule')
|
||||
for f in fields:
|
||||
initial[f] = getattr(self.copy_from, f)
|
||||
initial['questionnaires'] = self.copy_from.questionnaires.values_list('pk', flat=True)
|
||||
initial['copy_from'] = self.copy_from
|
||||
initial['has_variations'] = self.copy_from.variations.exists()
|
||||
|
||||
@@ -1367,7 +1407,7 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx = super().get_context_data()
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
return ctx
|
||||
|
||||
@@ -1375,11 +1415,9 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
|
||||
self.object = None
|
||||
form = self.get_form()
|
||||
if form.is_valid() and all([f.is_valid() for f in self.meta_forms]):
|
||||
try:
|
||||
return self.form_valid(form)
|
||||
except ValidationError as e:
|
||||
form.add_error(None, e)
|
||||
return self.form_invalid(form)
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
|
||||
@@ -1443,11 +1481,9 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
self.get_object()
|
||||
form = self.get_form()
|
||||
if self.is_valid(form) and all([f.is_valid() for f in self.meta_forms]):
|
||||
try:
|
||||
return self.form_valid(form)
|
||||
except ValidationError as exc:
|
||||
form.add_error(None, exc)
|
||||
return self.form_invalid(form)
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def save_formset(self, key, log_base, attr='item', order=True, serializer=None,
|
||||
rm_verb='removed'):
|
||||
@@ -1496,6 +1532,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
self.save_meta()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
|
||||
change_data = {
|
||||
k: form.cleaned_data.get(k)
|
||||
@@ -1555,9 +1592,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
else:
|
||||
v.save()
|
||||
|
||||
response = super().form_valid(form)
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return response
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
@@ -1570,7 +1605,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
return o
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx = super().get_context_data()
|
||||
ctx['plugin_forms'] = self.plugin_forms
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
ctx['formsets'] = self.formsets
|
||||
|
||||
@@ -600,7 +600,7 @@ class OrderDetail(OrderView):
|
||||
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type',
|
||||
'discount',
|
||||
).prefetch_related(
|
||||
'item__questionnaires', 'issued_gift_cards', 'owned_gift_cards', 'linked_media', # TODO(questionnaires) : item__questions gibts nicht mehr
|
||||
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||
Prefetch('all_checkins', queryset=Checkin.all.select_related('list', 'gate').order_by('datetime')),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device').order_by('datetime')),
|
||||
@@ -624,9 +624,9 @@ class OrderDetail(OrderView):
|
||||
p.additional_fields or
|
||||
(p.item.ask_attendee_data and self.request.event.settings.attendee_names_asked) or
|
||||
(p.item.ask_attendee_data and self.request.event.settings.attendee_emails_asked) or
|
||||
p.item.questionnaires.all()
|
||||
p.item.questions.all()
|
||||
)
|
||||
p.cache_answers(questionnaire_type=None, sales_channel=None)
|
||||
p.cache_answers()
|
||||
p.order = self.order
|
||||
|
||||
positions.append(p)
|
||||
|
||||
@@ -761,6 +761,7 @@ class UserNotificationsEditView(TemplateView):
|
||||
{k: a.get(t) for k, a in self.global_set.items()},
|
||||
)
|
||||
for t, tv in self.types.items()
|
||||
if tv.is_event_level or not self.event
|
||||
]
|
||||
ctx['event'] = self.event
|
||||
if self.event:
|
||||
|
||||
@@ -32,7 +32,6 @@ from django.conf import settings
|
||||
from django.utils import translation
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.translation import to_locale
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
date_conversion_to_moment = {
|
||||
'%a': 'ddd',
|
||||
@@ -245,14 +244,3 @@ def parse_date_localized(date_str) -> Optional[datetime]:
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
return dt
|
||||
|
||||
|
||||
def i18n_all_from_gettext(gettext_str, locales):
|
||||
return i18n_all(LazyI18nString.from_gettext(gettext_str).data, locales)
|
||||
|
||||
|
||||
def i18n_all(data, locales):
|
||||
out = {}
|
||||
for locale in locales:
|
||||
out[locale] = data[locale]
|
||||
return out
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-09-28 11:01+0000\n"
|
||||
"Last-Translator: Tim <plicnetwork@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-10-03 23:00+0000\n"
|
||||
"Last-Translator: Oksigenia <github@oksigenia.cc>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"es/>\n"
|
||||
"Language: es\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.9.1\n"
|
||||
"X-Generator: Weblate 2026.10\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -1793,16 +1793,12 @@ msgid "Transmission type"
|
||||
msgstr "Tipo de transmisión"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission status"
|
||||
msgstr "Tipo de transmisión"
|
||||
msgstr "Estado de la transmisión"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission date"
|
||||
msgstr "Tipo de transmisión"
|
||||
msgstr "Fecha de emisión"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
msgid "Cancellation"
|
||||
@@ -3289,7 +3285,7 @@ msgstr ""
|
||||
#: pretix/base/forms/validators.py pretix/control/views/event.py
|
||||
#, python-format
|
||||
msgid "Invalid placeholder: {%(value)s}"
|
||||
msgstr "Marcador de posición no válido: {%(value)ss}"
|
||||
msgstr "Marcador de posición no válido: {%(value)s}"
|
||||
|
||||
#: pretix/base/forms/widgets.py
|
||||
#, python-format
|
||||
@@ -4458,10 +4454,8 @@ msgid "Initialization date"
|
||||
msgstr "Fecha de inicialización"
|
||||
|
||||
#: pretix/base/models/devices.py
|
||||
#, fuzzy
|
||||
#| msgid "Save to profile"
|
||||
msgid "Security profile"
|
||||
msgstr "Guardar en perfil"
|
||||
msgstr "Perfil de seguridad"
|
||||
|
||||
#: pretix/base/models/discount.py
|
||||
msgctxt "subevent"
|
||||
@@ -5904,10 +5898,8 @@ msgid "Maximum value"
|
||||
msgstr "Valor máximo"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Maximum length"
|
||||
msgid "Minimum length"
|
||||
msgstr "Longitud máxima"
|
||||
msgstr "Longitud mínima"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Maximum length"
|
||||
@@ -5978,10 +5970,8 @@ msgid "The maximum value must not be lower than the minimum value."
|
||||
msgstr "El valor máximo no debe ser inferior al mínimo."
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "The maximum value must not be lower than the minimum value."
|
||||
msgid "The maximum length must not be shorter than the minimum length."
|
||||
msgstr "El valor máximo no debe ser inferior al mínimo."
|
||||
msgstr "La longitud máxima no debe ser inferior a la longitud mínima."
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid ""
|
||||
@@ -10581,10 +10571,8 @@ msgstr ""
|
||||
"después de la `Última fecha de pagos' configurada anteriormente."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, fuzzy
|
||||
#| msgid "Generate invoices for Sales channels"
|
||||
msgid "Allow postponed payment choice for sales channels"
|
||||
msgstr "Generar facturas para canales de ventas"
|
||||
msgstr "Permitir la opción de pago aplazado en los canales de venta"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid ""
|
||||
@@ -10592,6 +10580,9 @@ msgid ""
|
||||
"their order without selecting a payment method. This is useful whenever "
|
||||
"orders are not created by the same person who is making the payment."
|
||||
msgstr ""
|
||||
"Si en un canal de venta se permite el pago aplazado, los clientes pueden "
|
||||
"completar su pedido sin seleccionar una forma de pago. Esto resulta útil "
|
||||
"cuando los pedidos no los realiza la misma persona que efectúa el pago."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Show start date"
|
||||
@@ -16860,7 +16851,7 @@ msgstr "tercero"
|
||||
#: pretix/control/forms/rrule.py
|
||||
msgctxt "rrule"
|
||||
msgid "fourth"
|
||||
msgstr ""
|
||||
msgstr "cuarto"
|
||||
|
||||
#: pretix/control/forms/rrule.py
|
||||
msgctxt "rrule"
|
||||
@@ -17993,28 +17984,20 @@ msgid "Queued emails have been aborted."
|
||||
msgstr "Los correos electrónicos en cola se han cancelado."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The property has been created."
|
||||
msgid "An organizer meta property has been created."
|
||||
msgstr "La propiedad ha sido creada."
|
||||
msgstr "Se ha creado una metapropiedad de organizador."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The selected property has been deleted."
|
||||
msgid "An organizer meta property has been deleted."
|
||||
msgstr "La propiedad seleccionada ha sido eliminada."
|
||||
msgstr "Se ha eliminado una metapropiedad del organizador."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The organizer has been changed."
|
||||
msgid "An organizer meta property has been changed."
|
||||
msgstr "El pedido se ha cambiado."
|
||||
msgstr "Se ha modificado una metapropiedad del organizador."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The product has been reordered."
|
||||
msgid "An organizer meta property has been reordered."
|
||||
msgstr "El producto se ha pedido de nuevo."
|
||||
msgstr "Se ha cambiado el orden de una metapropiedad del organizador."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "Gift card acceptance for another organizer has been added."
|
||||
@@ -18228,6 +18211,8 @@ msgstr "Se ha generado un código de emergencia de doble factor."
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "Drift and throttle values for two-factor devices have been reset."
|
||||
msgstr ""
|
||||
"Se han restablecido los valores de deriva y aceleración para los "
|
||||
"dispositivos de dos factores."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
@@ -19180,10 +19165,8 @@ msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
#, fuzzy
|
||||
#| msgid "Authorized apps"
|
||||
msgid "Authorize"
|
||||
msgstr "Aplicaciones autorizadas"
|
||||
msgstr "Autorizar"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
msgid "Error:"
|
||||
@@ -22071,14 +22054,10 @@ msgstr ""
|
||||
"hacer referencia a este evento."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_basics.html
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "We strongly recommend against using short forms of more then 16 "
|
||||
#| "characters."
|
||||
msgid ""
|
||||
"We strongly recommend against using short forms of more than 16 characters."
|
||||
msgstr ""
|
||||
"Recomendamos encarecidamente no utilizar formularios cortos de más de 16 "
|
||||
"Recomendamos encarecidamente no utilizar abreviaturas de más de 16 "
|
||||
"caracteres."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_basics.html
|
||||
@@ -27957,27 +27936,23 @@ msgstr "Autenticación backend"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
msgid "Reset drift and throttle"
|
||||
msgstr ""
|
||||
msgstr "Restablecer la desviación y el control"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy
|
||||
#| msgid "Disable two-factor authentication"
|
||||
msgid "Available two-factor authentication methods"
|
||||
msgstr "Desactivar la autenticación de dos factores"
|
||||
msgstr "Métodos de autenticación de dos factores disponibles"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Expired since %(date)s"
|
||||
#, python-format
|
||||
msgid "1 failed attempt since %(date)s"
|
||||
msgid_plural "%(cnt)s failed attempts since %(date)s"
|
||||
msgstr[0] "Vencido desde el %(date)s"
|
||||
msgstr[1] "Vencido desde el %(date)s"
|
||||
msgstr[0] "1 intento fallido desde %(date)s"
|
||||
msgstr[1] "%(cnt)s intentos fallidos desde el %(date)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Valid until %(date)s"
|
||||
#, python-format
|
||||
msgid "Currently locked until %(date)s"
|
||||
msgstr "Válido hasta el %(date)s"
|
||||
msgstr "Actualmente bloqueado hasta el %(date)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
msgid "Team memberships"
|
||||
@@ -28519,20 +28494,18 @@ msgid "Invalid code, please try again."
|
||||
msgstr "Código inválido, por favor inténtelo de nuevo."
|
||||
|
||||
#: pretix/control/views/auth.py
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Invalid code, please try again."
|
||||
#, python-brace-format
|
||||
msgid "Invalid code. Please try again after waiting {value} minute."
|
||||
msgid_plural "Invalid code. Please try again after waiting {value} minutes."
|
||||
msgstr[0] "Código inválido, por favor inténtelo de nuevo."
|
||||
msgstr[1] "Código inválido, por favor inténtelo de nuevo."
|
||||
msgstr[0] "Código no válido. Inténtalo de nuevo tras esperar {value} minutos."
|
||||
msgstr[1] "ódigo no válido. Inténtalo de nuevo tras esperar {value} minutos."
|
||||
|
||||
#: pretix/control/views/auth.py
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Invalid code, please try again."
|
||||
#, python-brace-format
|
||||
msgid "Invalid code. Please try again after waiting {value} second."
|
||||
msgid_plural "Invalid code. Please try again after waiting {value} seconds."
|
||||
msgstr[0] "Código inválido, por favor inténtelo de nuevo."
|
||||
msgstr[1] "Código inválido, por favor inténtelo de nuevo."
|
||||
msgstr[0] "Código no válido. Inténtalo de nuevo tras esperar {value} segundo."
|
||||
msgstr[1] "Código no válido. Inténtalo de nuevo tras esperar {value} segundos."
|
||||
|
||||
#: pretix/control/views/checkin.py
|
||||
msgid "The selected check-ins have been reverted."
|
||||
@@ -29405,10 +29378,8 @@ msgid "You entered an order in an event with a different currency."
|
||||
msgstr "Ha introducido un pedido en un evento con una moneda diferente."
|
||||
|
||||
#: pretix/control/views/orders.py
|
||||
#, fuzzy
|
||||
#| msgid "You entered an order in an event with a different currency."
|
||||
msgid "You entered an order in an event that you do not have access to."
|
||||
msgstr "Ha introducido un pedido en un evento con una moneda diferente."
|
||||
msgstr "Ha introducido un pedido en un evento al que no tienes acceso."
|
||||
|
||||
#: pretix/control/views/orders.py
|
||||
msgid ""
|
||||
@@ -30296,10 +30267,9 @@ msgstr ""
|
||||
"código de emergencia anterior para este usuario permanecerá activo."
|
||||
|
||||
#: pretix/control/views/users.py
|
||||
#, fuzzy
|
||||
#| msgid "The password has been reset."
|
||||
msgid "The drift values for TOTP devices have been reset."
|
||||
msgstr "La contraseña ha sido reiniciada."
|
||||
msgstr ""
|
||||
"Se han restablecido los valores de desviación para los dispositivos TOTP."
|
||||
|
||||
#: pretix/control/views/users.py
|
||||
msgid "The new user has been created."
|
||||
@@ -30314,14 +30284,12 @@ msgid "Bypass quota"
|
||||
msgstr "Cuota de bypass"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
#, fuzzy
|
||||
#| msgid "Widget"
|
||||
msgid "Budget"
|
||||
msgstr "Widget"
|
||||
msgstr "Presupuesto"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
msgid "Budget used"
|
||||
msgstr ""
|
||||
msgstr "Presupuesto utilizado"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
msgid "The requested voucher does not exist."
|
||||
@@ -35022,29 +34990,29 @@ msgid ""
|
||||
"Not sure yet? You can complete your order first and then select a payment "
|
||||
"method later."
|
||||
msgstr ""
|
||||
"¿Aún no estás seguro? Puedes completar primero tu pedido y seleccionar la "
|
||||
"forma de pago más tarde."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
msgid ""
|
||||
"To do so, please first remove the payment methods you already selected above."
|
||||
msgstr ""
|
||||
"Para ello, elimina primero los métodos de pago que ya hayas seleccionado "
|
||||
"anteriormente."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Your event needs to be over to use this feature."
|
||||
#, python-format
|
||||
msgid "Your payment needs to be completed within %(minutes)s minutes."
|
||||
msgstr "Su evento debe haber terminado para utilizar esta función."
|
||||
msgstr "Su pago debe completarse en un plazo de %(minutes)s minutos."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Your event needs to be over to use this feature."
|
||||
#, python-format
|
||||
msgid "Your payment needs to be completed by %(deadline)s."
|
||||
msgstr "Su evento debe haber terminado para utilizar esta función."
|
||||
msgstr "Su pago debe completarse antes de %(deadline)s."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy
|
||||
#| msgid "Proceed with reset"
|
||||
msgid "Proceed without selection"
|
||||
msgstr "Continuar con el restablecimiento"
|
||||
msgstr "Continuar sin seleccionar nada"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
msgid "Before we continue, we need you to answer some questions."
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"PO-Revision-Date: 2026-10-03 23:00+0000\n"
|
||||
"Last-Translator: Oksigenia <github@oksigenia.cc>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/es/>\n"
|
||||
"Language: es\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.6.1\n"
|
||||
"X-Generator: Weblate 2026.10\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -780,7 +780,7 @@ msgstr "El organizador retiene %(currency)s %(amount)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "Se le devolverá %(moneda)s %(cantidad)s"
|
||||
msgstr "Se le devolverá %(currency)s %(amount)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
|
||||
@@ -4,10 +4,10 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-09-30 07:25+0000\n"
|
||||
"PO-Revision-Date: 2026-10-01 09:28+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"fr/>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -1793,16 +1793,12 @@ msgid "Transmission type"
|
||||
msgstr "Type de transmission"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission status"
|
||||
msgstr "Type de transmission"
|
||||
msgstr "État de la transmission"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission date"
|
||||
msgstr "Type de transmission"
|
||||
msgstr "Date de transmission"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
msgid "Cancellation"
|
||||
@@ -4468,10 +4464,8 @@ msgid "Initialization date"
|
||||
msgstr "Date d'initialisation"
|
||||
|
||||
#: pretix/base/models/devices.py
|
||||
#, fuzzy
|
||||
#| msgid "Save to profile"
|
||||
msgid "Security profile"
|
||||
msgstr "Enregistrer dans le profil"
|
||||
msgstr "Profil de sécurité"
|
||||
|
||||
#: pretix/base/models/discount.py
|
||||
msgctxt "subevent"
|
||||
@@ -5925,10 +5919,8 @@ msgid "Maximum value"
|
||||
msgstr "Valeur maximale"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Maximum length"
|
||||
msgid "Minimum length"
|
||||
msgstr "Longueur maximale"
|
||||
msgstr "Longueur minimale"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Maximum length"
|
||||
@@ -6000,10 +5992,9 @@ msgid "The maximum value must not be lower than the minimum value."
|
||||
msgstr "La valeur maximale ne doit pas être inférieure à la valeur minimale."
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "The maximum value must not be lower than the minimum value."
|
||||
msgid "The maximum length must not be shorter than the minimum length."
|
||||
msgstr "La valeur maximale ne doit pas être inférieure à la valeur minimale."
|
||||
msgstr ""
|
||||
"La longueur maximale ne doit pas être inférieure à la longueur minimale."
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid ""
|
||||
@@ -10647,10 +10638,8 @@ msgstr ""
|
||||
"accepté après la 'dernière date de paiement' configurée ci-dessus."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, fuzzy
|
||||
#| msgid "Generate invoices for Sales channels"
|
||||
msgid "Allow postponed payment choice for sales channels"
|
||||
msgstr "Générer des factures pour les canaux de vente"
|
||||
msgstr "Autoriser l'option de paiement différé pour les canaux de vente"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid ""
|
||||
@@ -10658,6 +10647,10 @@ msgid ""
|
||||
"their order without selecting a payment method. This is useful whenever "
|
||||
"orders are not created by the same person who is making the payment."
|
||||
msgstr ""
|
||||
"Si le paiement différé est autorisé sur un canal de vente, les clients "
|
||||
"peuvent finaliser leur commande sans sélectionner de mode de paiement. Cette "
|
||||
"fonctionnalité est utile lorsque la commande n'est pas passée par la même "
|
||||
"personne que celle qui effectue le paiement."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Show start date"
|
||||
@@ -17017,7 +17010,7 @@ msgstr "troisième"
|
||||
#: pretix/control/forms/rrule.py
|
||||
msgctxt "rrule"
|
||||
msgid "fourth"
|
||||
msgstr ""
|
||||
msgstr "quatrième"
|
||||
|
||||
#: pretix/control/forms/rrule.py
|
||||
msgctxt "rrule"
|
||||
@@ -18137,28 +18130,20 @@ msgid "Queued emails have been aborted."
|
||||
msgstr "Les e-mails en attente ont été annulés."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The property has been created."
|
||||
msgid "An organizer meta property has been created."
|
||||
msgstr "La propriété a été créée."
|
||||
msgstr "Une méta-propriété d'organisateur a été créée."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The selected property has been deleted."
|
||||
msgid "An organizer meta property has been deleted."
|
||||
msgstr "La propriété sélectionnée a été supprimée."
|
||||
msgstr "Une méta-propriété d'organisateur a été supprimée."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The organizer has been changed."
|
||||
msgid "An organizer meta property has been changed."
|
||||
msgstr "L’organisateur a été modifié."
|
||||
msgstr "Une méta-propriété d'organisateur a été modifiée."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The product has been reordered."
|
||||
msgid "An organizer meta property has been reordered."
|
||||
msgstr "Le produit a été commandé à nouveau."
|
||||
msgstr "Une méta-propriété d'organisateur a été réorganisée."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "Gift card acceptance for another organizer has been added."
|
||||
@@ -18374,6 +18359,8 @@ msgstr "Un code d'urgence à deux facteurs a été généré."
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "Drift and throttle values for two-factor devices have been reset."
|
||||
msgstr ""
|
||||
"Les valeurs de déviation et de puissance pour les appareils à deux facteurs "
|
||||
"ont été réinitialisées."
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
@@ -19322,10 +19309,8 @@ msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
#, fuzzy
|
||||
#| msgid "Authorized apps"
|
||||
msgid "Authorize"
|
||||
msgstr "App. autorisées"
|
||||
msgstr "Autoriser"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
msgid "Error:"
|
||||
@@ -22231,15 +22216,11 @@ msgstr ""
|
||||
"comme abréviations pour faire référence à cet événement."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_basics.html
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "We strongly recommend against using short forms of more then 16 "
|
||||
#| "characters."
|
||||
msgid ""
|
||||
"We strongly recommend against using short forms of more than 16 characters."
|
||||
msgstr ""
|
||||
"Nous vous déconseillons fortement d'utiliser des formulaires courts de plus "
|
||||
"de 16 caractères."
|
||||
"Nous déconseillons vivement d'utiliser des abréviations de plus de 16 "
|
||||
"caractères."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_basics.html
|
||||
msgid "Display settings"
|
||||
@@ -28171,27 +28152,23 @@ msgstr "Backend d’authentification"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
msgid "Reset drift and throttle"
|
||||
msgstr ""
|
||||
msgstr "Réinitialisation de la déviation et de la puissance"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy
|
||||
#| msgid "Disable two-factor authentication"
|
||||
msgid "Available two-factor authentication methods"
|
||||
msgstr "Désactiver l'authentification à deux facteurs"
|
||||
msgstr "Méthodes d'authentification à deux facteurs disponibles"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Expired since %(date)s"
|
||||
#, python-format
|
||||
msgid "1 failed attempt since %(date)s"
|
||||
msgid_plural "%(cnt)s failed attempts since %(date)s"
|
||||
msgstr[0] "Expiré depuis le %(date)s"
|
||||
msgstr[1] "Expiré depuis le %(date)s"
|
||||
msgstr[0] "1 tentative infructueuse depuis %(date)s"
|
||||
msgstr[1] "%(cnt)s tentatives infructueuses depuis le %(date)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Valid until %(date)s"
|
||||
#, python-format
|
||||
msgid "Currently locked until %(date)s"
|
||||
msgstr "Valable jusqu'au %(date)s"
|
||||
msgstr "Actuellement verrouillé jusqu'au %(date)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
msgid "Team memberships"
|
||||
@@ -28737,20 +28714,19 @@ msgid "Invalid code, please try again."
|
||||
msgstr "Code invalide, veuillez réessayer."
|
||||
|
||||
#: pretix/control/views/auth.py
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Invalid code, please try again."
|
||||
#, python-brace-format
|
||||
msgid "Invalid code. Please try again after waiting {value} minute."
|
||||
msgid_plural "Invalid code. Please try again after waiting {value} minutes."
|
||||
msgstr[0] "Code invalide, veuillez réessayer."
|
||||
msgstr[1] "Code invalide, veuillez réessayer."
|
||||
msgstr[0] "Code invalide. Veuillez réessayer après avoir patienté {value} minute."
|
||||
msgstr[1] "Code invalide. Veuillez réessayer après avoir patienté {value} minutes."
|
||||
|
||||
#: pretix/control/views/auth.py
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Invalid code, please try again."
|
||||
#, python-brace-format
|
||||
msgid "Invalid code. Please try again after waiting {value} second."
|
||||
msgid_plural "Invalid code. Please try again after waiting {value} seconds."
|
||||
msgstr[0] "Code invalide, veuillez réessayer."
|
||||
msgstr[1] "Code invalide, veuillez réessayer."
|
||||
msgstr[0] "Code invalide. Veuillez réessayer après avoir patienté {value} seconde."
|
||||
msgstr[1] ""
|
||||
"Code invalide. Veuillez réessayer après avoir patienté {value} secondes."
|
||||
|
||||
#: pretix/control/views/checkin.py
|
||||
msgid "The selected check-ins have been reverted."
|
||||
@@ -29632,11 +29608,9 @@ msgstr ""
|
||||
"Vous avez passé une commande dans un événement dont la devise est différente."
|
||||
|
||||
#: pretix/control/views/orders.py
|
||||
#, fuzzy
|
||||
#| msgid "You entered an order in an event with a different currency."
|
||||
msgid "You entered an order in an event that you do not have access to."
|
||||
msgstr ""
|
||||
"Vous avez passé une commande dans un événement dont la devise est différente."
|
||||
"Vous avez saisi une commande dans un événement auquel vous n'avez pas accès."
|
||||
|
||||
#: pretix/control/views/orders.py
|
||||
msgid ""
|
||||
@@ -30535,10 +30509,8 @@ msgstr ""
|
||||
"d'urgence précédents de cet utilisateur sont toujours actifs."
|
||||
|
||||
#: pretix/control/views/users.py
|
||||
#, fuzzy
|
||||
#| msgid "The password has been reset."
|
||||
msgid "The drift values for TOTP devices have been reset."
|
||||
msgstr "Le mot de passe a été réinitialisé."
|
||||
msgstr "Les valeurs de dérive des appareils TOTP ont été réinitialisées."
|
||||
|
||||
#: pretix/control/views/users.py
|
||||
msgid "The new user has been created."
|
||||
@@ -30553,14 +30525,12 @@ msgid "Bypass quota"
|
||||
msgstr "Outrepasser le quota"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
#, fuzzy
|
||||
#| msgid "Widget"
|
||||
msgid "Budget"
|
||||
msgstr "Widget"
|
||||
msgstr "Budget"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
msgid "Budget used"
|
||||
msgstr ""
|
||||
msgstr "Budget utilisé"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
msgid "The requested voucher does not exist."
|
||||
@@ -35309,29 +35279,29 @@ msgid ""
|
||||
"Not sure yet? You can complete your order first and then select a payment "
|
||||
"method later."
|
||||
msgstr ""
|
||||
"Vous hésitez encore ? Vous pouvez d'abord finaliser votre commande, puis "
|
||||
"choisir un mode de paiement ultérieurement."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
msgid ""
|
||||
"To do so, please first remove the payment methods you already selected above."
|
||||
msgstr ""
|
||||
"Pour ce faire, veuillez d'abord supprimer les moyens de paiement que vous "
|
||||
"avez déjà sélectionnés ci-dessus."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "The payment has to be completed before %(date)s."
|
||||
#, python-format
|
||||
msgid "Your payment needs to be completed within %(minutes)s minutes."
|
||||
msgstr "Le paiement doit être effectué avant le %(date)s."
|
||||
msgstr "otre paiement doit être effectué dans les %(minutes)s minutes."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "The payment has to be completed before %(date)s."
|
||||
#, python-format
|
||||
msgid "Your payment needs to be completed by %(deadline)s."
|
||||
msgstr "Le paiement doit être effectué avant le %(date)s."
|
||||
msgstr "Votre paiement doit être effectué avant le %(deadline)s."
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy
|
||||
#| msgid "Proceed with reset"
|
||||
msgid "Proceed without selection"
|
||||
msgstr "Procéder à la réinitialisation"
|
||||
msgstr "Continuer sans sélectionner"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
msgid "Before we continue, we need you to answer some questions."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-08-01 21:00+0000\n"
|
||||
"PO-Revision-Date: 2026-09-30 17:00+0000\n"
|
||||
"Last-Translator: szurofkamarciidfbe08444ef04788 <szurofkamarcii@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"hu/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.7.1\n"
|
||||
"X-Generator: Weblate 2026.9.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -93,7 +93,7 @@ msgstr "Héber"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
msgstr "Magyar"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
@@ -426,10 +426,8 @@ msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "Nem cserélhet adathordozót adathordozóra."
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "Product does not support medium exchange."
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr "A termék nem támogatja az adathordozó-cserét."
|
||||
msgstr "Az adathordozó szimuláció nem engedélyezett."
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
@@ -1779,16 +1777,12 @@ msgid "Transmission type"
|
||||
msgstr "Továbbítás típusa"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission status"
|
||||
msgstr "Továbbítás típusa"
|
||||
msgstr "Továbbítás állapot"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission date"
|
||||
msgstr "Továbbítás típusa"
|
||||
msgstr "Továbbítás dátum"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
msgid "Cancellation"
|
||||
@@ -3078,9 +3072,6 @@ msgid "Street"
|
||||
msgstr "Utca"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, fuzzy
|
||||
#| msgctxt "address"
|
||||
#| msgid "State"
|
||||
msgid "State"
|
||||
msgstr "Állam/tartomány"
|
||||
|
||||
@@ -4429,10 +4420,8 @@ msgid "Initialization date"
|
||||
msgstr "Inicializálás dátuma"
|
||||
|
||||
#: pretix/base/models/devices.py
|
||||
#, fuzzy
|
||||
#| msgid "Save to profile"
|
||||
msgid "Security profile"
|
||||
msgstr "Mentés a profilhoz"
|
||||
msgstr "Biztonsági profil"
|
||||
|
||||
#: pretix/base/models/discount.py
|
||||
msgctxt "subevent"
|
||||
@@ -5864,10 +5853,8 @@ msgid "Maximum value"
|
||||
msgstr "Maximális érték"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Maximum length"
|
||||
msgid "Minimum length"
|
||||
msgstr "Maximális hossz"
|
||||
msgstr "Minimum hossz"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Maximum length"
|
||||
@@ -5938,10 +5925,8 @@ msgid "The maximum value must not be lower than the minimum value."
|
||||
msgstr "A maximális érték nem lehet kisebb a minimális értéknél."
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "The maximum value must not be lower than the minimum value."
|
||||
msgid "The maximum length must not be shorter than the minimum length."
|
||||
msgstr "A maximális érték nem lehet kisebb a minimális értéknél."
|
||||
msgstr "A maximális hossz nem lehet rövidebb a minimális hossznál."
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid ""
|
||||
@@ -7489,9 +7474,6 @@ msgid "The payment for this invoice has already been received."
|
||||
msgstr "A számla már ki lett fizetve."
|
||||
|
||||
#: pretix/base/payment.py
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This payment is already being processed and can not be canceled any more."
|
||||
msgid ""
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr "Ez a fizetés már feldolgozás alatt áll, és többé nem törölhető."
|
||||
@@ -8267,16 +8249,12 @@ msgid "Presale end"
|
||||
msgstr "Elővétel vége"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order email"
|
||||
msgid "Order creation"
|
||||
msgstr "Rendelés e-mail címe"
|
||||
msgstr "Rendelés létrehozás"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order expired"
|
||||
msgid "Order expiry"
|
||||
msgstr "Rendelés lejárt"
|
||||
msgstr "Rendelés lejárat"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-09-07 18:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-10-05 14:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
"X-Generator: Weblate 2026.10\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -1776,16 +1776,12 @@ msgid "Transmission type"
|
||||
msgstr "送信タイプ"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission status"
|
||||
msgstr "送信タイプ"
|
||||
msgstr "送信ステータス"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
#, fuzzy
|
||||
#| msgid "Transmission type"
|
||||
msgid "Transmission date"
|
||||
msgstr "送信タイプ"
|
||||
msgstr "送信日"
|
||||
|
||||
#: pretix/base/exporters/invoices.py
|
||||
msgid "Cancellation"
|
||||
@@ -4400,10 +4396,8 @@ msgid "Initialization date"
|
||||
msgstr "初期化日"
|
||||
|
||||
#: pretix/base/models/devices.py
|
||||
#, fuzzy
|
||||
#| msgid "Save to profile"
|
||||
msgid "Security profile"
|
||||
msgstr "プロフィールに保存します"
|
||||
msgstr "セキュリティ・プロフィール"
|
||||
|
||||
#: pretix/base/models/discount.py
|
||||
msgctxt "subevent"
|
||||
@@ -5779,10 +5773,8 @@ msgid "Maximum value"
|
||||
msgstr "最大値"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "Maximum length"
|
||||
msgid "Minimum length"
|
||||
msgstr "最大長"
|
||||
msgstr "最小長"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Maximum length"
|
||||
@@ -5853,10 +5845,8 @@ msgid "The maximum value must not be lower than the minimum value."
|
||||
msgstr "最大値は最小値より低くできません。"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
#| msgid "The maximum value must not be lower than the minimum value."
|
||||
msgid "The maximum length must not be shorter than the minimum length."
|
||||
msgstr "最大値は最小値より低くできません。"
|
||||
msgstr "最大長は最小長より短くできません。"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid ""
|
||||
@@ -10269,10 +10259,8 @@ msgstr ""
|
||||
"す。上記で設定された\"支払いの最終日\"を過ぎてからは支払いを受け付けません。"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
#, fuzzy
|
||||
#| msgid "Generate invoices for Sales channels"
|
||||
msgid "Allow postponed payment choice for sales channels"
|
||||
msgstr "請求書を生成する販売チャネル"
|
||||
msgstr "販売チャネルの支払延期選択を許可する"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid ""
|
||||
@@ -10280,6 +10268,9 @@ msgid ""
|
||||
"their order without selecting a payment method. This is useful whenever "
|
||||
"orders are not created by the same person who is making the payment."
|
||||
msgstr ""
|
||||
"販売チャネルで支払いの延期が許可されている場合、顧客は支払い方法を選択せずに"
|
||||
"注文を完了できます。これは、支払いを行う同じ人物によって注文が作成されていな"
|
||||
"い場合に便利です。"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Show start date"
|
||||
@@ -16362,7 +16353,7 @@ msgstr "第三"
|
||||
#: pretix/control/forms/rrule.py
|
||||
msgctxt "rrule"
|
||||
msgid "fourth"
|
||||
msgstr ""
|
||||
msgstr "4番目"
|
||||
|
||||
#: pretix/control/forms/rrule.py
|
||||
msgctxt "rrule"
|
||||
@@ -17442,28 +17433,20 @@ msgid "Queued emails have been aborted."
|
||||
msgstr "キューに入っていた電子メールが、破棄されました。"
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The property has been created."
|
||||
msgid "An organizer meta property has been created."
|
||||
msgstr "プロパティが作成されました。"
|
||||
msgstr "主催者メタプロパティが作成されました。"
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The selected property has been deleted."
|
||||
msgid "An organizer meta property has been deleted."
|
||||
msgstr "選択されたプロパティは削除されました。"
|
||||
msgstr "オーガナイザーのメタプロパティが削除されました。"
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The organizer has been changed."
|
||||
msgid "An organizer meta property has been changed."
|
||||
msgstr "主催者が変更されました。"
|
||||
msgstr "主催者のメタプロパティが変更されました。"
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, fuzzy
|
||||
#| msgid "The product has been reordered."
|
||||
msgid "An organizer meta property has been reordered."
|
||||
msgstr "製品の並び順が変更されました。"
|
||||
msgstr "オーガナイザーのメタプロパティが再配置されました。"
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "Gift card acceptance for another organizer has been added."
|
||||
@@ -17673,7 +17656,7 @@ msgstr "二要素の緊急コードが生成されました。"
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "Drift and throttle values for two-factor devices have been reset."
|
||||
msgstr ""
|
||||
msgstr "二要素装置のドリフト値とスロットル値がリセットされました。"
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
@@ -18611,10 +18594,8 @@ msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
#, fuzzy
|
||||
#| msgid "Authorized apps"
|
||||
msgid "Authorize"
|
||||
msgstr "承認されたアプリ"
|
||||
msgstr "承認する"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
msgid "Error:"
|
||||
@@ -21418,10 +21399,6 @@ msgstr ""
|
||||
"銀行振込の参照としていくつかの場所で使用します。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_basics.html
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "We strongly recommend against using short forms of more then 16 "
|
||||
#| "characters."
|
||||
msgid ""
|
||||
"We strongly recommend against using short forms of more than 16 characters."
|
||||
msgstr "16文字を超える短縮形の使用は強くお勧めしません。"
|
||||
@@ -27136,26 +27113,22 @@ msgstr "認証バックエンド"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
msgid "Reset drift and throttle"
|
||||
msgstr ""
|
||||
msgstr "ドリフトとスロットルをリセットする"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy
|
||||
#| msgid "Disable two-factor authentication"
|
||||
msgid "Available two-factor authentication methods"
|
||||
msgstr "二要素認証を無効にする"
|
||||
msgstr "選択可能な二要素認証"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Expired since %(date)s"
|
||||
#, python-format
|
||||
msgid "1 failed attempt since %(date)s"
|
||||
msgid_plural "%(cnt)s failed attempts since %(date)s"
|
||||
msgstr[0] "%(date)s以降失効"
|
||||
msgstr[0] "%(date)s以降%(cnt)s回失敗"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Valid until %(date)s"
|
||||
#, python-format
|
||||
msgid "Currently locked until %(date)s"
|
||||
msgstr "%(date)sまで有効"
|
||||
msgstr "現時点で%(date)sまでロックされています"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/users/form.html
|
||||
msgid "Team memberships"
|
||||
@@ -27676,18 +27649,16 @@ msgid "Invalid code, please try again."
|
||||
msgstr "無効なコードです。もう一度お試しください。"
|
||||
|
||||
#: pretix/control/views/auth.py
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Invalid code, please try again."
|
||||
#, python-brace-format
|
||||
msgid "Invalid code. Please try again after waiting {value} minute."
|
||||
msgid_plural "Invalid code. Please try again after waiting {value} minutes."
|
||||
msgstr[0] "無効なコードです。もう一度お試しください。"
|
||||
msgstr[0] "無効なコードです。{value}分後再度お試しください。"
|
||||
|
||||
#: pretix/control/views/auth.py
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Invalid code, please try again."
|
||||
#, python-brace-format
|
||||
msgid "Invalid code. Please try again after waiting {value} second."
|
||||
msgid_plural "Invalid code. Please try again after waiting {value} seconds."
|
||||
msgstr[0] "無効なコードです。もう一度お試しください。"
|
||||
msgstr[0] "無効なコードです。{value}秒後再度お試しください。"
|
||||
|
||||
#: pretix/control/views/checkin.py
|
||||
msgid "The selected check-ins have been reverted."
|
||||
@@ -28533,10 +28504,8 @@ msgid "You entered an order in an event with a different currency."
|
||||
msgstr "イベントで異なる通貨で注文を入力しました。"
|
||||
|
||||
#: pretix/control/views/orders.py
|
||||
#, fuzzy
|
||||
#| msgid "You entered an order in an event with a different currency."
|
||||
msgid "You entered an order in an event that you do not have access to."
|
||||
msgstr "イベントで異なる通貨で注文を入力しました。"
|
||||
msgstr "アクセスできないイベントに注文が入力されました。"
|
||||
|
||||
#: pretix/control/views/orders.py
|
||||
msgid ""
|
||||
@@ -29394,10 +29363,8 @@ msgstr ""
|
||||
"ください。このユーザーの過去の緊急トークンは引き続き有効です。"
|
||||
|
||||
#: pretix/control/views/users.py
|
||||
#, fuzzy
|
||||
#| msgid "The password has been reset."
|
||||
msgid "The drift values for TOTP devices have been reset."
|
||||
msgstr "パスワードがリセットされました。"
|
||||
msgstr "TOTP デバイスのドリフト値がリセットされました。"
|
||||
|
||||
#: pretix/control/views/users.py
|
||||
msgid "The new user has been created."
|
||||
@@ -29412,14 +29379,12 @@ msgid "Bypass quota"
|
||||
msgstr "クォータをバイパスする"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
#, fuzzy
|
||||
#| msgid "Widget"
|
||||
msgid "Budget"
|
||||
msgstr "ウィジェット"
|
||||
msgstr "予算"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
msgid "Budget used"
|
||||
msgstr ""
|
||||
msgstr "消化済み予算"
|
||||
|
||||
#: pretix/control/views/vouchers.py
|
||||
msgid "The requested voucher does not exist."
|
||||
@@ -33979,29 +33944,27 @@ msgid ""
|
||||
"Not sure yet? You can complete your order first and then select a payment "
|
||||
"method later."
|
||||
msgstr ""
|
||||
"まだ確かではありませんか?ご注文を先に完了し、その後でお支払い方法を選択でき"
|
||||
"ます。"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
msgid ""
|
||||
"To do so, please first remove the payment methods you already selected above."
|
||||
msgstr ""
|
||||
msgstr "そのためには、まず上記で既に選択された支払い方法を削除してください。"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Your event needs to be over to use this feature."
|
||||
#, python-format
|
||||
msgid "Your payment needs to be completed within %(minutes)s minutes."
|
||||
msgstr "この機能を使用するには、イベントが終了している必要があります。"
|
||||
msgstr "支払いは、%(minutes)s分以内に完了する必要があります。"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Your event needs to be over to use this feature."
|
||||
#, python-format
|
||||
msgid "Your payment needs to be completed by %(deadline)s."
|
||||
msgstr "この機能を使用するには、イベントが終了している必要があります。"
|
||||
msgstr "支払いは、%(deadline)sまでに完了させる必要があります。"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
|
||||
#, fuzzy
|
||||
#| msgid "Proceed with reset"
|
||||
msgid "Proceed without selection"
|
||||
msgstr "リセットを実行"
|
||||
msgstr "選択なしで進む"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
msgid "Before we continue, we need you to answer some questions."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,9 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2025-10-17 16:55+0000\n"
|
||||
"Last-Translator: fd <fd@denkena-consulting.com>\n"
|
||||
"PO-Revision-Date: 2026-10-02 05:00+0000\n"
|
||||
"Last-Translator: Translate pretix user 594 "
|
||||
"<ivagvr@users.noreply.translate.pretix.eu>\n"
|
||||
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/ru/>\n"
|
||||
"Language: ru\n"
|
||||
@@ -18,7 +19,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
"X-Generator: Weblate 2026.10\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -78,10 +79,8 @@ msgid "SOFORT"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#, fuzzy
|
||||
#| msgid "Yes"
|
||||
msgid "eps"
|
||||
msgstr "Да"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "MyBank"
|
||||
@@ -147,7 +146,7 @@ msgstr "Подтверждается ваш платёж…"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
msgid "Payment method unavailable"
|
||||
msgstr ""
|
||||
msgstr "Способ оплаты недоступен"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||
msgid "Placed orders"
|
||||
@@ -200,21 +199,16 @@ msgid "Switch check-in list"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Check-in QR"
|
||||
msgid "Search results"
|
||||
msgstr "QR-код для регистрации"
|
||||
msgstr "Результаты поиска"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Close ticket shop"
|
||||
msgid "No tickets found"
|
||||
msgstr "Закрыть билетную кассу"
|
||||
msgstr "Нет доступных билетов"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Result"
|
||||
msgstr ""
|
||||
msgstr "Результат"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "This ticket requires special attention"
|
||||
@@ -226,7 +220,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
msgstr "Вход"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Exit"
|
||||
@@ -275,7 +269,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket not paid"
|
||||
msgstr ""
|
||||
msgstr "Билет не оплачен"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||
@@ -295,21 +289,19 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket already used"
|
||||
msgstr ""
|
||||
msgstr "Повторный проход по билету"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Information required"
|
||||
msgstr ""
|
||||
msgstr "Необходима информация"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
#| msgid "Unknown error."
|
||||
msgid "Unknown ticket"
|
||||
msgstr "Неизвестная ошибка."
|
||||
msgstr "Неизвестный билет"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket type not allowed here"
|
||||
msgstr ""
|
||||
msgstr "Здесь нельзя пройти по этому билету"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Entry not allowed"
|
||||
@@ -317,11 +309,11 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket code revoked/changed"
|
||||
msgstr ""
|
||||
msgstr "Код билета аннулирован и/или изменен"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket blocked"
|
||||
msgstr ""
|
||||
msgstr "Билет заблокирован"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Ticket not valid at this time"
|
||||
@@ -337,7 +329,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
msgid "Order not approved"
|
||||
msgstr ""
|
||||
msgstr "Заказ не подтвержден"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#, fuzzy
|
||||
@@ -377,32 +369,20 @@ msgstr "Закрыть"
|
||||
|
||||
#: pretix/static/pretixbase/js/addressform.js
|
||||
#: pretix/static/pretixpresale/js/ui/main.js
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Срок действия корзины истёк"
|
||||
msgstr "обязательно"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
#| "Depending on the size of your event, this might take up to a few minutes."
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
msgstr ""
|
||||
"Ваш запрос поставлен в очередь на сервере и будет обработан. В зависимости "
|
||||
"от масштаба вашего мероприятия это может занять до нескольких минут."
|
||||
"Ваш запрос принят в обработку. В зависимости от масштаба вашего мероприятия "
|
||||
"это может занять до нескольких минут."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
#| "Depending on the size of your event, this might take up to a few minutes."
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
"Ваш запрос поставлен в очередь на сервере и будет обработан. В зависимости "
|
||||
"от масштаба вашего мероприятия это может занять до нескольких минут."
|
||||
msgstr "Ваш запрос поставлен в очередь на сервере и будет обработан."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js
|
||||
msgid ""
|
||||
@@ -473,7 +453,7 @@ msgstr "Чтобы скопировать, нажмите Ctrl-C."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "Редактировать"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Visualize"
|
||||
@@ -532,7 +512,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
msgstr "Дубликат"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgctxt "entry_status"
|
||||
@@ -1215,15 +1195,15 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Tuesday"
|
||||
msgstr ""
|
||||
msgstr "Вторник"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Wednesday"
|
||||
msgstr ""
|
||||
msgstr "Среда"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Thursday"
|
||||
msgstr ""
|
||||
msgstr "Четверг"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Friday"
|
||||
@@ -1231,11 +1211,11 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Saturday"
|
||||
msgstr ""
|
||||
msgstr "Суббота"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "Sunday"
|
||||
msgstr ""
|
||||
msgstr "Воскресенье"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "January"
|
||||
@@ -1251,23 +1231,23 @@ msgstr "март"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "April"
|
||||
msgstr "апрель"
|
||||
msgstr "Апрель"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "May"
|
||||
msgstr "май"
|
||||
msgstr "Май"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "June"
|
||||
msgstr "июнь"
|
||||
msgstr "Июнь"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "July"
|
||||
msgstr "июль"
|
||||
msgstr "Июль"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "August"
|
||||
msgstr "август"
|
||||
msgstr "Август"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "September"
|
||||
@@ -1275,7 +1255,7 @@ msgstr "сентябрь"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "October"
|
||||
msgstr "октябрь"
|
||||
msgstr "Октябрь"
|
||||
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "November"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
|
||||
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
|
||||
"PO-Revision-Date: 2026-09-30 17:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"sv/>\n"
|
||||
"Language: sv\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
"X-Generator: Weblate 2026.9.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -19239,7 +19239,6 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/order_pay.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html
|
||||
#: pretix/presale/templates/pretixpresale/event/position_change.html
|
||||
#, fuzzy
|
||||
msgid "Continue"
|
||||
msgstr "Fortsätt"
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
|
||||
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
|
||||
"PO-Revision-Date: 2026-09-30 17:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/sv/>\n"
|
||||
"Language: sv\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
"X-Generator: Weblate 2026.9.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -133,7 +133,6 @@ msgstr "Mercado Pago"
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
#, fuzzy
|
||||
msgid "Continue"
|
||||
msgstr "Fortsätt"
|
||||
|
||||
|
||||
+1513
-1233
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-30 07:26+0000\n"
|
||||
"PO-Revision-Date: 2026-09-29 09:51+0000\n"
|
||||
"PO-Revision-Date: 2026-10-02 05:00+0000\n"
|
||||
"Last-Translator: Nate Horst <nate@agcthailand.org>\n"
|
||||
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"th/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 2026.9.1\n"
|
||||
"X-Generator: Weblate 2026.10\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -426,7 +426,7 @@ msgstr "แก้ไข"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Visualize"
|
||||
msgstr ""
|
||||
msgstr "แสดงภาพผัง"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid ""
|
||||
@@ -434,10 +434,12 @@ msgid ""
|
||||
"or variations are not contained in any of your rule parts so people with "
|
||||
"these tickets will not get in:"
|
||||
msgstr ""
|
||||
"กฎของคุณมีการกรองตามสินค้าหรือรูปแบบย่อยเสมอ แต่สินค้าหรือรูปแบบย่อยต่อไปนี้ไม่ได้ถูกรวมอยู่ในส่วนใ"
|
||||
"ดของกฎ คุณจึงส่งผลให้ผู้ที่ถือตั๋วเหล่านี้จะไม่สามารถผ่านเข้างานได้:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||
msgid "Please double-check if this was intentional."
|
||||
msgstr ""
|
||||
msgstr "โปรดตรวจสอบอีกครั้งว่าเป็นการดำเนินการโดยตั้งใจหรือไม่"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||
msgid "All of the conditions below (AND)"
|
||||
|
||||
@@ -40,3 +40,6 @@ class SendMailApp(AppConfig):
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
from . import tasks # NOQA
|
||||
|
||||
def uninstalled(self, event):
|
||||
event.sendmail_rules.update(enabled=False)
|
||||
|
||||
@@ -161,7 +161,12 @@ class OrderMailForm(BaseMailForm):
|
||||
),
|
||||
label=pgettext_lazy('sendmail_form', 'Restrict to products'),
|
||||
required=True,
|
||||
queryset=Item.objects.none()
|
||||
queryset=Item.objects.none(),
|
||||
help_text=pgettext_lazy(
|
||||
'sendmail_form',
|
||||
'There may be multiple mails sent out to the same mail address if one order contains multiple attendee '
|
||||
'products for it, if you restrict to products while also restricting mails to attendees only. '
|
||||
'This is intended, as every one of those get linked to their own separate order page restricted to only that product.')
|
||||
)
|
||||
filter_checkins = forms.BooleanField(
|
||||
label=_('Filter check-in status'),
|
||||
@@ -374,6 +379,11 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
||||
del self.fields['subevent']
|
||||
|
||||
self.fields['limit_products'].queryset = Item.objects.filter(event=self.event)
|
||||
self.fields['limit_products'].help_text = pgettext_lazy(
|
||||
'sendmail_form',
|
||||
'There may be multiple mails sent out to the same mail address if one order contains multiple attendee '
|
||||
'products for it, if you restrict to products while also restricting mails to attendees only. '
|
||||
'This is intended, as every one of those get linked to their own separate order page restricted to only that product.')
|
||||
|
||||
self.fields['schedule_type'] = forms.ChoiceField(
|
||||
label=_('Type of schedule time'),
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
from django.db import migrations
|
||||
from django.db.models import Exists, OuterRef
|
||||
|
||||
|
||||
def activate_plugin(apps, schema_editor):
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
ScheduledMail = apps.get_model("sendmail", "ScheduledMail")
|
||||
Rule = apps.get_model("sendmail", "Rule")
|
||||
|
||||
events = (
|
||||
Event.objects
|
||||
.exclude(plugins__icontains="pretix.plugins.sendmail")
|
||||
.filter(
|
||||
Exists(
|
||||
ScheduledMail.objects.filter(event=OuterRef('pk'), rule__enabled=True).exclude(
|
||||
state__in=['completed', 'missed'])
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for event in events:
|
||||
plugins_active = event.plugins.split(',')
|
||||
plugins_active.append('pretix.plugins.sendmail')
|
||||
event.plugins = ','.join(plugins_active)
|
||||
event.save(update_fields=['plugins'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("sendmail", "0012_remove_cross_event_scheduled_mails"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(activate_plugin),
|
||||
]
|
||||
@@ -19,6 +19,7 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
from dateutil.tz import datetime_exists
|
||||
@@ -39,6 +40,8 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.base import LoggingMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScheduledMail(models.Model):
|
||||
STATE_SCHEDULED = 'scheduled'
|
||||
@@ -115,7 +118,7 @@ class ScheduledMail(models.Model):
|
||||
|
||||
filter_orders_by_op = False
|
||||
op_qs = OrderPosition.objects.filter(
|
||||
order__event=self.event,
|
||||
order__event=e,
|
||||
canceled=False,
|
||||
)
|
||||
|
||||
@@ -167,14 +170,58 @@ class ScheduledMail(models.Model):
|
||||
for o in orders:
|
||||
with language(o.locale, e.settings.region):
|
||||
positions = list(o.positions.all())
|
||||
o_sent = False
|
||||
send_to_order = send_to_orders
|
||||
|
||||
try:
|
||||
ia = o.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = InvoiceAddress(order=o)
|
||||
|
||||
if send_to_orders and o.email:
|
||||
if send_to_attendees:
|
||||
parent_op = None
|
||||
sent_to_positions = set()
|
||||
for p in positions:
|
||||
if p.addon_to_id is None:
|
||||
# this op might have matching add-ons, so save for later
|
||||
parent_op = p
|
||||
elif not parent_op or p.addon_to_id != parent_op.id:
|
||||
# this op is an add-on, but not to the current parent_op
|
||||
# something got mixed up as add-ons should always come directly after their parent
|
||||
logger.warning(f"Add-ons are mixed up for position #{p.positionid} in order {o.full_code}")
|
||||
continue
|
||||
|
||||
if p.id not in position_ids:
|
||||
# not a matching op, just there for parent_op
|
||||
continue
|
||||
|
||||
if p.addon_to_id and (
|
||||
not p.attendee_email or p.attendee_email == parent_op.attendee_email
|
||||
):
|
||||
# if op is addon and either has no mail or the same as parent => send to parent
|
||||
p = parent_op
|
||||
|
||||
if p.pk in sent_to_positions:
|
||||
# this position already got an email
|
||||
continue
|
||||
|
||||
if not p.attendee_email or p.attendee_email == o.email:
|
||||
# no email or same as order => send to order
|
||||
send_to_order = True
|
||||
continue
|
||||
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
position=p,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
|
||||
sent_to_positions.add(p.pk)
|
||||
|
||||
if send_to_order and o.email:
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
@@ -184,38 +231,6 @@ class ScheduledMail(models.Model):
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
|
||||
if send_to_attendees:
|
||||
if not self.rule.all_products:
|
||||
positions = [p for p in positions if p.item_id in limit_products]
|
||||
if self.subevent_id:
|
||||
positions = [p for p in positions if p.subevent_id == self.subevent_id]
|
||||
|
||||
for p in positions:
|
||||
if p.id in position_ids:
|
||||
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
position=p,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
|
||||
elif not o_sent and o.email:
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
|
||||
self.last_successful_order_id = o.pk
|
||||
|
||||
|
||||
@@ -181,6 +181,7 @@ def sendmail_run_rules(sender, **kwargs):
|
||||
state=ScheduledMail.STATE_MISSED
|
||||
)
|
||||
for m_id in mails.filter(
|
||||
event__plugins__contains="pretix.plugins.sendmail",
|
||||
state__in=(ScheduledMail.STATE_SCHEDULED, ScheduledMail.STATE_FAILED),
|
||||
rule__enabled=True,
|
||||
event__live=True,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
@@ -43,6 +44,8 @@ from pretix.base.services.mail import mail
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.celery_app import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _chunks(lst, n):
|
||||
"""
|
||||
@@ -70,6 +73,8 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = InvoiceAddress(order=o)
|
||||
|
||||
parent_op = None
|
||||
sent_to_positions = set()
|
||||
if recipients in ('both', 'attendees'):
|
||||
for p in o.positions.annotate(
|
||||
any_checkins=Exists(
|
||||
@@ -85,10 +90,16 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
)
|
||||
),
|
||||
).prefetch_related('addons', 'subevent'):
|
||||
if p.addon_to_id is not None:
|
||||
|
||||
if p.addon_to_id is None:
|
||||
parent_op = p
|
||||
elif not parent_op or p.addon_to_id != parent_op.id:
|
||||
# this op is an add-on, but not to the current parent_op
|
||||
# something got mixed up as add-ons should always come directly after their parent
|
||||
logger.warning(f"Add-ons are mixed up for position #{p.positionid} in order {o.full_code}")
|
||||
continue
|
||||
|
||||
if p.item_id not in items and not any(a.item_id in items for a in p.addons.all()):
|
||||
if p.item_id not in items:
|
||||
continue
|
||||
|
||||
if filter_checkins:
|
||||
@@ -99,13 +110,11 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
if not allowed:
|
||||
continue
|
||||
|
||||
if not p.attendee_email:
|
||||
if recipients == 'attendees':
|
||||
send_to_order = True
|
||||
continue
|
||||
|
||||
if p.attendee_email == o.email and send_to_order:
|
||||
continue
|
||||
if p.addon_to_id and (
|
||||
not p.attendee_email or p.attendee_email == parent_op.attendee_email
|
||||
):
|
||||
# if op is addon and either has no mail or the same as parent => send to parent
|
||||
p = parent_op
|
||||
|
||||
if subevent and p.subevent_id != subevent:
|
||||
continue
|
||||
@@ -116,6 +125,18 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
if subevents_to and p.subevent.date_from >= subevents_to:
|
||||
continue
|
||||
|
||||
if not p.attendee_email:
|
||||
send_to_order = True
|
||||
continue
|
||||
|
||||
if p.pk in sent_to_positions:
|
||||
# this position already got an email
|
||||
continue
|
||||
|
||||
if p.attendee_email == o.email:
|
||||
send_to_order = True
|
||||
continue
|
||||
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
|
||||
outgoing_mail = mail(
|
||||
@@ -137,6 +158,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
user=user,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
sent_to_positions.add(p.pk)
|
||||
|
||||
if send_to_order and o.email:
|
||||
with language(o.locale, event.settings.region):
|
||||
|
||||
@@ -98,9 +98,7 @@ from pretix.presale.signals import (
|
||||
question_form_fields_overrides,
|
||||
)
|
||||
from pretix.presale.utils import customer_login
|
||||
from pretix.presale.views import (
|
||||
CartMixin, get_cart_is_free, get_cart_positions,
|
||||
)
|
||||
from pretix.presale.views import CartMixin, get_cart, get_cart_is_free
|
||||
from pretix.presale.views.cart import (
|
||||
_items_from_post_data, cart_session, create_empty_cart_id,
|
||||
get_or_create_cart_id,
|
||||
@@ -499,7 +497,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
self.request = request
|
||||
|
||||
# check whether addons are applicable
|
||||
if get_cart_positions(request).filter(item__addons__isnull=False).exists():
|
||||
if get_cart(request).filter(item__addons__isnull=False).exists():
|
||||
return True
|
||||
|
||||
# don't re-check whether cross-selling is applicable if we're already past the AddOnsStep
|
||||
@@ -525,7 +523,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
def is_completed(self, request, warn=False):
|
||||
if getattr(self, '_completed', None) is not None:
|
||||
return self._completed
|
||||
for cartpos in get_cart_positions(request).filter(addon_to__isnull=True).prefetch_related(
|
||||
for cartpos in get_cart(request).filter(addon_to__isnull=True).prefetch_related(
|
||||
'item__addons', 'item__addons__addon_category', 'addons', 'addons__item'
|
||||
):
|
||||
a = cartpos.addons.all()
|
||||
@@ -588,7 +586,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
formset = []
|
||||
quota_cache = {}
|
||||
item_cache = {}
|
||||
for cartpos in sorted(get_cart_positions(self.request).filter(addon_to__isnull=True).prefetch_related(
|
||||
for cartpos in sorted(get_cart(self.request).filter(addon_to__isnull=True).prefetch_related(
|
||||
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation',
|
||||
), key=lambda c: c.sort_key):
|
||||
formsetentry = {
|
||||
@@ -1092,18 +1090,26 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
return False
|
||||
|
||||
for cp in self._positions_for_questions:
|
||||
qc_cache = {
|
||||
qc.pk: qc for qq in cp.item.relevant_questionnaires for qc in qq.childlist
|
||||
answ = {
|
||||
aw.question_id: aw for aw in cp.answerlist
|
||||
}
|
||||
question_cache = {
|
||||
q.pk: q for q in cp.item.questions_to_ask
|
||||
}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in qc_cache:
|
||||
if parentid not in question_cache:
|
||||
return False
|
||||
parentqc = qc_cache[parentid]
|
||||
if parentqc.dependency_question_id and not question_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||
return False
|
||||
answer_values = cp.get_dependency_answer_values(parentqc)
|
||||
return answer_values and any(qval in answer_values for qval in qvals)
|
||||
if parentid not in answ:
|
||||
return False
|
||||
return (
|
||||
('True' in qvals and answ[parentid].answer == 'True')
|
||||
or ('False' in qvals and answ[parentid].answer == 'False')
|
||||
or (any(qval in [o.identifier for o in answ[parentid].options.all()] for qval in qvals))
|
||||
)
|
||||
|
||||
def question_is_required(q):
|
||||
return (
|
||||
@@ -1112,16 +1118,31 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
)
|
||||
|
||||
if not self.all_optional:
|
||||
for qq in cp.item.relevant_questionnaires:
|
||||
for qc in qq.childlist:
|
||||
if qc.user_datafield_id and question_is_required(qc) and qc.user_datafield_id not in cp.answer_cache:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if qc.system_datafield and question_is_required(qc) and not cp.get_system_answer(qc.system_datafield):
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
for q in cp.item.questions_to_ask:
|
||||
if question_is_required(q) and q.id not in answ:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_names_required', as_type=bool) \
|
||||
and not cp.attendee_name_parts:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_emails_required', as_type=bool) \
|
||||
and cp.attendee_email is None:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_company_required', as_type=bool) \
|
||||
and cp.company is None:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_addresses_required', as_type=bool) \
|
||||
and (cp.street is None and cp.city is None and cp.country is None):
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
|
||||
responses = question_form_fields.send(sender=self.request.event, position=cp)
|
||||
form_data = cp.meta_info_data.get('question_form_data', {})
|
||||
@@ -1289,7 +1310,7 @@ class PaymentStep(CartMixin, TemplateFlowStep):
|
||||
|
||||
@cached_property
|
||||
def _total_order_value(self):
|
||||
cart = get_cart_positions(self.request)
|
||||
cart = get_cart(self.request)
|
||||
try:
|
||||
fees = get_fees(
|
||||
event=self.request.event, request=self.request, invoice_address=self.invoice_address,
|
||||
@@ -1481,7 +1502,7 @@ class PaymentStep(CartMixin, TemplateFlowStep):
|
||||
messages.error(request, _('Please select a payment method to proceed.'))
|
||||
return False
|
||||
|
||||
cart = get_cart_positions(self.request)
|
||||
cart = get_cart(self.request)
|
||||
try:
|
||||
fees = get_fees(
|
||||
event=self.request.event,
|
||||
@@ -1521,7 +1542,7 @@ class PaymentStep(CartMixin, TemplateFlowStep):
|
||||
def is_applicable(self, request):
|
||||
self.request = request
|
||||
|
||||
for cartpos in get_cart_positions(self.request):
|
||||
for cartpos in get_cart(self.request):
|
||||
if cartpos.requires_approval(invoice_address=self.invoice_address):
|
||||
if 'payments' in self.cart_session:
|
||||
del self.cart_session['payments']
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for q in line.questions %}
|
||||
<dt>{{ q.label }}</dt>
|
||||
<dt>{{ q.question }}</dt>
|
||||
<dd>
|
||||
{% include "pretixpresale/event/fragment_question_answer.html" with request=request question=q answer=q.answer %}
|
||||
</dd>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{% load safelink %}
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% if not answer and not question %}
|
||||
{% elif answer %}
|
||||
{% if answer %}
|
||||
{% if answer.file %}
|
||||
<span class="fa fa-file" aria-hidden="true"></span>
|
||||
<a href="{{ answer.frontend_file_url }}?token={% answer_token request answer %}">
|
||||
|
||||
@@ -41,7 +41,7 @@ from itertools import groupby
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Sum
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Sum
|
||||
from django.utils import translation
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
@@ -52,9 +52,8 @@ from pretix.base.i18n import get_language_without_region, set_region
|
||||
from pretix.base.middleware import get_supported_language
|
||||
from pretix.base.models import (
|
||||
CartPosition, Customer, InvoiceAddress, ItemAddOn, OrderFee, Question,
|
||||
QuestionAnswer, Questionnaire, QuestionOption, TaxRule,
|
||||
QuestionAnswer, QuestionOption, TaxRule,
|
||||
)
|
||||
from pretix.base.models.items import QuestionnaireChild
|
||||
from pretix.base.models.orders import CheckoutSession
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.services.pricing import apply_rounding
|
||||
@@ -96,7 +95,7 @@ class CartMixin:
|
||||
"""
|
||||
A list of this users cart position
|
||||
"""
|
||||
return list(get_cart_positions(self.request))
|
||||
return list(get_cart(self.request))
|
||||
|
||||
@cached_property
|
||||
def cart_session(self):
|
||||
@@ -136,7 +135,7 @@ class CartMixin:
|
||||
if queryset is not None:
|
||||
prefetch = []
|
||||
if answers:
|
||||
prefetch.append('item__questionnaires')
|
||||
prefetch.append('item__questions')
|
||||
prefetch.append(Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options')))
|
||||
|
||||
cartpos = queryset.order_by(
|
||||
@@ -213,7 +212,7 @@ class CartMixin:
|
||||
# Never group if the position has add-ons
|
||||
pos.pk not in has_addons and
|
||||
# Never group if we have answers to show
|
||||
(not answers or (not has_attendee_data and not bool(pos.item.questionnaires.all()))) and # do not use .exists() to re-use prefetch cache
|
||||
(not answers or (not has_attendee_data and not bool(pos.item.questions.all()))) and # do not use .exists() to re-use prefetch cache
|
||||
# Never group when we have a final order and a gift card code
|
||||
(isinstance(pos, CartPosition) or not pos.item.issue_giftcard)
|
||||
)
|
||||
@@ -251,7 +250,7 @@ class CartMixin:
|
||||
group.price_for_input_net = group.net_price_before_rounding + sum(a.net_price_before_rounding for a in has_addons[group.pk])
|
||||
|
||||
if answers:
|
||||
group.cache_answers(questionnaire_type=Questionnaire.QuestionnaireType.ORDER_POSITION_SALE, sales_channel=self.request.sales_channel)
|
||||
group.cache_answers(all=False)
|
||||
group.additional_answers = pos_additional_fields.get(group.pk)
|
||||
positions.append(group)
|
||||
|
||||
@@ -400,7 +399,7 @@ def cart_exists(request):
|
||||
return bool(request._cart_cache)
|
||||
|
||||
|
||||
def get_cart_positions(request):
|
||||
def get_cart(request):
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
if not hasattr(request, '_cart_cache'):
|
||||
@@ -408,11 +407,8 @@ def get_cart_positions(request):
|
||||
if not cart_id:
|
||||
request._cart_cache = CartPosition.objects.none()
|
||||
else:
|
||||
qqs = request.event.questionnaires.all()
|
||||
qqs = qqs.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=request.sales_channel.identifier),
|
||||
type='PS'
|
||||
)
|
||||
qqs = request.event.questions.all()
|
||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False, container_type=Question.ContainerType.ORDERPOSITION)
|
||||
request._cart_cache = CartPosition.objects.filter(
|
||||
cart_id=cart_id, event=request.event
|
||||
).annotate(
|
||||
@@ -433,23 +429,18 @@ def get_cart_positions(request):
|
||||
Prefetch('answers',
|
||||
QuestionAnswer.objects.prefetch_related('options'),
|
||||
to_attr='answerlist'),
|
||||
Prefetch('item__questionnaires',
|
||||
Prefetch('item__questions',
|
||||
qqs.prefetch_related(
|
||||
Prefetch('children', QuestionnaireChild.objects.prefetch_related(
|
||||
Prefetch('user_datafield', Question.objects.prefetch_related(
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
))
|
||||
),
|
||||
to_attr='childlist')
|
||||
),
|
||||
to_attr='relevant_questionnaires')
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
)
|
||||
by_id = {cp.pk: cp for cp in request._cart_cache}
|
||||
for cp in request._cart_cache:
|
||||
@@ -460,9 +451,6 @@ def get_cart_positions(request):
|
||||
return request._cart_cache
|
||||
|
||||
|
||||
get_cart = get_cart_positions # legacy compatibility
|
||||
|
||||
|
||||
def get_cart_total(request):
|
||||
"""
|
||||
Use the following pattern instead::
|
||||
@@ -513,7 +501,7 @@ def get_cart_is_free(request):
|
||||
|
||||
if not hasattr(request, '_cart_free_cache'):
|
||||
cs = cart_session(request)
|
||||
pos = get_cart_positions(request)
|
||||
pos = get_cart(request)
|
||||
ia = get_cart_invoice_address(request)
|
||||
try:
|
||||
fees = get_fees(event=request.event, request=request, invoice_address=ia,
|
||||
|
||||
@@ -183,7 +183,6 @@ class OrderPositionDetailMixin(NoSearchIndexViewMixin):
|
||||
def position(self):
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
addon_to__isnull=True,
|
||||
order__code=self.kwargs['order'],
|
||||
positionid=self.kwargs['position']
|
||||
).select_related('order', 'order__event')
|
||||
|
||||
@@ -87,8 +87,6 @@ dialog::backdrop {
|
||||
100% { transform: skewX(0deg); }
|
||||
}
|
||||
|
||||
.modal-card.no-padding, .modal-card.no-padding .modal-card-content { padding: 0; }
|
||||
.modal-card.no-scroll { overflow: hidden; }
|
||||
|
||||
|
||||
/* Legacy dialogs (still used for #ajaxerr and #popupmodal) */
|
||||
|
||||
@@ -117,6 +117,16 @@ input[type=number]::-webkit-outer-spin-button {
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.btn.disabled, .btn[disabled], fieldset[disabled] .btn {
|
||||
box-shadow: 0px 0px 0px 1px #ccc inset, inset 0 1px 3px 0 #d9d9d9;
|
||||
background: #eee;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.btn-link.disabled, .btn-link[disabled], fieldset[disabled] .btn-link {
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.btn-group-flex { display: flex; }
|
||||
.btn-group-flex > .btn { flex: 1; }
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
try {
|
||||
window.parent.postMessage({
|
||||
type: 'pretix:notify-parent',
|
||||
data: JSON.parse(document.getElementById('notify_info').textContent),
|
||||
}, location.origin)
|
||||
} catch (e) {
|
||||
console.error('Could not post message to parent.', e)
|
||||
}
|
||||
@@ -72,10 +72,6 @@ $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
|
||||
})
|
||||
|
||||
let form_handlers = function (el) {
|
||||
// Prevent running multiple times on the same elements
|
||||
if (el.is('[data-formset-form-handlers-created]')) return
|
||||
el.find('[data-formset-form]').attr('data-formset-form-handlers-created', 'true')
|
||||
|
||||
el.trigger('rescan.areYouSure')
|
||||
el.find('[data-formset]').formset(
|
||||
{
|
||||
@@ -446,15 +442,6 @@ let form_handlers = function (el) {
|
||||
})
|
||||
})
|
||||
|
||||
function addClearButton($input) {
|
||||
var $btn = $("<button class='clear-input-button'><span class='fa fa-times'></span></button>").insertAfter($input).on('click', function(e) {
|
||||
$input.val('').trigger('input').trigger('change'); e.preventDefault()
|
||||
}).toggle($input.val() !== '')
|
||||
$input.on('input', function() {
|
||||
$btn.toggle($input.val() !== '')
|
||||
})
|
||||
}
|
||||
|
||||
el.find('div.scrolling-choice:not(.no-search)').each(function () {
|
||||
if ($(this).find('input[type=text]').length > 0) {
|
||||
return
|
||||
@@ -462,7 +449,6 @@ let form_handlers = function (el) {
|
||||
let $menu = $('<div>').addClass('choice-options-menu')
|
||||
let $inp_search = $('<input>').addClass('form-control').attr('type', 'text').attr('placeholder', gettext('Search query'))
|
||||
$menu.append($inp_search)
|
||||
addClearButton($inp_search)
|
||||
$(this).prepend($menu)
|
||||
|
||||
$inp_search.on('keyup change', function (e) {
|
||||
@@ -491,7 +477,6 @@ let form_handlers = function (el) {
|
||||
$menu.append($lbl_tgl)
|
||||
}
|
||||
$(this).prepend($menu)
|
||||
addClearButton($inp_search)
|
||||
|
||||
$(this).find('.choice-options-none').click(function (e) {
|
||||
$(this).closest('.scrolling-multiple-choice').find('input[type=checkbox]:not(.menu-checkbox)').prop('checked', false)
|
||||
@@ -751,27 +736,6 @@ let form_handlers = function (el) {
|
||||
questions_toggle_dependent()
|
||||
questions_init_photos(el)
|
||||
|
||||
el.find("[data-iframe-dialog]").on("click", function(e) {
|
||||
let url;
|
||||
if (this.tagName === "A" && !this.hasAttribute("data-iframe-dialog-url")) {
|
||||
url = this.getAttribute("href")
|
||||
url += (url.includes('?') ? '&' : '?') + 'notify_parent=true'
|
||||
} else {
|
||||
url = this.getAttribute("data-iframe-dialog-url")
|
||||
}
|
||||
show_iframe_dialog(url, (data) => {
|
||||
if (this.getAttribute("data-iframe-dialog-target")) {
|
||||
var $target = findDependency(this.getAttribute("data-iframe-dialog-target"), this)
|
||||
if ($target.is('select')) {
|
||||
$target.append(new Option(data.object_str, data.object, false, true)).trigger('change')
|
||||
}
|
||||
}
|
||||
})
|
||||
e.preventDefault()
|
||||
})
|
||||
}
|
||||
|
||||
function setup_placeholders() {
|
||||
let lastFocusedInput
|
||||
$(document).on('focusin', 'input, textarea', function (e) {
|
||||
lastFocusedInput = e.target
|
||||
@@ -849,7 +813,13 @@ function setup_basics (el) {
|
||||
return
|
||||
}
|
||||
|
||||
$('<li><a href="#' + scrollTarget.id + '">' + $.trim(label) + '</a> – ' + description + '</li>')
|
||||
$('<li></li>')
|
||||
.append(
|
||||
$('<a></a>').attr("href", "#" + scrollTarget.id).text($.trim(label))
|
||||
)
|
||||
.append(
|
||||
document.createTextNode(' - ' + description)
|
||||
)
|
||||
.appendTo(alert.querySelector('ul') || $('<ul>').appendTo(alert))
|
||||
.find('a').on('click', function (e) {
|
||||
$panel.collapse('show')
|
||||
@@ -1115,7 +1085,6 @@ $(function () {
|
||||
|
||||
setup_basics($('body'))
|
||||
form_handlers($('body'))
|
||||
setup_placeholders()
|
||||
$(document).trigger('pretix:bind-forms')
|
||||
|
||||
$('#ajaxerr').on('click', '.ajaxerr-close', ajaxErrDialog.hide)
|
||||
@@ -1158,66 +1127,3 @@ $(function () {
|
||||
return $(this).find('button:not([type=button]), input[type=submit]').length > 0
|
||||
}).areYouSure({ message: gettext('You have unsaved changes!') })
|
||||
})
|
||||
function show_iframe_dialog(url, callback) {
|
||||
function messageEvent(e) {
|
||||
if (e.origin === location.origin && e.data.type === 'pretix:dialog-loaded') {
|
||||
$dlg.find("iframe").attr("height", Math.min(window.innerHeight - 120, e.data.contentHeight|0)).css("visibility", "visible")
|
||||
$dlg.find("center").remove()
|
||||
}
|
||||
if (e.origin === location.origin && e.data.type === 'pretix:notify-parent') {
|
||||
$dlg[0].close()
|
||||
if (!callback(e.data.data)) {
|
||||
if (e.data.data.messages?.length) {
|
||||
alert(e.data.data.messages.map(m => m.message).join('\n\n'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var $dlg = $('<dialog class="modal-card no-padding no-scroll" closedby="any"><center><i class="fa fa-cog big-rotating-icon"></i></center><iframe height="100" width="100%"></iframe></div>')
|
||||
.css('max-width', '60em')
|
||||
$dlg.find("iframe").attr("src", url).css("visibility", "hidden").css("border", "0")
|
||||
window.addEventListener('message', messageEvent)
|
||||
$dlg.appendTo("body")
|
||||
$dlg.on('close', function() {
|
||||
window.removeEventListener('message', messageEvent)
|
||||
$dlg.remove()
|
||||
})
|
||||
$dlg[0].showModal()
|
||||
}
|
||||
function setup_dialog_frame_events() {
|
||||
var dom_ready = false, parent_responded = false
|
||||
function notify_dialog_loaded() {
|
||||
console.log('ready', window.innerWidth, window.innerHeight, $('#page-wrapper > .container-fluid').outerHeight() + 20)
|
||||
window.parent.postMessage({
|
||||
type: 'pretix:dialog-loaded',
|
||||
contentHeight: $('#page-wrapper > .container-fluid').outerHeight() + 20,
|
||||
title: document.title,
|
||||
}, location.origin)
|
||||
}
|
||||
window.addEventListener('message', function(e) {
|
||||
if (e.source === window) return // don't handle messages from ourselves
|
||||
if (e.origin !== location.origin) return // only handle messages from same origin
|
||||
|
||||
if (e.data.type === 'pretix:dialog-handshake') {
|
||||
// we are inside an iframe in a dialog
|
||||
if (!parent_responded) {
|
||||
window.document.documentElement.classList.add('in-iframe')
|
||||
if (dom_ready) setTimeout(notify_dialog_loaded, 1)
|
||||
parent_responded = true
|
||||
}
|
||||
} else if (e.data.type === 'pretix:dialog-loading') {
|
||||
// we are parent to a dialog
|
||||
e.source.postMessage({ type: 'pretix:dialog-handshake' })
|
||||
}
|
||||
})
|
||||
$(function () {
|
||||
if (parent_responded) setTimeout(notify_dialog_loaded, 1)
|
||||
dom_ready = true
|
||||
})
|
||||
try {
|
||||
window.parent.postMessage({
|
||||
type: 'pretix:dialog-loading',
|
||||
}, location.origin)
|
||||
} catch {}
|
||||
}
|
||||
setup_dialog_frame_events()
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Editor from './Editor.vue';
|
||||
import {provide, ref} from "vue";
|
||||
import * as api from "./api";
|
||||
import {sort, numericComp, groupBy} from './helper';
|
||||
import {Category, GroupedItems, Item, SalesChannel} from "./model";
|
||||
|
||||
const sales_channels_list = ref<SalesChannel[]>();
|
||||
const grouped_items = ref<GroupedItems>()
|
||||
|
||||
provide('pretix:env:organizer:sales_channels', sales_channels_list)
|
||||
provide('pretix:env:event:grouped_items', grouped_items)
|
||||
|
||||
async function loadEnvironmentData() {
|
||||
sales_channels_list.value = await api.getSalesChannels();
|
||||
const items_list = await api.getItems();
|
||||
const categories_list = await api.getCategories();
|
||||
const categories = Object.fromEntries(categories_list.map(cat => [cat.id, cat]));
|
||||
categories['null'] = { position: -1, internal_name: gettext('Uncategorized'), name: null, id: null };
|
||||
sort(items_list, numericComp(item => categories[item.category]?.position), numericComp(item => item.position));
|
||||
grouped_items.value = [...groupBy(items_list, item => categories[item.category])]
|
||||
}
|
||||
loadEnvironmentData()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<Editor/>
|
||||
|
||||
<template #fallback>
|
||||
<div class="big-load-indicator"><i class="fa fa-cog big-rotating-icon"></i></div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
<style>
|
||||
.big-load-indicator { text-align: center; padding-bottom: 100px }
|
||||
|
||||
.progressBar { position: fixed; top: 0; left: 0; right: 0; z-index:1000000; pointer-events: none; }
|
||||
.progressBar.local { position: absolute; }
|
||||
.progressBar .progressBar_progress { height: 5px; background: #0091EA; border-bottom: #026099; }
|
||||
.progressBar .progressBar_progress.indeterminate {
|
||||
width: 30%; animation: slide 5s forwards;
|
||||
}
|
||||
.progressBarText { width: 100%; color: white; text-shadow: 1px 1px 1px black, -1px -1px 1px black;
|
||||
font-weight: bold; padding-left: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@keyframes slide {
|
||||
0% { width: 20%; }
|
||||
30% { width: 50%; }
|
||||
100% { width: 60%; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {gettext} from "./gettextstub";
|
||||
import NativeDialog from "./NativeDialog.vue";
|
||||
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
'defaultUrl': {type: String, default: null},
|
||||
'maxWidth': {type: String, default: '43em'},
|
||||
})
|
||||
const emit = defineEmits(['confirm'])
|
||||
const dlgDjangoDialog = ref()
|
||||
const url = ref(props.defaultUrl)
|
||||
const frameHeight = ref(400)
|
||||
const frameLoading = ref(true)
|
||||
|
||||
function messageEvent(e: MessageEvent) {
|
||||
console.log('messageEvent', e.origin, e.source, e.data)
|
||||
if (!dlgDjangoDialog.value.visible) return
|
||||
if (e.origin === location.origin && e.data.type === 'pretix:dialog-loaded') {
|
||||
frameHeight.value = Math.min(window.innerHeight - 120, e.data.contentHeight|0)
|
||||
frameLoading.value = false
|
||||
}
|
||||
if (e.origin === location.origin && e.data.type === 'pretix:notify-parent') {
|
||||
dlgDjangoDialog.value.close()
|
||||
emit('confirm', e.data.data)
|
||||
if (e.data.data.messages) {
|
||||
alert(e.data.data.messages.map(m => m.message).join('\n\n'))
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', messageEvent)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', messageEvent)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
dialog: dlgDjangoDialog,
|
||||
open: (newUrl) => {
|
||||
frameLoading.value = true
|
||||
frameHeight.value = 400
|
||||
url.value = newUrl
|
||||
dlgDjangoDialog.value.show()
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeDialog ref="dlgDjangoDialog" class="modal-card" :no-padding="true" :no-scroll="true" :max-width="maxWidth">
|
||||
<div :style="{'height': frameHeight + 'px'}">
|
||||
<div class="frame-load-indicator" v-if="frameLoading"><i class="fa fa-cog big-rotating-icon"></i></div>
|
||||
<iframe :src="url" v-if="dlgDjangoDialog.visible" :height="frameHeight" :style="{'opacity': frameLoading ? '0' : '1'}"></iframe>
|
||||
</div>
|
||||
</NativeDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.frame-load-indicator { text-align: center; }
|
||||
iframe { width: 100%; border: 0; }
|
||||
</style>
|
||||
@@ -1,276 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import QuestionnaireElement from './QuestionnaireElement.vue';
|
||||
import * as api from './api';
|
||||
import {Datafield, Questionnaire} from './model';
|
||||
import {i18n_any, sort, numericComp, groupBy, SYSTEM_DATAFIELDS} from './helper';
|
||||
import { gettext } from './gettextstub';
|
||||
import {inject, nextTick, onMounted, onUnmounted, ref} from 'vue';
|
||||
import { SlickList, SlickItem } from 'vue-slicksort';
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
|
||||
const grouped_items = inject('pretix:env:event:grouped_items')
|
||||
|
||||
type QuestionnaireMaybeUnsaved = Omit<Questionnaire, 'id'> & { _new_id?: number, id?: number };
|
||||
const order_questionnaires = ref<QuestionnaireMaybeUnsaved[]>();
|
||||
const position_questionnaires = ref<QuestionnaireMaybeUnsaved[]>();
|
||||
const order_datafields = ref<Datafield[]>();
|
||||
const position_datafields = ref<Datafield[]>();
|
||||
|
||||
let lastDataRefresh = 0
|
||||
async function refreshQuestionnaireList () {
|
||||
const all_questionnaires: QuestionnaireMaybeUnsaved[] = await api.getQuestionnaires();
|
||||
|
||||
order_questionnaires.value = all_questionnaires.filter(q => q.type.startsWith('O'))
|
||||
position_questionnaires.value = all_questionnaires.filter(q => q.type.startsWith('P'))
|
||||
lastDataRefresh = Date.now()
|
||||
}
|
||||
async function refreshDatafieldList () {
|
||||
order_datafields.value = await api.getDatafields('O')
|
||||
position_datafields.value = (await api.getDatafields('P')).concat(Object.values(SYSTEM_DATAFIELDS))
|
||||
console.log('datafield list refreshed')
|
||||
}
|
||||
await Promise.all([refreshQuestionnaireList(), refreshDatafieldList()])
|
||||
|
||||
function saveQuestionnaire(questionnaire) {
|
||||
let result;
|
||||
questionnaire._loading = true
|
||||
if (questionnaire.id) {
|
||||
result = api.updateQuestionnaire(questionnaire.id, questionnaire)
|
||||
} else {
|
||||
result = api.createQuestionnaire(questionnaire)
|
||||
}
|
||||
result = result.then(d => {
|
||||
questionnaire.id = d.id
|
||||
questionnaire.children = d.children
|
||||
questionnaire._err_mes = null
|
||||
questionnaire._loading = false
|
||||
return d;
|
||||
}, err => {
|
||||
questionnaire._err_mes = err
|
||||
questionnaire._loading = false
|
||||
return err;
|
||||
})
|
||||
return result;
|
||||
}
|
||||
function addPositionQuestionnaire () {
|
||||
position_questionnaires.value.push({
|
||||
all_sales_channels: true, children: [], limit_sales_channels: [], position: position_questionnaires.value.length,
|
||||
items: [], internal_name: "Unnamed questionnaire", type: "PS",
|
||||
_new_id: Date.now(),
|
||||
});
|
||||
}
|
||||
function addOrderQuestionnaire () {
|
||||
order_questionnaires.value.push({
|
||||
all_sales_channels: true, children: [], limit_sales_channels: [], position: order_questionnaires.value.length,
|
||||
items: [], internal_name: "Unnamed questionnaire", type: "OS",
|
||||
_new_id: Date.now(),
|
||||
});
|
||||
}
|
||||
async function saveData () {
|
||||
using pb = ProgressBar.show('saving questionnaires')
|
||||
let promises = [];
|
||||
let i = 0;
|
||||
for (const questionnaire of order_questionnaires.value) {
|
||||
questionnaire.position = i++;
|
||||
promises.push(saveQuestionnaire(questionnaire))
|
||||
}
|
||||
i = 0;
|
||||
for (const questionnaire of position_questionnaires.value) {
|
||||
questionnaire.position = i++;
|
||||
promises.push(saveQuestionnaire(questionnaire))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
/*
|
||||
export default {
|
||||
components: {
|
||||
QuestionnaireElement, SlickList, SlickItem,
|
||||
},
|
||||
methods: {
|
||||
i18n_any,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
order_questionnaires,
|
||||
position_questionnaires,
|
||||
order_datafields,
|
||||
position_datafields,
|
||||
items: items_list,
|
||||
grouped_items,
|
||||
categories,
|
||||
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
function windowFocused() {
|
||||
if (Date.now() - lastDataRefresh > 30000) {
|
||||
if (document.querySelector("dialog[open]")) {
|
||||
// TODO this should not be skipped completely but handled in a more robust way
|
||||
// currently dialogs are very flaky if the whole questionnaires list gets replaced underneath
|
||||
// them (they will stay open, but saving them will cause weird problems i don't fully understand yet)
|
||||
console.log('skipping refresh because a dialog is open')
|
||||
return
|
||||
}
|
||||
console.log('refreshing to avoid overwriting with older data on edit')
|
||||
refreshQuestionnaireList()
|
||||
}
|
||||
}
|
||||
onMounted(() => window.addEventListener('focus', windowFocused))
|
||||
onUnmounted(() => window.removeEventListener('focus', windowFocused))
|
||||
|
||||
const selected_product = ref("")
|
||||
const preview_mode = ref(false)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hidden-questionnaire { opacity: 0.3; }
|
||||
|
||||
.questionnaires-list {
|
||||
padding: 20px 190px 20px 0;
|
||||
background: linear-gradient(to right, #fff 0, #fff calc(100% - 172px), #ddd calc(100% - 171px), #f6f2f8 calc(100% - 171px), #fff calc(100% - 100px), #fff 100%);
|
||||
}
|
||||
.question-edit-buttons { float:right; }
|
||||
.question-edit-buttons div { position: absolute; margin-left: 10px; min-width: 100px; }
|
||||
.question-edit-buttons button { }
|
||||
.form-group { margin-bottom: 30px }
|
||||
|
||||
.questionnaires-editor.product-selected .questionnaire-panel .questionnaire-panel-heading {}
|
||||
|
||||
.questionnaires-editor:not(.preview-mode) .questionnaire-panel .questionnaire-panel-heading .editor-row { border-top: 1px solid rgb(175 175 175 / 0.3); }
|
||||
.questionnaires-editor:not(.preview-mode) .questionnaire-panel[open] .questionnaire-panel-heading .editor-row { border-bottom: 1px solid rgb(216 216 216 / 0.3); }
|
||||
|
||||
.questionnaires-editor.preview-mode .questionnaire-panel .questionnaire-panel-heading { color: #888; margin-top: 5px; border-top: 1px dashed rgb(175 175 175 / 0.3); border-bottom: 1px dashed rgb(216 216 216 / 0.3); font-style: italic; padding: 0; }
|
||||
|
||||
.questionnaires-editor .editor-row:not(:hover):not(:focus-within) .btn.btn-default,
|
||||
.questionnaires-editor .editor-action-row:not(:hover):not(:focus-within) .btn.btn-default{ box-shadow: 0 0 0 0 #eeeeee; background: transparent; color: #555; }
|
||||
|
||||
.filter-row { background: #f8e6ff; border: 1px solid #e3cbed; padding: 10px; }
|
||||
|
||||
.debuginfo { font-size: 70%; background: rgba(200, 200, 200, 0.5); }
|
||||
.dependency-info { position: absolute; }
|
||||
.dependency-info > span { }
|
||||
|
||||
.category-header { margin: 8px 0 -5px 0; font-weight: bold; color: #737373; }
|
||||
|
||||
.editor-row { display: flex; width: calc(100% + 190px); background: transparent; }
|
||||
.editor-row:hover { background: rgba(255,233,244,0.3); }
|
||||
.editor-preview-area { flex: 1; padding-left: 10px; padding-right: 40px; }
|
||||
.editor-action-area { width: 160px; padding-left: 20px; }
|
||||
.questionnaire-panel-heading .editor-preview-area { padding-block: 11px; }
|
||||
.questionnaire-panel-heading .editor-action-area { padding-block: 5px; }
|
||||
.questionnaire-panel-body .editor-preview-area, .questionnaire-panel-body .editor-action-area { padding-block: 10px; }
|
||||
.preview-mode .editor-preview-area { padding-right: 50px; }
|
||||
|
||||
.questionnaires-editor.questionnaires-editor .form-group { margin-bottom: 0px; }
|
||||
|
||||
.questionnaire-panel .questionnaire-panel-heading .editor-preview-area::before {
|
||||
margin-top: -.5em;
|
||||
content: "";
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 150ms ease-in 0s;
|
||||
}
|
||||
.questionnaire-panel[open] .questionnaire-panel-heading .editor-preview-area::before {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.editor-action-row { padding-top: 10px; width: calc(100% + 190px); padding-right: 170px }
|
||||
.questionnaires-list > .editor-action-row { border-top: 1px solid rgb(175 175 175 / 0.3); padding-top: 25px; padding-bottom: 10px }
|
||||
</style>
|
||||
<template>
|
||||
<div class="questionnaires-editor" v-if="!preview_mode">
|
||||
<div class="filter-row">
|
||||
Order questionnaires
|
||||
</div>
|
||||
<div class="questionnaires-list">
|
||||
<SlickList axis="y" v-model:list="order_questionnaires" useDragHandle appendTo="#orderQuestionnaireListParent" id="orderQuestionnaireListParent" @update:list="saveData()">
|
||||
<SlickItem v-for="(questionnaire, index) in order_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
|
||||
<QuestionnaireElement
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="order_datafields"
|
||||
:grouped_items="null"
|
||||
:selected_product="null"
|
||||
:preview_mode="false"
|
||||
@update="saveQuestionnaire(questionnaire)"
|
||||
@invalidate:datafields="refreshDatafieldList()"
|
||||
@invalidate:questionnaires="refreshQuestionnaireList()"/>
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
<div class="editor-action-row form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-push-3">
|
||||
<button class="btn btn-default" @click="addOrderQuestionnaire()"><i class="fa fa-plus"></i> {{ gettext('New questionnaire') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="`questionnaires-editor ${selected_product ? 'product-selected':''} ${preview_mode ? 'preview-mode':''}`">
|
||||
<div class="filter-row" v-if="preview_mode">
|
||||
<button class="btn btn-default" @click="preview_mode = false"><span class="fa fa-chevron-left"></span> {{ gettext('Back') }}</button>
|
||||
Previewing questionnaires for product {{ selected_product }}
|
||||
</div>
|
||||
<div class="filter-row" v-else>
|
||||
Questionnaires for product:
|
||||
<select v-model="selected_product">
|
||||
<option value="">(all)</option>
|
||||
<optgroup v-for="[category, items] in grouped_items" :label="category.internal_name || i18n_any(category.name)">
|
||||
<option v-for="item in items" :value="item.id">
|
||||
{{ item.internal_name || i18n_any(item.name) }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<button class="btn btn-default" @click="preview_mode = true" :disabled="!selected_product"> {{ gettext('Preview') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="questionnaires-list" v-if="preview_mode && selected_product">
|
||||
<details class="panel panel-default details-open" open>
|
||||
<summary class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<strong>Product {{ selected_product }}</strong>
|
||||
</h4>
|
||||
</summary>
|
||||
|
||||
<QuestionnaireElement v-for="(questionnaire, index) in position_questionnaires.filter(q => q.items.indexOf(selected_product as any) !== -1)"
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="position_datafields"
|
||||
:grouped_items="grouped_items"
|
||||
:selected_product="selected_product"
|
||||
:preview_mode="true"
|
||||
@update="saveQuestionnaire(questionnaire)"
|
||||
@invalidate:datafields="refreshDatafieldList()"
|
||||
@invalidate:questionnaires="refreshQuestionnaireList()"/>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="questionnaires-list" v-else>
|
||||
<SlickList axis="y" v-model:list="position_questionnaires" useDragHandle appendTo="#questionnaireListParent" id="questionnaireListParent" @update:list="saveData()">
|
||||
<SlickItem v-for="(questionnaire, index) in position_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
|
||||
<QuestionnaireElement
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="position_datafields"
|
||||
:grouped_items="grouped_items"
|
||||
:selected_product="selected_product"
|
||||
:preview_mode="false"
|
||||
@update="saveQuestionnaire(questionnaire)"
|
||||
@invalidate:datafields="refreshDatafieldList()"
|
||||
@invalidate:questionnaires="refreshQuestionnaireList()"/>
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
<div v-if="!preview_mode" class="editor-action-row form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-push-3">
|
||||
<button class="btn btn-default" @click="addPositionQuestionnaire()"><i class="fa fa-plus"></i> {{ gettext('New questionnaire') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {getEventLocales} from "./api"
|
||||
|
||||
const locales = getEventLocales();
|
||||
|
||||
const props = defineProps<{ value: any, id?: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="i18n-form-group" :id="id">
|
||||
<textarea v-for="locale in locales" cols="40" rows="2" :lang="locale" dir="ltr" class="form-control" title="Englisch" :id="`${id}_${locale}`" :placeholder="locale" v-model="value[locale]"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {getEventLocales} from "./api"
|
||||
|
||||
const locales = getEventLocales();
|
||||
|
||||
const props = defineProps<{ value: any, id?: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="i18n-form-group" :id="id">
|
||||
<input v-for="locale in locales" cols="40" rows="2" :lang="locale" dir="ltr" class="form-control" title="Englisch" :id="`${id}_${locale}`" :placeholder="locale" v-model="value[locale]">
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,58 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useId } from 'vue';
|
||||
|
||||
const dialog = ref<HTMLDialogElement>();
|
||||
|
||||
const props = defineProps({
|
||||
classes: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
title: {type: String, default: ''},
|
||||
noPadding: {type: Boolean, default: false},
|
||||
noScroll: {type: Boolean, default: true},
|
||||
maxWidth: {type: String, default: '43em'},
|
||||
});
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
const showModal = () => {
|
||||
dialog.value?.showModal();
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: showModal,
|
||||
close: (returnVal?: string): void => dialog.value?.close(returnVal),
|
||||
visible,
|
||||
});
|
||||
const id = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog
|
||||
ref="dialog" :class="`modal-card ${props.noPadding ? 'no-padding' : ''} ${props.noPadding ? 'no-scroll' : ''}`"
|
||||
@close="visible = false"
|
||||
closedby="any"
|
||||
:aria-labelledby="`${id}-title`"
|
||||
:style="{maxWidth: props.maxWidth}"
|
||||
>
|
||||
<form
|
||||
v-if="visible"
|
||||
method="dialog" class="modal-card-inner form-horizontal"
|
||||
:class="{
|
||||
[props.classes]: props.classes,
|
||||
}"
|
||||
>
|
||||
<div class="modal-card-content">
|
||||
<h2 :id="`${id}-title`" class="modal-card-title h3" v-if="title">{{ title }}</h2>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.modal-card.no-padding, .modal-card.no-padding .modal-card-content { padding: 0; }
|
||||
.modal-card.no-scroll { overflow: hidden; }
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
|
||||
type MyAttrs = {
|
||||
[key: `on${any}`]: EventListener, style?: {[key:string]: string} | string, innerHTML?: string,
|
||||
appendTo?: HTMLElement | ShadowRoot, prependTo?: HTMLElement | ShadowRoot, insertBefore?: HTMLElement, insertAfter?: HTMLElement,
|
||||
[key: Exclude<string, 'component' | 'args'>]: any,
|
||||
};
|
||||
|
||||
export function EL<K extends keyof HTMLElementTagNameMap>(tagName: K, attrs:MyAttrs, ...children:(string|HTMLElement)[]): HTMLElementTagNameMap[K];
|
||||
export function EL(tagName:string, attrs:MyAttrs, ...children:(string|HTMLElement)[]) : HTMLElement;
|
||||
export function EL(tagName:string, attrs:MyAttrs, ...children:(string|HTMLElement)[]) : HTMLElement {
|
||||
if (attrs?.idGetOrCreate) {
|
||||
const el = document.getElementById(attrs.idGetOrCreate);
|
||||
if (el) {
|
||||
return el;
|
||||
} else {
|
||||
attrs.id = attrs.idGetOrCreate; delete attrs.idGetOrCreate;
|
||||
}
|
||||
}
|
||||
var el = document.createElement(tagName);
|
||||
if (attrs) for(var key in attrs)
|
||||
if (key === 'style' && typeof attrs[key] === 'object') Object.assign(el.style, attrs[key]);
|
||||
else if (key === 'component' && 'args' in attrs) el['Component'] = new attrs.component(el, ...attrs.args);
|
||||
else if (key === 'innerHTML') el.innerHTML = attrs[key];
|
||||
else if (key === 'appendTo' && (attrs.appendTo instanceof HTMLElement || attrs.appendTo instanceof ShadowRoot)) attrs.appendTo.append(el);
|
||||
else if (key === 'prependTo' && (attrs.prependTo instanceof HTMLElement || attrs.prependTo instanceof ShadowRoot)) attrs.prependTo.prepend(el);
|
||||
else if (key === 'insertBefore' && attrs.insertBefore instanceof HTMLElement) attrs.insertBefore.before(el);
|
||||
else if (key === 'insertAfter' && attrs.insertAfter instanceof HTMLElement) attrs.insertAfter.after(el);
|
||||
else if (key.startsWith("on")) el.addEventListener(key.substring(2), attrs[key] as unknown as EventListener, false);
|
||||
else if (key.startsWith(":")) el[key.substring(1)] = attrs[key];
|
||||
else if (key === 'checked' && 'checked' in el) el.checked = attrs.checked;
|
||||
else if (key === 'disabled' && 'disabled' in el) el.disabled = attrs.disabled;
|
||||
else if (key === 'selected' && 'selected' in el) el.selected = attrs.selected;
|
||||
else if (key === 'multiple' && 'multiple' in el) el.multiple = attrs.multiple;
|
||||
else el.setAttribute(key, attrs[key]);
|
||||
|
||||
for(var i=0;i<children.length;i++){
|
||||
if (children[i] instanceof HTMLElement) el.appendChild(<HTMLElement>children[i]);
|
||||
else if (children[i]) el.appendChild(document.createTextNode(""+children[i]));
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
export class ProgressBar {
|
||||
private static container : HTMLDivElement = EL('div', { appendTo: document.body, class: 'progressBar' }, EL('div', {class:'progressBarText'}));
|
||||
|
||||
static show(message?: string, max?: number) {
|
||||
const texts = ProgressBar.container.lastElementChild;
|
||||
const bar = EL('div', {class:'progressBar_progress'+(max ? '' : ' indeterminate')});
|
||||
let text : HTMLElement;
|
||||
if (message) {
|
||||
text = EL('span', {}, message);
|
||||
texts.append(text);
|
||||
}
|
||||
texts.before(bar);
|
||||
return {
|
||||
disposed: false,
|
||||
[Symbol.dispose]() {
|
||||
if (this.disposed) return;
|
||||
bar.remove();
|
||||
if (text) text.remove();
|
||||
this.disposed = true;
|
||||
},
|
||||
set message(val: string) {
|
||||
text.innerText = val;
|
||||
},
|
||||
set progress(val: number) {
|
||||
if (max) bar.style.width = `${val / max * 100}%`;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { i18n_any, QUESTION_TYPE, QUESTION_TYPE_LABEL, SYSTEM_DATAFIELDS } from './helper';
|
||||
import NativeDialog from './NativeDialog.vue';
|
||||
import I18nTextField from './I18nTextField.vue';
|
||||
import {useId, ref, computed} from 'vue'
|
||||
import { DragHandle } from 'vue-slicksort';
|
||||
import {getDatafieldCreateUrl, getDatafieldEditUrl, getDatafieldViewUrl} from "./api";
|
||||
import I18nTextArea from "./I18nTextArea.vue";
|
||||
import DjangoDialog from "./DjangoDialog.vue";
|
||||
|
||||
const id = useId();
|
||||
const props = defineProps(['question', 'datafields', 'editable', 'possible_dependencies'])
|
||||
const emit = defineEmits(['removeSelf', 'update', 'invalidate:datafields']);
|
||||
const gettext = (window as any).gettext;
|
||||
const question = ref(props.question);
|
||||
|
||||
const dlgEditDatafield = ref()
|
||||
|
||||
const df = computed(() => typeof question.value.datafield === 'number' ?
|
||||
props.datafields.find(el => el.id === question.value.datafield) :
|
||||
typeof question.value.datafield === 'string' ?
|
||||
SYSTEM_DATAFIELDS[question.value.datafield] :
|
||||
null);
|
||||
|
||||
const dependency_values_options = computed(() => props.datafields.find(el => el.id === question.value.dependency_question)?.options);
|
||||
const dependency_values_resolved = computed(() => question.value.dependency_values.map(ident => i18n_any(dependency_values_options.value.find(opt => opt.identifier === ident)?.answer) ?? ident));
|
||||
|
||||
if (!question.value.label) question.value.label = {};
|
||||
if (!question.value.help_text) question.value.help_text = {};
|
||||
|
||||
const editor = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor-row">
|
||||
<div class="editor-preview-area">
|
||||
<div class="form-group">
|
||||
|
||||
<template v-if="df">
|
||||
<div v-if="question.dependency_question" class="dependency-info debuginfo">
|
||||
<span><span class="fa fa-link"></span> {{ question.dependency_question }} = {{ dependency_values_resolved }}</span>
|
||||
</div>
|
||||
<div class="col-md-3 control-label label-empty" v-if="df.type === QUESTION_TYPE.BOOLEAN"></div>
|
||||
<label class="col-md-3 control-label" :for="id" v-else>
|
||||
{{ i18n_any(question.label) }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<input :id="id" type="text" v-if="df.type === QUESTION_TYPE.STRING || df.type === QUESTION_TYPE.PHONENUMBER" class="form-control">
|
||||
<textarea :id="id" v-if="df.type === QUESTION_TYPE.TEXT" class="form-control"></textarea>
|
||||
<div class="checkbox" v-if="df.type === QUESTION_TYPE.BOOLEAN">
|
||||
<label :for="id">
|
||||
<input :id="id" type="checkbox"> {{ i18n_any(question.label) }}
|
||||
</label>
|
||||
</div>
|
||||
<input :id="id" type="number" v-if="df.type === QUESTION_TYPE.NUMBER" class="form-control">
|
||||
<input :id="id" type="file" v-if="df.type === QUESTION_TYPE.FILE" class="form-control">
|
||||
<select :id="id" class="form-control"
|
||||
v-if="df.type === QUESTION_TYPE.CHOICE || df.type === QUESTION_TYPE.COUNTRYCODE">
|
||||
<option></option>
|
||||
<option v-for="opt in df.options">{{ i18n_any(opt.answer) }}</option>
|
||||
</select>
|
||||
<div class="checkbox" v-if="df.type === QUESTION_TYPE.CHOICE_MULTIPLE" v-for="(opt, index) in df.options">
|
||||
<label :for="`${id}-${index}`">
|
||||
<input :id="`${id}-${index}`" type="checkbox"> {{ i18n_any(opt.answer) }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="help-block" v-html="question.rendered_help_text" v-if="question.rendered_help_text"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="col-md-9 col-md-push-3">
|
||||
<h4>{{ i18n_any(question.label) }}</h4>
|
||||
<p v-html="question.rendered_help_text" v-if="question.rendered_help_text"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="editor-action-area">
|
||||
<div class="btn-group" v-if="editable">
|
||||
<DragHandle tag="button" class="btn btn-default"><i class="fa fa-arrows"></i></DragHandle>
|
||||
<button class="btn btn-default" @click="editor.show()"><i class="fa fa-edit"></i></button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<NativeDialog ref="editor" class="modal-card"
|
||||
:title="df ? gettext('Edit question') : gettext('Edit text block')">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ df ? gettext('Question') : gettext('Headline') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<I18nTextField :value="question.label"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Help text') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<I18nTextArea :value="question.help_text"/>
|
||||
<div class="help-block">Wenn diese Frage noch weitere Erklärung braucht, können Sie sie hier eintragen.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="df">
|
||||
<label class="col-md-3 control-label">
|
||||
Data field
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<template v-if="typeof question.datafield === 'number'">
|
||||
{{ df.internal_name }}
|
||||
<div>
|
||||
<a class="btn btn-sm btn-default" href="javascript:" @click="dlgEditDatafield.open(getDatafieldEditUrl(df.id))"><span class="fa fa-wrench"></span> Manage data field details</a>
|
||||
<a class="btn btn-sm btn-default" :href="getDatafieldViewUrl(df.id)" target="_blank"><span class="fa fa-external-link"></span> View answers</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ question.datafield }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="df">
|
||||
<label class="col-md-3 control-label">
|
||||
Data field type
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<select v-model="df.type" class="form-control" disabled>
|
||||
<option v-for="(label, type) in QUESTION_TYPE_LABEL" :value="QUESTION_TYPE[type]">{{ label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="df">
|
||||
<label class="col-md-3 control-label label-empty"> </label>
|
||||
<div class="col-md-9">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" v-model="question.required">
|
||||
{{ gettext('Required question') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Only visible if...') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<select v-model="question.dependency_question" class="form-control">
|
||||
<option :value="null">{{ gettext('(none)') }}</option>
|
||||
<option v-for="(qc, index) in possible_dependencies" :value="qc.datafield">{{ i18n_any(qc.label) }}</option>
|
||||
</select>
|
||||
<select v-model="question.dependency_values" class="form-control" multiple>
|
||||
<option v-for="(qc, index) in dependency_values_options" :value="qc.identifier">{{ i18n_any(qc.answer) }}</option>
|
||||
</select>
|
||||
<div v-if="question.dependency_question">
|
||||
<p><span class="debuginfo">depends on {{ question.dependency_question }} = {{ dependency_values_resolved }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="editor.close(); emit('update')" class="btn btn-primary pull-right"><span class="fa fa-check"></span> Save and close</button>
|
||||
<button @click="emit('removeSelf')" class="btn btn-default">Remove from questionnaire</button>
|
||||
</NativeDialog>
|
||||
|
||||
<DjangoDialog ref="dlgEditDatafield" @confirm="emit('invalidate:datafields')" max-width="60em"></DjangoDialog>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,80 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {gettext} from "./gettextstub";
|
||||
import {GroupedItems, Questionnaire, SalesChannel} from "./model";
|
||||
import {inject, Ref, useId} from "vue";
|
||||
import {i18n_any, QUESTIONNAIRE_TYPE, QUESTIONNAIRE_TYPE_LABEL, setListState} from "./helper";
|
||||
import SalesChannelIcon from "./SalesChannelIcon.vue";
|
||||
|
||||
const sales_channels: Ref<SalesChannel[]> = inject('pretix:env:organizer:sales_channels')
|
||||
|
||||
const id = useId()
|
||||
|
||||
const props = defineProps<{
|
||||
questionnaire: Questionnaire,
|
||||
grouped_items: GroupedItems,
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Internal name') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control" v-model="questionnaire.internal_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('When to ask') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<select v-model="questionnaire.type" class="form-control">
|
||||
<template v-for="(label, type) in QUESTIONNAIRE_TYPE_LABEL">
|
||||
<option v-if="QUESTIONNAIRE_TYPE[type][0] == questionnaire.type[0]" :value="QUESTIONNAIRE_TYPE[type]">{{ label }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="questionnaire.type[1] == 'S'">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Sales channels') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" v-model="questionnaire.all_sales_channels"> {{ gettext('All sales channels') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox" v-for="channel in sales_channels">
|
||||
<label>
|
||||
<input type="checkbox" :checked="questionnaire.all_sales_channels || questionnaire.limit_sales_channels.indexOf(channel.identifier) !== -1"
|
||||
@change="e => setListState(questionnaire.limit_sales_channels, (e.target as HTMLInputElement).checked, channel.identifier)"
|
||||
:disabled="questionnaire.all_sales_channels">
|
||||
<SalesChannelIcon :sales-channel="channel"/>
|
||||
{{ i18n_any(channel.label) }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="grouped_items">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Visible on products') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<div v-for="[category, items] in grouped_items">
|
||||
<div class="category-header">{{ category.internal_name || i18n_any(category.name) }}</div>
|
||||
<div class="checkbox" v-for="item in items">
|
||||
<label :for="id + '_' + item.id">
|
||||
<input :id="id + '_' + item.id" type="checkbox" :checked="questionnaire.items.indexOf(item.id) !== -1" @change="e => setListState(questionnaire.items, (e.target as HTMLInputElement).checked, item.id)"> {{ item.internal_name || i18n_any(item.name) }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
@@ -1,230 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useId, ref, computed, onMounted, watch, Ref, inject} from 'vue'
|
||||
import QuestionElement from "./QuestionElement.vue";
|
||||
import {
|
||||
i18n_any,
|
||||
QUESTION_TYPE,
|
||||
QUESTION_TYPE_LABEL,
|
||||
QUESTIONNAIRE_TYPE,
|
||||
QUESTIONNAIRE_TYPE_LABEL, setListState,
|
||||
SYSTEM_DATAFIELDS
|
||||
} from "./helper";
|
||||
import { gettext } from './gettextstub';
|
||||
import I18nTextField from "./I18nTextField.vue";
|
||||
import NativeDialog from "./NativeDialog.vue";
|
||||
import { SlickList, SlickItem, DragHandle } from 'vue-slicksort';
|
||||
import {getDatafieldCreateUrl} from "./api";
|
||||
import DjangoDialog from "./DjangoDialog.vue";
|
||||
import * as api from './api';
|
||||
import QuestionnaireDetailForm from "./QuestionnaireDetailForm.vue";
|
||||
import SalesChannelIcon from "./SalesChannelIcon.vue";
|
||||
import {SalesChannel} from "./model";
|
||||
|
||||
const dlgEditor = ref()
|
||||
const dlgAddExisting = ref()
|
||||
const dlgAddTextblock = ref()
|
||||
const dlgNewDatafield = ref()
|
||||
|
||||
const newTextblockTitle = ref()
|
||||
const newTextblockText = ref()
|
||||
|
||||
const id = useId();
|
||||
const props = defineProps(['questionnaire', 'datafields', 'selected_product', 'grouped_items', 'preview_mode', 'err_mes'])
|
||||
const emit = defineEmits(['update', 'invalidate:datafields', 'invalidate:questionnaires'])
|
||||
const sales_channels: Ref<SalesChannel[]> = inject('pretix:env:organizer:sales_channels')
|
||||
|
||||
let nextId = 1;
|
||||
watch(() => props.questionnaire.children, () => {
|
||||
for (let qc of props.questionnaire.children) {
|
||||
if (!qc._cid) qc._cid = id + (++nextId);
|
||||
}
|
||||
})
|
||||
|
||||
function addExistingDatafield (field) {
|
||||
props.questionnaire.children.push({
|
||||
_cid: useId(),
|
||||
datafield: field.id,
|
||||
required: false,
|
||||
label: field.question ?? {en: field.internal_name},
|
||||
help_text: {},
|
||||
dependency_question: null,
|
||||
dependency_values: [],
|
||||
})
|
||||
dlgAddExisting.value.close()
|
||||
emit('update')
|
||||
}
|
||||
|
||||
function showAddTextblockDialog () {
|
||||
newTextblockTitle.value = {}
|
||||
newTextblockText.value = {}
|
||||
dlgAddTextblock.value.show()
|
||||
}
|
||||
|
||||
function addTextblock () {
|
||||
props.questionnaire.children.push({
|
||||
_cid: useId(),
|
||||
datafield: null,
|
||||
required: false,
|
||||
label: newTextblockTitle.value,
|
||||
help_text: newTextblockText.value,
|
||||
dependency_question: null,
|
||||
dependency_values: [],
|
||||
})
|
||||
dlgAddTextblock.value.close()
|
||||
emit('update')
|
||||
}
|
||||
|
||||
function newDatafield (container_type) {
|
||||
dlgNewDatafield.value.open(getDatafieldCreateUrl(props.questionnaire.type[0]))
|
||||
}
|
||||
|
||||
async function onNewDatafieldCreated (data) {
|
||||
watch(
|
||||
() => props.datafields,
|
||||
(newValue, oldValue) => {
|
||||
console.log('datafields changed watcher called')
|
||||
addExistingDatafield(newValue.find(f => f.id === data.object))
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
emit('invalidate:datafields')
|
||||
}
|
||||
|
||||
async function deleteQuestionnaire () {
|
||||
if (confirm("Are you sure?")) {
|
||||
try {
|
||||
await api.deleteQuestionnaire(props.questionnaire.id)
|
||||
} catch (e) {
|
||||
console.error('Failed to delete questionnaire', e)
|
||||
alert('Error while deleting questionnaire')
|
||||
}
|
||||
emit('invalidate:questionnaires')
|
||||
}
|
||||
}
|
||||
|
||||
const isHidden = computed(() => props.selected_product && props.questionnaire.items.indexOf(props.selected_product) === -1)
|
||||
const isEditable = computed(() => props.selected_product && props.questionnaire.items.indexOf(props.selected_product) !== -1)
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.sales-channel-icons > * { margin-left: 4px; opacity: 0.8; }
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
||||
<details class="questionnaire-panel" :open="preview_mode"
|
||||
:class="{ 'hidden-questionnaire': isHidden }">
|
||||
<summary class="questionnaire-panel-heading">
|
||||
<div class=" editor-row">
|
||||
<div class="editor-preview-area">
|
||||
<input type="checkbox" @change="e => {setListState(props.questionnaire.items, (e.target as HTMLInputElement).checked, selected_product); emit('update')}" v-if="selected_product && !preview_mode" :checked="!isHidden">
|
||||
{{ props.questionnaire.internal_name }}
|
||||
<span class="fa fa-warning" v-if="questionnaire._err_mes"></span>
|
||||
<span class="fa fa-cog fa-spin" v-if="questionnaire._loading"></span>
|
||||
|
||||
<span class="pull-right" v-if="questionnaire.type === 'PC'">
|
||||
<span class="label label-default">{{ gettext('Check-in') }}</span>
|
||||
</span>
|
||||
<span class="pull-right" v-else-if="questionnaire.type === 'PA'">
|
||||
<span class="label label-default">{{ gettext('Attendee link') }}</span>
|
||||
</span>
|
||||
<span class="pull-right sales-channel-icons" v-else>
|
||||
<template v-for="sc in sales_channels">
|
||||
<SalesChannelIcon :sales-channel="sc" :tooltip="true"
|
||||
v-if="props.questionnaire.all_sales_channels || props.questionnaire.limit_sales_channels.indexOf(sc.identifier) !== -1" />
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<aside class="editor-action-area"><div class="btn-group">
|
||||
<DragHandle tag="button" class="btn btn-default" v-if="!preview_mode"><i class="fa fa-arrows"></i></DragHandle>
|
||||
<button class="btn btn-default" @click="dlgEditor.show()" :title="`#${questionnaire.id}`"><i class="fa fa-wrench"></i></button>
|
||||
</div></aside>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="questionnaire-panel-body" v-if="!isHidden">
|
||||
<div class="alert alert-warning" v-if="questionnaire._err_mes">
|
||||
<dl v-if="questionnaire._err_mes.api_error">
|
||||
<template v-for="(messages, key) in questionnaire._err_mes.api_error">
|
||||
<dt>{{ key }}</dt>
|
||||
<dd><ul><li v-for="message in messages">{{ message }}</li></ul></dd>
|
||||
</template>
|
||||
</dl>
|
||||
<p v-else>{{ questionnaire._err_mes }}</p>
|
||||
</div>
|
||||
<div class="form-horizontal" :id="`questionListParent${props.questionnaire.id}`">
|
||||
<SlickList axis="y" v-model:list="props.questionnaire.children" useDragHandle :appendTo="`#questionListParent${props.questionnaire.id}`" @update:list="emit('update')">
|
||||
<SlickItem v-for="(child, index) in props.questionnaire.children" :key="child._cid" :index="index">
|
||||
<QuestionElement
|
||||
:datafields="props.datafields"
|
||||
:question="child"
|
||||
:editable="true"
|
||||
:possible_dependencies="props.questionnaire.children.slice(0, index)"
|
||||
@remove-self="questionnaire.children.splice(index, 1); emit('update')"
|
||||
@update="emit('update')"
|
||||
@invalidate:datafields="emit('invalidate:datafields')"/>
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
</div>
|
||||
<div class="editor-action-row form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-push-3">
|
||||
<p class="btn-group" role="group">
|
||||
<button class="btn btn-default" @click="dlgAddExisting.show()"><i class="fa fa-plus"></i> {{ gettext('Existing data field') }}</button>
|
||||
<button class="btn btn-default" @click="newDatafield(questionnaire.type[0])"><i class="fa fa-plus"></i> {{ gettext('New data field') }}</button>
|
||||
<button class="btn btn-default" @click="showAddTextblockDialog()"><i class="fa fa-plus"></i> {{ gettext('Text') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<Teleport to="body">
|
||||
<NativeDialog ref="dlgEditor" class="modal-card"
|
||||
:title="gettext('Edit questionnaire')">
|
||||
<QuestionnaireDetailForm :questionnaire="questionnaire" :grouped_items="grouped_items"/>
|
||||
|
||||
<button @click="dlgEditor.close(); emit('update')" class="btn btn-primary pull-right"><span class="fa fa-check"></span> {{ gettext('Save and close') }}</button>
|
||||
<button @click="deleteQuestionnaire" class="btn btn-default">{{ gettext('Delete') }}</button>
|
||||
</NativeDialog>
|
||||
|
||||
<NativeDialog ref="dlgAddExisting" class="modal-card"
|
||||
:title="gettext('Add existing data field')">
|
||||
|
||||
<div class="list-group">
|
||||
<a href="javascript:" @click="addExistingDatafield(field)" v-for="field in datafields" class="list-group-item">{{ field.internal_name ?? i18n_any(field.question) }}</a>
|
||||
</div>
|
||||
|
||||
<button @click="dlgAddExisting.close()" class="btn btn-default pull-right">{{ gettext('Cancel') }}</button>
|
||||
</NativeDialog>
|
||||
|
||||
<NativeDialog ref="dlgAddTextblock" class="modal-card"
|
||||
:title="gettext('Add sub heading')">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Title') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<I18nTextField :value="newTextblockTitle"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Text') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<I18nTextField :value="newTextblockText"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="addTextblock()" class="btn btn-default pull-right">{{ gettext('OK') }}</button>
|
||||
<button @click="dlgAddTextblock.close()" class="btn btn-default pull-right">{{ gettext('Cancel') }}</button>
|
||||
|
||||
</NativeDialog>
|
||||
|
||||
<DjangoDialog ref="dlgNewDatafield" @confirm="onNewDatafieldCreated" max-width="60em"></DjangoDialog>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {SalesChannel} from "./model";
|
||||
import {i18n_any} from "./helper";
|
||||
|
||||
const props = defineProps<{ salesChannel: SalesChannel, tooltip?: boolean }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="salesChannel.icon_url">
|
||||
<img :src="salesChannel.icon_url" class="fa-like-image" :alt="i18n_any(salesChannel.label)" :data-toggle="tooltip ? 'tooltip' : ''" :title="tooltip ? i18n_any(salesChannel.label) : ''">
|
||||
</template>
|
||||
<template v-else>
|
||||
<span :class="`fa fa-${salesChannel.icon} fa-fw text-muted`" :data-toggle="tooltip ? 'tooltip' : ''" :title="tooltip ? i18n_any(salesChannel.label) : ''"></span>
|
||||
</template>
|
||||
</template>
|
||||
@@ -1,98 +0,0 @@
|
||||
import {ApiListResponse, Datafield, Questionnaire, Item, Category, SalesChannel} from './model'
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
import {fromJsonScript} from "./helper";
|
||||
|
||||
const organizer_slug = document.body.getAttribute('data-organizer'),
|
||||
event_slug = document.body.getAttribute('data-event')
|
||||
|
||||
async function api_get(resource) {
|
||||
return await $.getJSON(`/api/v1/${resource}?_nocache=${+new Date()}`)
|
||||
}
|
||||
|
||||
async function api_get_all<T>(resource): Promise<T[]> {
|
||||
let next = `/api/v1/${resource}?_nocache=${+new Date()}`
|
||||
const result: T[] = [];
|
||||
while (next) {
|
||||
const response: ApiListResponse<T> = await $.getJSON(next)
|
||||
result.push(...response.results)
|
||||
next = response.next
|
||||
console.log('api_get_all: ' + resource, next, response, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
class APIError extends Error {
|
||||
api_error: string;
|
||||
constructor(json) {
|
||||
super('' + Object.values(json)[0]);
|
||||
this.api_error = json;
|
||||
}
|
||||
}
|
||||
async function api_json_request(resource, method, json_body) {
|
||||
const response = await fetch(`/api/v1/${resource}`, {
|
||||
body: JSON.stringify(json_body),
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val() as string,
|
||||
},
|
||||
});
|
||||
if (response.status === 204)
|
||||
return {}
|
||||
else if (response.status >= 400)
|
||||
throw new APIError(await response.json());
|
||||
else
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
export async function getDatafields(container_type) {
|
||||
using pb = ProgressBar.show('loading data fields')
|
||||
return await api_get_all<Datafield>(`organizers/${organizer_slug}/events/${event_slug}/${container_type == 'O' ? 'orderdatafields' : 'datafields'}/`);
|
||||
}
|
||||
|
||||
export async function getQuestionnaires() {
|
||||
using pb = ProgressBar.show('loading questionnaires')
|
||||
return await api_get_all<Questionnaire>(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/`);
|
||||
}
|
||||
|
||||
export async function updateQuestionnaire(id, data) {
|
||||
return await api_json_request(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/${id}/`, 'PATCH', data);
|
||||
}
|
||||
|
||||
export async function createQuestionnaire(data) {
|
||||
return await api_json_request(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/`, 'POST', data);
|
||||
}
|
||||
|
||||
export async function deleteQuestionnaire(id) {
|
||||
return await api_json_request(`organizers/${organizer_slug}/events/${event_slug}/questionnaires/${id}/`, 'DELETE', {});
|
||||
}
|
||||
|
||||
export async function getItems() {
|
||||
using pb = ProgressBar.show('loading product list')
|
||||
return await api_get_all<Item>(`organizers/${organizer_slug}/events/${event_slug}/items/`);
|
||||
}
|
||||
|
||||
export async function getCategories() {
|
||||
using pb = ProgressBar.show('loading category list')
|
||||
return await api_get_all<Category>(`organizers/${organizer_slug}/events/${event_slug}/categories/`);
|
||||
}
|
||||
|
||||
export async function getSalesChannels() {
|
||||
return await api_get_all<SalesChannel>(`organizers/${organizer_slug}/saleschannels/`);
|
||||
}
|
||||
|
||||
export function getEventLocales() {
|
||||
return fromJsonScript('event_locales');
|
||||
}
|
||||
|
||||
export function getDatafieldViewUrl(datafield_id) {
|
||||
return fromJsonScript('datafield_view_url').replace('/0/', `/${datafield_id}/`);
|
||||
}
|
||||
|
||||
export function getDatafieldEditUrl(datafield_id) {
|
||||
return fromJsonScript('datafield_edit_url').replace('/0/', `/${datafield_id}/`) + '?notify_parent=true&';
|
||||
}
|
||||
|
||||
export function getDatafieldCreateUrl(container_type) {
|
||||
return fromJsonScript('datafield_create_url') + '?notify_parent=true&container_type=' + container_type;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user