Compare commits

..

8 Commits

Author SHA1 Message Date
Lukas Bockstaller ce7c1870ea put errors.js in an IIFE call 2026-06-16 13:27:54 +02:00
Lukas Bockstaller 91e923805a Update src/pretix/static/pretixbase/js/asynctask.js
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
2026-06-16 12:57:57 +02:00
Lukas Bockstaller c1c7043da7 include errors.js in control base.html 2026-06-16 11:59:12 +02:00
Lukas Bockstaller fd4e87352b include errors.js in error.html 2026-06-16 11:59:12 +02:00
Lukas Bockstaller dbf5dc0ef5 drop the jquery dependency for error.js
Co-authored-by: pajowu <pajowu@pajowu.de>
2026-06-16 09:32:07 +02:00
Lukas Bockstaller a1f796da9e Update src/pretix/static/pretixbase/js/asynctask.js
Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-06-15 13:15:56 +02:00
Lukas Bockstaller 3a10a5387e scope down event 2026-06-15 11:55:58 +02:00
Lukas Bockstaller 6d405a608f load errors.js as standard and make it coop with async_task_replace_page 2026-06-15 11:41:28 +02:00
297 changed files with 566048 additions and 530456 deletions
+2 -1
View File
@@ -10,7 +10,8 @@ tests:
- cd src
- python manage.py check
- make all compress
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --ignore=tests/e2e --maxfail=100
- playwright install
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
except:
- '/^v.*$/'
pypi:
+1 -1
View File
@@ -81,7 +81,7 @@ is a python method that emulates a behavior similar to ``reverse``:
If you need to communicate the URL externally, you can use a different method to ensure that it is always an absolute URL:
.. autofunction:: pretix.multidomain.urlreverse.eventreverse_absolute
.. autofunction:: pretix.multidomain.urlreverse.build_absolute_uri
In addition, there is a template tag that works similar to ``url`` but takes an event or organizer object
as its first argument and can be used like this::
+1 -1
View File
@@ -53,7 +53,7 @@ Working with the code
---------------------
If you do not have a recent installation of ``nodejs``, install it now::
curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt install nodejs
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
+91 -109
View File
@@ -370,14 +370,14 @@
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.2"
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
@@ -427,9 +427,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"version": "0.129.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -758,9 +758,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
"cpu": [
"arm64"
],
@@ -775,9 +775,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
"cpu": [
"arm64"
],
@@ -792,9 +792,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
"cpu": [
"x64"
],
@@ -809,9 +809,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
"cpu": [
"x64"
],
@@ -826,9 +826,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
"cpu": [
"arm"
],
@@ -843,16 +843,13 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -863,16 +860,13 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -883,16 +877,13 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -903,16 +894,13 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -923,16 +911,13 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -943,16 +928,13 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -963,9 +945,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
"cpu": [
"arm64"
],
@@ -980,9 +962,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
"cpu": [
"wasm32"
],
@@ -999,9 +981,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
"cpu": [
"arm64"
],
@@ -1016,9 +998,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
"cpu": [
"x64"
],
@@ -3176,9 +3158,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
@@ -3352,9 +3334,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"funding": [
{
"type": "opencollective",
@@ -3371,7 +3353,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -3628,14 +3610,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
"@oxc-project/types": "=0.129.0",
"@rolldown/pluginutils": "1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -3644,27 +3626,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
"@rolldown/binding-android-arm64": "1.0.0",
"@rolldown/binding-darwin-arm64": "1.0.0",
"@rolldown/binding-darwin-x64": "1.0.0",
"@rolldown/binding-freebsd-x64": "1.0.0",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
"@rolldown/binding-linux-arm64-musl": "1.0.0",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
"@rolldown/binding-linux-x64-gnu": "1.0.0",
"@rolldown/binding-linux-x64-musl": "1.0.0",
"@rolldown/binding-openharmony-arm64": "1.0.0",
"@rolldown/binding-wasm32-wasi": "1.0.0",
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
"@rolldown/binding-win32-x64-msvc": "1.0.0"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
"dev": true,
"license": "MIT"
},
@@ -4343,9 +4325,9 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4483,17 +4465,17 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"version": "8.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.15",
"rolldown": "1.0.3",
"tinyglobby": "^0.2.17"
"postcss": "^8.5.14",
"rolldown": "1.0.0",
"tinyglobby": "^0.2.16"
},
"bin": {
"vite": "bin/vite.js"
+11 -12
View File
@@ -33,17 +33,17 @@ dependencies = [
"bleach==6.4.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=49.0.0",
"css-inline==0.21.*",
"cryptography>=48.0.1",
"css-inline==0.20.*",
"defusedcsv>=3.0.0",
"dnspython==2.*",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
"django-filter==26.1",
"django-filter==25.1",
"django-formset-js-improved==0.5.0.5",
"django-formtools==2.7",
"django-formtools==2.6.1",
"django-hierarkey==2.0.*,>=2.0.1",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
@@ -53,8 +53,7 @@ dependencies = [
"django-oauth-toolkit==2.3.*",
"django-otp==1.7.*",
"django-phonenumber-field==8.4.*",
"django-querytagger==0.0.3",
"django-redis==7.0.*",
"django-redis==6.0.*",
"django-scopes==2.0.*",
"django-statici18n==2.7.*",
"djangorestframework==3.17.*",
@@ -77,7 +76,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.13.*",
"phonenumberslite==9.0.*",
"Pillow==12.3.*",
"Pillow==12.2.*",
"pretix-plugin-build",
"protobuf==7.35.*",
"psycopg2-binary",
@@ -92,9 +91,9 @@ dependencies = [
"pyuca",
"qrcode==8.2",
"redis==7.4.*",
"reportlab==5.0.*",
"requests==2.34.*",
"sentry-sdk==2.65.*",
"reportlab==4.5.*",
"requests==2.32.*",
"sentry-sdk==2.62.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -102,7 +101,7 @@ dependencies = [
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
"webauthn==3.0.*",
"webauthn==2.7.*",
"zeep==4.3.*"
]
@@ -126,7 +125,7 @@ dev = [
"pytest-sugar",
"pytest-xdist==3.8.*",
"pytest-playwright",
"pytest==9.1.*",
"pytest==9.0.*",
"playwright",
"responses",
]
+2 -2
View File
@@ -6,8 +6,8 @@ localecompile:
./manage.py compilemessages
localegen:
./manage.py makemessages --keep-pot --add-location file --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot --add-location file -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: npminstall npmbuild jsi18n
./manage.py collectstatic --noinput
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.7.0.dev0"
__version__ = "2026.6.0.dev0"
-1
View File
@@ -118,7 +118,6 @@ ALL_LANGUAGES = [
('sv', _('Swedish')),
('es', _('Spanish')),
('es-419', _('Spanish (Latin America)')),
('th', _('Thai')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
]
+2 -3
View File
@@ -73,7 +73,7 @@ from pretix.base.settings import (
LazyI18nStringList, validate_event_settings,
)
from pretix.base.signals import api_event_settings_fields
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -173,7 +173,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
)
def get_event_url(self, event):
return eventreverse_absolute(event, 'presale:event.index')
return build_absolute_uri(event, 'presale:event.index')
class Meta:
model = Event
@@ -747,7 +747,6 @@ class EventSettingsSerializer(SettingsSerializer):
'max_items_per_order',
'reservation_time',
'contact_mail',
'contact_url',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
+3 -3
View File
@@ -76,7 +76,7 @@ from pretix.base.settings import (
)
from pretix.base.signals import register_ticket_outputs
from pretix.helpers.countries import CachedCountries
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -757,7 +757,7 @@ class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None
return eventreverse_absolute(instance.order.event, 'presale:event.order.pay', kwargs={
return build_absolute_uri(instance.order.event, 'presale:event.order.pay', kwargs={
'order': instance.order.code,
'secret': instance.order.secret,
'payment': instance.pk,
@@ -806,7 +806,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order):
return eventreverse_absolute(instance.event, 'presale:event.order', kwargs={
return build_absolute_uri(instance.event, 'presale:event.order', kwargs={
'order': instance.code,
'secret': instance.secret,
})
+6 -9
View File
@@ -27,7 +27,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -58,8 +58,8 @@ from pretix.helpers.permission_migration import (
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
)
from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -71,7 +71,7 @@ class OrganizerSerializer(I18nAwareModelSerializer):
slug = serializers.CharField(read_only=True)
def get_organizer_url(self, organizer):
return eventreverse_absolute(organizer, 'presale:organizer.index')
return build_absolute_uri(organizer, 'presale:organizer.index')
class Meta:
model = Organizer
@@ -492,16 +492,14 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.context['organizer'].name,
},
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
'url': mainreverse_absolute('control:auth.invite', kwargs={
'url': build_global_uri('control:auth.invite', kwargs={
'token': instance.token
})
},
@@ -576,7 +574,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'contact_url',
'imprint_url',
'organizer_info_text',
'event_list_type',
-1
View File
@@ -872,7 +872,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'media_policy': e.media_policy,
'media_type': e.media_type,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
'reason': e.code,
'reason_explanation': e.msg,
}, status=400)
except CheckInError as e:
+1 -3
View File
@@ -1658,7 +1658,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
count_waitinglist=False,
force=request.data.get('force', False),
send_mail=send_mail,
ignore_date=request.data.get('force', False),
)
except Quota.QuotaExceededException:
pass
@@ -1694,8 +1693,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
auth=self.request.auth,
count_waitinglist=False,
send_mail=send_mail,
force=force,
ignore_date=force)
force=force)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:
+2 -2
View File
@@ -36,7 +36,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from requests import RequestException
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -313,7 +313,7 @@ def _get_or_create_server_keypair(organizer):
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
payload = {
'iss': eventreverse_absolute(client.organizer, 'presale:organizer.index').rstrip('/'),
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'),
'aud': client.client_id,
'exp': int(expires.timestamp()),
'iat': int(time.time()),
+3 -3
View File
@@ -28,7 +28,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
def get_answer(op, question_identifier=None):
@@ -545,7 +545,7 @@ def get_data_fields(event, for_model=None):
_("Order link"),
Question.TYPE_STRING,
None,
lambda order: eventreverse_absolute(
lambda order: build_absolute_uri(
event,
'presale:event.order', kwargs={
'order': order.code,
@@ -560,7 +560,7 @@ def get_data_fields(event, for_model=None):
_("Ticket link"),
Question.TYPE_STRING,
None,
lambda op: eventreverse_absolute(
lambda op: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': op.order.code,
-5
View File
@@ -57,8 +57,6 @@ logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend)
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
try:
@@ -255,15 +253,12 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise socket.error(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise socket.error(f"Request to private address {sa[0]} blocked")
if check_ip4 in _cgnat_net:
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
sock = None
try:
+3 -3
View File
@@ -68,7 +68,7 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
from ...multidomain.urlreverse import eventreverse_absolute
from ...multidomain.urlreverse import build_absolute_uri
from ..exporter import (
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -429,7 +429,7 @@ class OrderListExporter(MultiSheetListExporter):
]))
row.append(
eventreverse_absolute(order.event, 'presale:event.order', kwargs={
build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
})
@@ -855,7 +855,7 @@ class OrderListExporter(MultiSheetListExporter):
]))
row.append(
eventreverse_absolute(order.event, 'presale:event.order.position', kwargs={
build_absolute_uri(order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': op.web_secret,
'position': op.positionid
+10 -23
View File
@@ -53,7 +53,6 @@ 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
from django.utils.functional import lazy
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
@@ -325,21 +324,16 @@ class WrappedPhonePrefixSelect(Select):
initial = None
def __init__(self, initial=None):
def _get_choices():
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
return choices
choices = lazy(_get_choices, list)()
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
super().__init__(choices=choices, attrs={
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
'autocomplete': 'tel-country-code',
@@ -1116,13 +1110,6 @@ class BaseQuestionsForm(forms.Form):
if q.dependency_question_id and not question_is_visible(q.dependency_question_id, q.dependency_values) and answer is not None:
d['question_%d' % q.pk] = None
# Strip False answers to required yes/no questions even if all_optional is set, as our data model assumes that
# required yes/no questions can only be answered with yes
for q in question_cache.values():
if q.required and q.type == Question.TYPE_BOOLEAN:
if 'question_%d' % q.pk in d and d['question_%d' % q.pk] is False:
del d['question_%d' % q.pk]
return d
@@ -1411,7 +1398,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
elif self.validate_vat_id and vat_id_applicable:
try:
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = bool(normalized_id)
self.instance.vat_id_validated = True
self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e:
if self.all_optional:
+90 -76
View File
@@ -22,7 +22,9 @@
import datetime
import logging
import math
import re
import textwrap
import unicodedata
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
@@ -56,8 +58,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import (
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
normalize_text, register_ttf_font_if_new, reshaper,
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
reshaper,
)
from pretix.presale.style import get_fonts
@@ -257,8 +259,18 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
def _normalize(self, text):
# alias kept for plugin compatibility
return normalize_text(text)
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _upper(self, val):
# We uppercase labels, but not in every language
@@ -339,15 +351,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
# For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
# tags.
return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
return self._normalize(bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
class PaidMarker(Flowable):
@@ -398,7 +405,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -409,8 +417,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = PlainTextParagraph(
self.invoice.full_invoice_from,
p = FontFallbackParagraph(
self._clean_text(self.invoice.full_invoice_from),
style=self.stylesheet['InvoiceFrom']
)
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -540,12 +548,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
txt = bleach.clean(txt, tags=set()).strip()
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
return txt
@@ -563,7 +572,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else:
p_str = shorten(self.invoice.event.name)
p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -636,37 +645,39 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
if type_info_text:
story.append(PlainTextParagraph(
story.append(FontFallbackParagraph(
type_info_text,
self.stylesheet['WarningBlock']
))
if self.invoice.custom_field:
story.append(PlainTextParagraph(
story.append(FontFallbackParagraph(
'{}: {}'.format(
str(self.invoice.event.settings.invoice_address_custom_field),
self.invoice.custom_field,
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
self._clean_text(self.invoice.custom_field),
),
self.stylesheet['Normal']
))
if self.invoice.internal_reference:
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=self.invoice.internal_reference,
),
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference),
)),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(PlainTextParagraph(
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal']
))
@@ -696,11 +707,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
PlainTextParagraph(
(
FontFallbackParagraph(
self._normalize(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
self.stylesheet['Heading1']
),
Spacer(1, 5 * mm),
@@ -722,17 +733,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
]
if has_taxes:
tdata = [(
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
)]
else:
tdata = [(
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
)]
def _group_key(line):
@@ -769,8 +780,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = PlainTextParagraph(
curr_description,
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p_style
)
h = p.wrap(max_width, doc.height)[1]
@@ -851,7 +862,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice
request_show_service_date = period_line
elif period_line:
description_p_list.append(PlainTextParagraph(
description_p_list.append(FontFallbackParagraph(
period_line,
self.stylesheet['Fineprint']
))
@@ -863,7 +874,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(PlainTextParagraph(
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
@@ -872,11 +883,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0),
str(len(lines)),
localize(tax_rate) + " %",
PlainTextParagraph(
FontFallbackParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
PlainTextParagraph(
FontFallbackParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -893,14 +904,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(PlainTextParagraph(
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
description_p_list.pop(0),
str(len(lines)),
PlainTextParagraph(
FontFallbackParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -933,12 +944,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
tdata.append([
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
])
else:
tdata.append([
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
])
@@ -947,12 +958,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append(
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)]
)
tdata.append(
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)]
)
@@ -969,12 +980,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount')
)['s'] or Decimal('0.00')
tdata.append(
[PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)]
)
tdata.append(
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
)
@@ -997,14 +1008,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm))
if request_show_service_date:
story.append(PlainTextParagraph(
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
self.stylesheet['Normal']
))
if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph(
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self._normalize(self.invoice.payment_provider_text),
self.stylesheet['Normal']
))
@@ -1028,10 +1039,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
''
]
tdata = [thead]
@@ -1042,7 +1053,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue
tax = taxvalue_map[idx]
tdata.append([
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
@@ -1061,7 +1072,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
table
]))
@@ -1078,7 +1089,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax
tdata.append([
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), ''
])
@@ -1087,13 +1098,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([
Spacer(1, height=2 * mm),
PlainTextParagraph(
pgettext(
FontFallbackParagraph(
self._normalize(pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
self.stylesheet['Fineprint']
),
Spacer(1, height=3 * mm),
@@ -1102,14 +1113,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(PlainTextParagraph(
story.append(FontFallbackParagraph(self._normalize(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total)),
total=fmt(foreign_total))),
self.stylesheet['Fineprint']
))
@@ -1151,8 +1162,11 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas):
if not self.invoice.address_invoice_from:
return
c = self.invoice.address_invoice_from.strip().split('\n')
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
c = [
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
@@ -1211,8 +1225,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
]
p = PlainTextParagraph(
date_format(self.invoice.date, "DATE_FORMAT"),
p = FontFallbackParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1269,7 +1283,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(PlainTextParagraph(
i.append(FontFallbackParagraph(
pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(),
),
@@ -1,29 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Do nothing. Useful for startup performance testing."
def handle(self, *args, **options):
pass
@@ -40,7 +40,6 @@ from django.core.cache import cache
from django.core.management.base import BaseCommand
from django.db import close_old_connections
from django.dispatch.dispatcher import NO_RECEIVERS
from django_querytagger.tagging import with_tag
from pretix.helpers.periodic import SKIPPED
@@ -83,8 +82,7 @@ class Command(BaseCommand):
try:
# Check if the DB connection is still good, it might be closed if the previous task took too long.
close_old_connections()
with with_tag(f"periodictask={name}"):
r = receiver(signal=periodic_task, sender=self)
r = receiver(signal=periodic_task, sender=self)
except Exception as err:
if isinstance(err, KeyboardInterrupt):
raise err
+2 -4
View File
@@ -282,12 +282,10 @@ def metric_values():
# Throwaway metrics
exact_tables = [
Order, Invoice, Event, Organizer
Order, OrderPosition, Invoice, Event, Organizer
]
for m in apps.get_models(): # Count all models
if issubclass(m, OrderPosition):
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.all.count()
elif any(issubclass(m, p) for p in exact_tables):
if any(issubclass(m, p) for p in exact_tables):
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
else:
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
+46 -99
View File
@@ -19,8 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import re
from collections import OrderedDict
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -45,8 +43,6 @@ from pretix.multidomain.urlreverse import (
)
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
_supported = None
@@ -78,7 +74,6 @@ class LocaleMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest):
language = get_language_from_request(request)
region = None
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
# set and can be taken into account for the decision.
@@ -99,16 +94,15 @@ class LocaleMiddleware(MiddlewareMixin):
if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region
if settings_holder.settings.region:
region = settings_holder.settings.region
set_region(settings_holder.settings.region)
else:
gs = global_settings_object(request)
if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region
if gs.settings.region:
region = gs.settings.region
set_region(gs.settings.region)
translation.activate(language)
set_region(region)
request.LANGUAGE_CODE = get_language_without_region()
tzname = None
@@ -227,26 +221,7 @@ def _parse_csp(header):
return h
VALID_CSP_DIRECTIVES = [
"child-src", "connect-src", "default-src", "fenced-frame-src", "font-src", "form-action", "frame-src", "img-src",
"manifest-src", "media-src", "object-src", "prefetch-src", "report-uri", "script-src", "script-src-elem",
"script-src-attr", "style-src", "style-src-elem", "style-src-attr", "worker-src",
]
CSP_ILLEGAL_CHARS = re.compile(r'[\s,;]')
def _sanitize_csp(h):
for k, v in h.items():
if k not in VALID_CSP_DIRECTIVES:
raise ValueError("Invalid CSP directive " + k)
if any(CSP_ILLEGAL_CHARS.search(el) for el in v):
logger.warning("Stripping invalid component from CSP: %r", h)
h[k] = [el for el in v if not CSP_ILLEGAL_CHARS.search(el)]
def _render_csp(h):
_sanitize_csp(h)
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
@@ -266,23 +241,21 @@ def _merge_csp(a, b):
class SecurityMiddleware(MiddlewareMixin):
SAFE_TYPES = (
# CSP policies are only used for:
# - HTML and SVG in top-level contexts
# - SVG or JS Workers delivered in embedded contexts
# See: https://www.w3.org/TR/CSP2/#which-policy-applies
# Therefore, we can save bandwidth on not including our (sometimes huge) policy
# on API responses or CSS. We do however include it with other types as a precaution
# (whitelist instead of blacklist) and we also do not whitelist JavaScript in
# we ever add service workers to not break the protection of this feature:
# https://www.w3.org/TR/CSP2/#sandboxing-and-workers
'application/json',
'text/css',
# We used to skip CSP for PDF since it was necessary for inline previews in Safari,
# but at the moment it does not seem to be an issue to just send it.
CSP_EXEMPT = (
'/api/v1/docs/',
)
def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
# pages
@@ -293,20 +266,18 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
if 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
img_src = []
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
if not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
def _build_csp(self, request, resp):
url = resolve(request.path_info)
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = {
'default-src': ["{static}"],
@@ -315,8 +286,8 @@ class SecurityMiddleware(MiddlewareMixin):
'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"],
'font-src': ["{static}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"] + list(font_src),
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
@@ -325,13 +296,6 @@ class SecurityMiddleware(MiddlewareMixin):
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
}
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
h['img-src'].append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
if hasattr(request, 'event'):
h['font-src'] += list(self._get_font_origins(request.event))
if settings.VITE_DEV_MODE:
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
h['style-src'] += ["'unsafe-inline'"]
@@ -343,7 +307,6 @@ class SecurityMiddleware(MiddlewareMixin):
if not settings.VITE_DEV_MODE:
# can't have 'unsafe-inline' and nonce at the same time
h['style-src'].append(nonce)
# Only include pay.google.com for wallet detection purposes on the Payment selection page
if (
url.url_name == "event.order.pay.change" or
@@ -352,32 +315,27 @@ class SecurityMiddleware(MiddlewareMixin):
h['script-src'].append('https://pay.google.com')
h['frame-src'].append('https://pay.google.com')
h['connect-src'].append('https://google.com/pay')
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
if settings.CSP_ADDITIONAL_HEADER:
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
placeholders = {
"{static}": ["'self'"],
"{dynamic}": ["'self'"],
"{media}": ["'self'"],
}
staticdomain = "'self'"
dynamicdomain = "'self'"
mediadomain = "'self'"
if settings.MEDIA_URL.startswith('http'):
placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
if settings.STATIC_URL.startswith('http'):
placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
if settings.SITE_URL.startswith('http'):
if settings.SITE_URL.find('/', 9) > 0:
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
else:
placeholders["{static}"].append(settings.SITE_URL)
placeholders["{dynamic}"].append(settings.SITE_URL)
staticdomain += " " + settings.SITE_URL
dynamicdomain += " " + settings.SITE_URL
if hasattr(request, 'organizer') and request.organizer:
if hasattr(request, 'event') and request.event:
@@ -388,29 +346,18 @@ class SecurityMiddleware(MiddlewareMixin):
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)
placeholders["{dynamic}"].append(domain)
dynamicdomain += " " + domain
for k, v in h.items():
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
media=mediadomain)
for k, v in h.items():
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
resp['Content-Security-Policy'] = _render_csp(h)
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return h
def _get_font_origins(self, event):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
font_src = set()
for font in get_fonts(event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
return font_src
return resp
class RejectInvalidInputMiddleware(MiddlewareMixin):
@@ -1,58 +0,0 @@
# Generated by Django 4.2.8 on 2024-07-01 09:27
import logging
from django.db import migrations
from django.db.models import Count
logger = logging.getLogger(__name__)
def clean_duplicate_secrets(apps, schema_editor):
# This will autofix all possible duplicate Order.code and OrderPosition.secret values,
# unless Order.code is already too long to append something. This would need to be fixed by
# sysadmins manually.
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
Order = apps.get_model("pretixbase", "Order")
qs = OrderPosition.all.values("secret", "order__event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
for row in qs:
affected = OrderPosition.all.filter(
**{k: v for k, v in row.items() if k != "c"}
).order_by("pk")
logger.error(f"Found {row['c']} tickets with with the same secret \"{row['secret']}\" in organizer {row['order__event__organizer_id']}, all except one will be changed")
for i, a in enumerate(affected):
if i > 0:
a.secret = a.secret + "__dupl__" + str(a.pk)
logger.info(
f"Ticket {a.pk} has new secret {a.secret}"
)
a.save(update_fields=["organizer_id", "secret"])
qs = Order.objects.values("code", "event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
for row in qs:
affected = Order.objects.filter(
**{k: v for k, v in row.items() if k != "c"}
).order_by("pk")
logger.error(f"Found {row['c']} orders with with the same code \"{row['code']}\" in organizer {row['event__organizer_id']}, all except one will be changed")
for i, a in enumerate(affected):
if i > 0:
if len(a.code) > 16 - len(str(a.pk)):
raise ValueError(f"Cannot auto-fix order with duplicate code {a.code}, order code is too long already")
a.code = a.code + str(a.pk).zfill(16 - len(a.code))
logger.info(
f"Order {a.pk} has new code {a.code}"
)
a.save(update_fields=["organizer_id", "code"])
class Migration(migrations.Migration):
dependencies = [
(
"pretixbase",
"0301_reusablemedium_remove_orderposition",
),
]
operations = [
migrations.RunPython(clean_duplicate_secrets, migrations.RunPython.noop),
]
@@ -1,46 +0,0 @@
# Generated by Django 4.2.8 on 2024-07-01 09:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"pretixbase",
"0302_resolve_duplicate_codes_and_secrets",
),
]
operations = [
migrations.RunSQL(
"UPDATE pretixbase_order "
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_event e WHERE e.id = pretixbase_order.event_id) "
"WHERE pretixbase_order.organizer_id IS NULL;",
migrations.RunSQL.noop,
),
migrations.RunSQL(
"UPDATE pretixbase_orderposition "
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_order o LEFT JOIN pretixbase_event e ON e.id = o.event_id WHERE o.id = pretixbase_orderposition.order_id) "
"WHERE pretixbase_orderposition.organizer_id IS NULL;",
migrations.RunSQL.noop,
),
migrations.AlterField(
model_name="order",
name="organizer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="orders",
to="pretixbase.organizer",
),
),
migrations.AlterField(
model_name="orderposition",
name="organizer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="order_positions",
to="pretixbase.organizer",
),
),
]
+17 -17
View File
@@ -57,7 +57,7 @@ from django_otp.models import Device
from django_scopes import scopes_disabled
from pretix.base.i18n import language
from pretix.helpers.urls import mainreverse_absolute
from pretix.helpers.urls import build_absolute_uri
from ...helpers.countries import FastCountryField
from ...helpers.u2f import pub_key_from_der, websafe_decode
@@ -373,12 +373,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
email or self.email,
_('Changes to your account'),
_('Account information changed'),
'pretixcontrol/email/security_notice.txt',
{
'user': self,
'messages': msg,
'url': mainreverse_absolute('control:user.settings'),
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
@@ -400,13 +400,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
with language(self.locale):
if reason == 'email_change':
msg = str(_('To change your email address from {old_email} to {new_email}, use the following code:').format(
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
old_email=self.email, new_email=email,
))
elif reason == 'email_verify':
msg = str(_('To verify your email address {email} on {instance}, use the following code:').format(
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format(
email=self.email,
instance=settings.PRETIX_INSTANCE_NAME,
))
else:
raise Exception('Invalid confirmation code reason')
@@ -419,7 +418,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
}
mail(
email or self.email,
_('Your confirmation code'),
_('pretix confirmation code'),
'pretixcontrol/email/confirmation_code.txt',
{
'user': self,
@@ -463,13 +462,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
from pretix.base.services.mail import mail
mail(
self.email,
_('Reset your password'),
'pretixcontrol/email/forgot.txt',
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'url': (mainreverse_absolute('control:auth.forgot.recover')
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
},
None, locale=self.locale, user=self
@@ -650,22 +647,25 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
)
def has_active_staff_session(self, session_key):
def has_active_staff_session(self, session_key=None):
"""
Returns whether or not a user has an active staff session (formerly known as superuser session)
with the given session key.
"""
return self.get_active_staff_session(session_key) is not None
def get_active_staff_session(self, session_key):
if not self.is_staff or not session_key:
def get_active_staff_session(self, session_key=None):
if not self.is_staff:
return None
if not hasattr(self, '_staff_session_cache'):
self._staff_session_cache = {}
if session_key not in self._staff_session_cache:
sess = StaffSession.objects.filter(
user=self, date_end__isnull=True, session_key=session_key
).first()
qs = StaffSession.objects.filter(
user=self, date_end__isnull=True
)
if session_key:
qs = qs.filter(session_key=session_key)
sess = qs.first()
if sess:
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
sess.date_end = now()
+5 -5
View File
@@ -167,7 +167,7 @@ class Customer(LoggedModel):
def send_security_notice(self, message, email=None):
from pretix.base.services.mail import SendMailException, mail
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
try:
with language(self.locale):
@@ -178,7 +178,7 @@ class Customer(LoggedModel):
{
**self.get_email_context(),
'message': str(message),
'url': eventreverse_absolute(self.organizer, 'presale:organizer.customer.index')
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
},
customer=self,
organizer=self.organizer,
@@ -299,12 +299,12 @@ class Customer(LoggedModel):
def send_activation_mail(self):
from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator
ctx = self.get_email_context()
token = TokenGenerator().make_token(self)
ctx['url'] = eventreverse_absolute(
ctx['url'] = build_absolute_uri(
self.organizer,
'presale:organizer.customer.activate'
) + '?id=' + self.identifier + '&token=' + token
@@ -395,7 +395,7 @@ class AttendeeProfile(models.Model):
self.company,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name if self.country else None,
self.country.name,
]
for a in self.answers:
value = a.get('value')
+15 -32
View File
@@ -40,7 +40,6 @@ import warnings
from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta
from operator import attrgetter
from typing import TYPE_CHECKING
from urllib.parse import urljoin
from zoneinfo import ZoneInfo
@@ -80,16 +79,10 @@ from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
from .organizer import Organizer, Team
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
logger = logging.getLogger(__name__)
class EventMixin:
if TYPE_CHECKING:
settings: HierarkeyProxy
def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
@@ -731,7 +724,7 @@ class Event(EventMixin, LoggedModel):
@property
def social_image(self):
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
img = None
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
@@ -749,7 +742,7 @@ class Event(EventMixin, LoggedModel):
logger.exception(f'Failed to create thumbnail of {logo_file}')
img = default_storage.url(logo_file)
if img:
return urljoin(eventreverse_absolute(self, 'presale:event.index'), img)
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
def _seats(self, ignore_voucher=None):
from .seating import Seat
@@ -890,8 +883,6 @@ class Event(EventMixin, LoggedModel):
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
)
is_cross_organizer = other.organizer_id != self.organizer_id
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
# Plugins can create data in installed() hook based on existing data of the event.
# Calling set_active_plugins() results in defaults being created while actually data
@@ -906,7 +897,7 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if hasattr(other, 'alternative_domain_assignment') and not is_cross_organizer:
if hasattr(other, 'alternative_domain_assignment'):
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
if not self.all_sales_channels:
@@ -920,15 +911,6 @@ class Event(EventMixin, LoggedModel):
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
emv.event = self
if is_cross_organizer:
try:
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
except EventMetaProperty.DoesNotExist:
meta_prop = emv.property
meta_prop.pk = None
meta_prop.organizer = self.organizer
meta_prop.save(force_insert=True)
emv.property = meta_prop
emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other):
@@ -982,13 +964,13 @@ class Event(EventMixin, LoggedModel):
if i.tax_rule_id:
i.tax_rule = tax_map[i.tax_rule_id]
if i.grant_membership_type and is_cross_organizer:
if i.grant_membership_type and other.organizer_id != self.organizer_id:
i.grant_membership_type = None
i.save() # no force_insert since i.picture.save could have already inserted
i.log_action('pretix.object.cloned')
if require_membership_types and not is_cross_organizer:
if require_membership_types and other.organizer_id == self.organizer_id:
i.require_membership_types.set(require_membership_types)
if not i.all_sales_channels:
@@ -1003,7 +985,7 @@ class Event(EventMixin, LoggedModel):
v._prefetched_objects_cache = {}
v.save(force_insert=True)
if require_membership_types and not is_cross_organizer:
if require_membership_types and other.organizer_id == self.organizer_id:
v.require_membership_types.set(require_membership_types)
if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
@@ -1403,12 +1385,15 @@ class Event(EventMixin, LoggedModel):
for mp in self.organizer.meta_properties.all():
if mp.required and not self.meta_data.get(mp.name):
issues.append(format_html(
'<a href="{href}{href_hash}">{text}</a>',
text=gettext('You need to fill the meta parameter "{property}".').format(property=mp.name),
href=reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
href_hash=f'#id_prop-{mp.pk}-value',
))
issues.append(
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
property=mp.name,
a_attr='href="%s#id_prop-%d-value"' % (
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
mp.pk
)
)
)
responses = event_live_issues.send(self)
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
@@ -1884,8 +1869,6 @@ class EventMetaValue(LoggedModel):
self.event.cache.clear()
def save(self, *args, **kwargs):
if self.event and self.event.organizer != self.property.organizer:
raise ValidationError(_("Property and event must belong to the same organizer."))
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
+36 -54
View File
@@ -224,6 +224,8 @@ class Order(LockModel, LoggedModel):
"Organizer",
related_name="orders",
on_delete=models.CASCADE,
null=True,
blank=True,
)
event = models.ForeignKey(
Event,
@@ -327,7 +329,7 @@ class Order(LockModel, LoggedModel):
default="line",
)
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
class Meta:
verbose_name = _("Order")
@@ -352,60 +354,38 @@ class Order(LockModel, LoggedModel):
def _transaction_key_reset(self):
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
@classmethod
def gracefully_delete_bulk(cls, event, orders, user=None, auth=None):
# Expects to be called in a transaction
from . import (
GiftCard, GiftCardTransaction, LogEntry, Membership, Voucher,
)
if not transaction.get_connection().in_atomic_block:
raise Exception('gracefully_delete_bulk should only be called in atomic transaction!')
logs_create = []
for o in orders:
if not o.testmode:
raise TypeError("Only test mode orders can be deleted.")
order_gracefully_delete.send(event, order=o)
logs_create.append(o.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': o.code,
},
save=False,
))
LogEntry.bulk_create_and_postprocess(logs_create)
voucher_ids = OrderPosition.objects.filter(
order__in=orders,
voucher__isnull=False
).exclude(order__status=Order.STATUS_CANCELED).values_list("voucher_id", flat=True)
voucher_usages = Counter(voucher_ids)
for v_id, usage_count in voucher_usages.items():
Voucher.objects.filter(pk=v_id).update(redeemed=Greatest(0, F('redeemed') - usage_count))
GiftCardTransaction.objects.filter(payment__order__in=orders).update(payment=None)
GiftCardTransaction.objects.filter(refund__order__in=orders).update(refund=None)
GiftCardTransaction.objects.filter(order__in=orders).update(order=None)
GiftCard.objects.filter(issued_in__order__in=orders).update(issued_in=None)
Membership.objects.filter(granted_in__order__in=orders, testmode=True).update(granted_in=None)
OrderPosition.all.filter(order__in=orders, addon_to__isnull=False).delete()
OrderPosition.all.filter(order__in=orders).delete()
OrderFee.all.filter(order__in=orders).delete()
Transaction.objects.filter(order__in=orders).delete()
OrderRefund.objects.filter(order__in=orders).delete()
OrderPayment.objects.filter(order__in=orders).delete()
if isinstance(orders, models.QuerySet):
orders.delete()
else:
Order.objects.filter(pk__in=[o.pk for o in orders]).delete()
event.cache.delete('complain_testmode_orders')
def gracefully_delete(self, user=None, auth=None):
from . import GiftCard, GiftCardTransaction, Membership, Voucher
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
self.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
}
)
Order.gracefully_delete_bulk(self.event, Order.objects.filter(pk=self.pk), user, auth)
order_gracefully_delete.send(self.event, order=self)
if self.status != Order.STATUS_CANCELED:
for position in self.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None)
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
GiftCardTransaction.objects.filter(order=self).update(order=None)
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete()
self.refunds.all().delete()
self.payments.all().delete()
self.event.cache.delete('complain_testmode_orders')
self.delete()
def email_confirm_secret(self):
return self.tagged_secret("email_confirm", 9)
@@ -1695,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
self.company,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name if self.country else ''
self.country.name
]
lines = [r.strip() for r in lines if r]
return '\n'.join(lines).strip()
@@ -2539,6 +2519,8 @@ class OrderPosition(AbstractPosition):
"Organizer",
related_name="order_positions",
on_delete=models.CASCADE,
null=True,
blank=True,
)
order = models.ForeignKey(
Order,
@@ -2595,7 +2577,7 @@ class OrderPosition(AbstractPosition):
blank=True,
)
all = ScopedManager(organizer='organizer')
all = ScopedManager(organizer='order__event__organizer')
objects = ActivePositionManager()
def __init__(self, *args, **kwargs):
@@ -3434,7 +3416,7 @@ class InvoiceAddress(models.Model):
self.name,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name if self.country else '',
self.country.name,
self.vat_id,
self.custom_field,
self.internal_reference,
-7
View File
@@ -35,7 +35,6 @@ import operator
import string
from datetime import date, datetime, time
from functools import reduce
from typing import TYPE_CHECKING
import pytz_deprecation_shim
from django.conf import settings
@@ -62,9 +61,6 @@ from ...helpers.permission_migration import (
from ..settings import settings_hierarkey
from .auth import User
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
@settings_hierarkey.add(cache_namespace='organizer')
class Organizer(LoggedModel):
@@ -82,9 +78,6 @@ class Organizer(LoggedModel):
"""
settings_namespace = 'organizer'
if TYPE_CHECKING:
settings: HierarkeyProxy
name = models.CharField(max_length=200,
verbose_name=_("Name"))
slug = models.CharField(
+3 -6
View File
@@ -118,10 +118,7 @@ class SeatingPlan(LoggedModel):
for zi, z in enumerate(self.layout_data['zones']):
zpos = (z['position']['x'], z['position']['y'])
for ri, r in enumerate(z['rows']):
rpos = (
zpos[0] + r.get('position', {}).get('x', 0),
zpos[1] + r.get('position', {}).get('y', 0),
)
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y'])
row_label = None
if r.get('row_label'):
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
@@ -150,8 +147,8 @@ class SeatingPlan(LoggedModel):
zone=z['name'],
category=s['category'],
sorting_rank=rank,
x=rpos[0] + s.get('position', {}).get('x', 0),
y=rpos[1] + s.get('position', {}).get('y', 0),
x=rpos[0] + s['position']['x'],
y=rpos[1] + s['position']['y'],
)
+38 -45
View File
@@ -32,10 +32,8 @@
# 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 datetime
from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal
from typing import Union
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -423,33 +421,27 @@ class Voucher(LoggedModel):
return False
@staticmethod
def get_affected_quotas(quota, item, variation, subevent):
if quota:
return {quota}
elif item and variation:
return set(variation.quotas.filter(subevent=subevent))
elif item and not item.has_variations:
return set(item.quotas.filter(subevent=subevent))
elif item and item.has_variations:
return set(
Quota.objects.filter(
pk__in=Quota.variations.through.objects.filter(
itemvariation__item=item,
quota__subevent=subevent,
).values('quota_id')
)
)
else:
return set()
@staticmethod
def clean_quota_get_ignored(voucher_data: Union["VoucherBulkData", "Voucher"]):
if voucher_data:
valid = voucher_data.valid_until is None or voucher_data.valid_until >= now()
if valid and voucher_data.block_quota and voucher_data.max_usages > voucher_data.redeemed:
return Voucher.get_affected_quotas(voucher_data.quota, voucher_data.item, voucher_data.variation, voucher_data.subevent)
return set()
def clean_quota_get_ignored(old_instance):
quotas = set()
was_valid = old_instance and (
old_instance.valid_until is None or old_instance.valid_until >= now()
)
if old_instance and old_instance.block_quota and was_valid:
if old_instance.quota:
quotas.add(old_instance.quota)
elif old_instance.variation:
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
elif old_instance.item:
if old_instance.item.has_variations:
quotas |= set(
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
itemvariation__item=old_instance.item,
quota__subevent=old_instance.subevent,
).values('quota_id'))
)
else:
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
return quotas
@staticmethod
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
@@ -461,8 +453,22 @@ class Voucher(LoggedModel):
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
new_quotas = Voucher.get_affected_quotas(quota, item, variation, data.get('subevent'))
if not new_quotas:
if quota:
new_quotas = {quota}
elif item and variation:
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
elif item and not item.has_variations:
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
elif item and item.has_variations:
new_quotas = set(
Quota.objects.filter(
pk__in=Quota.variations.through.objects.filter(
itemvariation__item=item,
quota__subevent=data.get('subevent'),
).values('quota_id')
)
)
else:
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
'tickets.'))
@@ -638,16 +644,3 @@ class Voucher(LoggedModel):
]
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
return ops
@dataclass
class VoucherBulkData:
item: object
variation: object
quota: object
block_quota: bool
valid_until: datetime.datetime
subevent: object
redeemed: int
max_usages: int
allow_ignore_quota: bool
+2 -2
View File
@@ -43,7 +43,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types
from pretix.base.templatetags.money import money_filter
from pretix.helpers.urls import mainreverse_absolute
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
_ALL_TYPES = None
@@ -170,7 +170,7 @@ class ParametrizedOrderNotificationType(NotificationType):
def build_notification(self, logentry: LogEntry):
order = logentry.content_object
order_url = mainreverse_absolute(
order_url = build_absolute_uri(
'control:event.order',
kwargs={
'organizer': logentry.event.organizer.slug,
+3 -55
View File
@@ -71,7 +71,7 @@ from pretix.helpers import OF_SELF
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.multidomain.urlreverse import build_absolute_uri
from pretix.presale.views import get_cart
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
@@ -379,7 +379,7 @@ class BasePaymentProvider:
if not self.settings.get('_hidden_seed'):
self.settings.set('_hidden_seed', get_random_string(64))
hidden_url = eventreverse_absolute(self.event, 'presale:event.payment.unlock', kwargs={
hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
})
@@ -834,7 +834,7 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str | None:
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
"""
After the user has confirmed their purchase, this method will be called to complete
the payment process. This is the place to actually move the money if applicable.
@@ -1706,58 +1706,6 @@ class GiftCardPayment(BasePaymentProvider):
)
class BaseHistoricalPaymentProvider(BasePaymentProvider):
"""
Base class for payment providers that no longer exist but can't be deleted to make sure historical
payments are shown correctly.
Subclasses are recommended to only implement:
- identifier
- verbose_name
- public_name
- payment_control_render
- payment_control_render_short
- refund_control_render
- refund_control_render_short
- render_invoice_text
- render_invoice_stamp
- api_payment_details
- api_refund_details
- shred_payment_info
- matching_id
- refund_matching_id
"""
@property
def is_enabled(self) -> bool:
return False
@property
def settings_form_fields(self) -> dict:
return {}
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return False
def payment_is_valid_session(self, request: HttpRequest, payment: OrderPayment):
return False
def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool:
return False
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return False
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
return False
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
def execute_refund(self, refund: OrderRefund):
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
+6 -16
View File
@@ -77,7 +77,6 @@ from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -373,11 +372,6 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Atlantis"),
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
}),
("invoice_custom_field", {
"label": _("Invoice custom recipient field"),
"editor_sample": _("Custom recipient field"),
"evaluate": lambda op, order, ev: order.invoice_address.custom_field if getattr(order, 'invoice_address', None) else ''
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
@@ -402,7 +396,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}),
("event_info_text", {}), # Placeholder to "reserve" position, defined later in `get_variables`
("event_info_text", {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
}),
("now_date", {
"label": _("Printing date"),
"editor_sample": _("2017-05-31"),
@@ -667,14 +665,6 @@ def get_images(event):
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
templating_context = PlaceholderContext(event=event)
v['event_info_text'] = {
"label": _("Event info text"),
"editor_sample": _("Event info text"),
"evaluate": lambda op, order, ev:
templating_context.format(str(order.event.settings.event_info_text))
}
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
@@ -1072,7 +1062,7 @@ class Renderer:
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
p = Paragraph(text, style=style)
return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
+1 -3
View File
@@ -22,7 +22,6 @@
import logging
from decimal import Decimal
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
from django.utils.crypto import get_random_string
@@ -378,13 +377,12 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
confirmation_code = get_random_string(8, allowed_chars="01234567890")
mail(
user.email,
subject=gettext('Confirm event cancellation and bulk refund'),
subject=gettext('Bulk-refund confirmation'),
template='pretixbase/email/cancel_confirm.txt',
context={
"event": str(event),
"amount": money_filter(refund_total, event.currency),
"confirmation_code": confirmation_code,
"instance": settings.PRETIX_INSTANCE_NAME,
},
locale=user.locale,
)
+4 -5
View File
@@ -51,7 +51,7 @@ from pretix.base.signals import (
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF, repeatable_reads_transaction
from pretix.helpers.urls import mainreverse_absolute
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -340,13 +340,12 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
if schedule.owner.is_active:
mail(
email=schedule.owner.email,
subject=gettext('Scheduled export failed'),
subject=gettext('Export failed'),
template='pretixbase/email/export_failed.txt',
context={
'configuration_url': config_url,
'reason': msg,
'soft': soft,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=context if isinstance(context, Event) else None,
organizer=context.organizer if isinstance(context, Event) else context,
@@ -456,7 +455,7 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
schedule,
organizer,
exporter,
mainreverse_absolute(
build_absolute_uri(
'control:organizer.export',
kwargs={
'organizer': organizer.slug,
@@ -482,7 +481,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
schedule,
event,
exporter,
mainreverse_absolute(
build_absolute_uri(
'control:event.orders.export',
kwargs={
'event': event.slug,
+3 -3
View File
@@ -85,7 +85,7 @@ from pretix.helpers.format import (
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
logger = logging.getLogger('pretix.base.mail')
@@ -997,7 +997,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
orderurl=eventreverse_absolute(
orderurl=build_absolute_uri(
order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
@@ -1013,7 +1013,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=eventreverse_absolute(
event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
+3 -33
View File
@@ -26,9 +26,8 @@ from typing import List
from django.conf import settings as django_settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.utils import IntegrityError
from django.utils.timezone import now
from django.utils.translation import gettext as _, ngettext
from django.utils.translation import gettext as _
from pretix.base.i18n import language
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
@@ -261,7 +260,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
# shorter. We'll see what works better in reality…
vouchers = []
codes = set()
lock_seats = []
for i, record in enumerate(data):
try:
@@ -270,14 +268,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
if not record.get("code"):
raise ValidationError(_('A voucher cannot be created without a code.'))
code = record.get("code")
if code.upper() in codes:
raise ValidationError(
_('Voucher codes must be unique. Code "{code}" already exists in this import.').format(
code=code,
)
)
codes.add(code.upper())
Voucher.clean_item_properties(
record,
event,
@@ -296,22 +286,8 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
lock_seats.append(voucher.seat)
except (ValidationError, ImportError) as e:
raise DataImportError(
_('Invalid data in row {row}: {message}').format(row=i + 1, message=str(e))
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
)
existing_codes = Voucher.objects.filter(
event=event,
code__in=codes,
).values_list("code", flat=True)
if len(existing_codes):
raise DataImportError(
ngettext(
'Voucher codes must be unique. Import contains existing voucher code {code}.',
'Voucher codes must be unique. Import contains existing voucher codes {code}.',
len(existing_codes)
).format(
code=", ".join(existing_codes)
)
)
with transaction.atomic():
# We don't support quotas here, so we only need to lock if seats are in use
@@ -324,13 +300,7 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
save_logentries = []
for v in vouchers:
try:
v.save()
except IntegrityError:
# should not happen as we check existing codes before, but we did not lock so we might have a race-condition
raise DataImportError(
_('Vouchers could not be imported, probably due to a voucher code already being in use.')
)
v.save()
save_logentries.append(v.log_action(
'pretix.voucher.added',
user=user,
+3 -3
View File
@@ -37,7 +37,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import notification
from pretix.celery_app import app
from pretix.helpers.celery import get_task_priority
from pretix.helpers.urls import mainreverse_absolute
from pretix.helpers.urls import build_absolute_uri
@app.task(base=TransactionAwareTask, acks_late=True, max_retries=9, default_retry_delay=900)
@@ -136,10 +136,10 @@ def send_notification_mail(notification: Notification, user: User):
'site_url': settings.SITE_URL,
'color': settings.PRETIX_PRIMARY_COLOR,
'notification': notification,
'settings_url': mainreverse_absolute(
'settings_url': build_absolute_uri(
'control:user.settings.notifications',
),
'disable_url': mainreverse_absolute(
'disable_url': build_absolute_uri(
'control:user.settings.notifications.off',
kwargs={
'token': user.notifications_token,
+29 -29
View File
@@ -327,7 +327,7 @@ def get_best_name(position_or_address, parts=False):
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
def _event_sample(event):
if event.has_subevents:
@@ -388,14 +388,14 @@ def base_placeholders(sender, **kwargs):
lambda event: LazyDate(now() + timedelta(days=15))
),
SimpleFunctionalTextPlaceholder(
'url', ['order', 'event'], lambda order, event: eventreverse_absolute(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_secret()
}
), lambda event: eventreverse_absolute(
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
@@ -406,7 +406,7 @@ def base_placeholders(sender, **kwargs):
),
SimpleButtonPlaceholder(
'url_button', ['order', 'event'],
url_func=lambda order, event: eventreverse_absolute(
url_func=lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
@@ -415,7 +415,7 @@ def base_placeholders(sender, **kwargs):
}
),
text_func=lambda order, event: _("View order details"),
sample_url_func=lambda event: eventreverse_absolute(
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
@@ -426,13 +426,13 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View order details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: eventreverse_absolute(
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
@@ -441,13 +441,13 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: eventreverse_absolute(
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
@@ -456,13 +456,13 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: eventreverse_absolute(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: eventreverse_absolute(
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
@@ -471,7 +471,7 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'position'], lambda event, position: eventreverse_absolute(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
@@ -480,7 +480,7 @@ def base_placeholders(sender, **kwargs):
'position': position.positionid
}
),
lambda event: eventreverse_absolute(
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
@@ -491,7 +491,7 @@ def base_placeholders(sender, **kwargs):
),
SimpleButtonPlaceholder(
'url_button', ['event', 'position'],
url_func=lambda event, position: eventreverse_absolute(
url_func=lambda event, position: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': position.order.code,
@@ -500,7 +500,7 @@ def base_placeholders(sender, **kwargs):
}
),
text_func=lambda event, position: _("View registration details"),
sample_url_func=lambda event: eventreverse_absolute(
sample_url_func=lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
@@ -511,14 +511,14 @@ def base_placeholders(sender, **kwargs):
sample_text_func=lambda event: _("View registration details"),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
event,
'presale:event.order.position.modify', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
), lambda event: eventreverse_absolute(
), lambda event: build_absolute_uri(
event,
'presale:event.order.position.modify', kwargs={
'order': 'F8VVL',
@@ -528,14 +528,14 @@ def base_placeholders(sender, **kwargs):
),
),
SimpleFunctionalTextPlaceholder(
'url_products_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
event,
'presale:event.order.position.change', kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
), lambda event: eventreverse_absolute(
), lambda event: build_absolute_uri(
event,
'presale:event.order.position.change', kwargs={
'order': 'F8VVL',
@@ -581,20 +581,20 @@ def base_placeholders(sender, **kwargs):
),
SimpleFunctionalTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: eventreverse_absolute(
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: eventreverse_absolute(
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: eventreverse_absolute(
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: eventreverse_absolute(
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
@@ -611,7 +611,7 @@ def base_placeholders(sender, **kwargs):
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
@@ -623,7 +623,7 @@ def base_placeholders(sender, **kwargs):
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
@@ -662,13 +662,13 @@ def base_placeholders(sender, **kwargs):
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
eventreverse_absolute(
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
eventreverse_absolute(
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
@@ -676,10 +676,10 @@ def base_placeholders(sender, **kwargs):
inline=False,
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: eventreverse_absolute(event, 'presale:event.index', kwargs={
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: eventreverse_absolute(event, 'presale:event.index', kwargs={
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
+3 -4
View File
@@ -44,7 +44,7 @@ from django.conf import settings
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name
@@ -171,16 +171,15 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
if user:
with language(user.locale):
event_name = str(event.name)
mail(
user.email,
gettext('Data shredding completed for %(event)s') % {'event': event_name},
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': event_name,
'event': str(event.name),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
},
+2 -2
View File
@@ -37,7 +37,7 @@ from pretix.base.services.mail import mail
from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import periodic_task
from pretix.celery_app import app
from pretix.helpers.urls import mainreverse_absolute
from pretix.helpers.urls import build_absolute_uri
@receiver(signal=periodic_task)
@@ -121,7 +121,7 @@ def send_update_notification_email():
)
),
{
'url': mainreverse_absolute('control:global.update')
'url': build_absolute_uri('control:global.update')
},
)
+3 -12
View File
@@ -1276,7 +1276,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict(
label=_("Allow updating existing invoices"),
label=_("Allow to update existing invoices"),
help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we "
"recommend to leave this option turned off and always issue a new invoice if a change needs "
"to be made."),
@@ -1924,6 +1924,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Hide all past dates from calendar"),
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
"calendar.")
)
},
'allow_modifications': {
@@ -2284,17 +2286,6 @@ DEFAULTS = {
help_text=_("We'll show this publicly to allow attendees to contact you.")
)
},
'contact_url': {
'default': None,
'type': str,
'serializer_class': serializers.URLField,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Contact URL"),
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
},
'imprint_url': {
'default': None,
'type': str,
+2 -3
View File
@@ -535,9 +535,8 @@ EventPluginRegistry = PluginAwareRegistry # for backwards compatibility
event_live_issues = EventPluginSignal()
"""
This signal is sent out to determine whether an event can be taken live. If you want to
prevent the event from going live, return an error message to display to the user (either
as a SafeString containing HTML, or a string that will be HTML-escaped). If you don't,
your receiver should return ``None``.
prevent the event from going live, return a string that will be displayed to the user
as the error message. If you don't, your receiver should return ``None``.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -1,14 +1,10 @@
{% load i18n %}
{% trans "You requested to cancel an event that involves a large bulk refund:" %}
{% trans "You have requested us to cancel an event which includes a larger bulk-refund:" %}
- {% trans "Event" %}: {{ event }}
- {% trans "Estimated refund" %}: **{{ amount }}**
{% trans "Event" %}: {{ event }}
{% trans "To confirm, paste the following code into the cancellation form:" %}
{% trans "Estimated refund amount" %}: **{{ amount }}**
{{ confirmation_code }}
{% trans "Please confirm that you want to proceed by coping the following confirmation code into the cancellation form:" %}
{% blocktrans with instance=instance %}Don't share this code with anyone. The {{ instance }} team will never ask you for it.{% endblocktrans %}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
**{{ confirmation_code }}**
@@ -1,13 +1,12 @@
{% load i18n %}
{% trans "Your scheduled export failed." %}
{% trans "Your export failed." %}
- {% trans "Reason" %}: {{ reason }}
{% trans "Reason:" %} {{ reason }}
{% if not soft %}{% trans "If an export fails five times in a row, we'll stop sending it." %}{% endif %}
{% if not soft %}
{% trans "If your export fails five times in a row, it will no longer be sent." %}
{% endif %}
{% trans "You can adjust or remove this export here:" %}
{% trans "Configuration link:" %}
{{ configuration_url }}
{% blocktrans with instance=instance %}Thanks,
The {{ instance }} Team{% endblocktrans %}
@@ -52,13 +52,13 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% trans "You're receiving this email based on your notification settings." %}<br>
{% trans "You receive these emails based on your notification settings." %}<br>
<a href="{{ settings_url }}">
{% trans "Manage settings" %}
{% trans "Click here to view and change your notification settings" %}
</a>
{% if disable_url %}<br>
<a href="{{ disable_url }}">
{% trans "Disable all notifications" %}
{% trans "Click here disable all notifications immediately." %}
</a>
{% endif %}
</div>
@@ -1,21 +1,19 @@
{% load i18n %}
{{ notification.title }}{% if notification.detail %}
{{ notification.detail }}{% endif %}{% if notification.url %}
{{ notification.detail }}
{% endif %}{% if notification.url %}
{{ notification.url }}{% endif %}{% if notification.attributes %}
{{ notification.url }}{% endif %}{% for attr in notification.attributes %}
{% for attr in notification.attributes %}- {{ attr.title }}: {{ attr.value }}
{% endfor %}{% endif %}{% for action in notification.actions %}
{{ attr.title }}: {{ attr.value }}{% endfor %}{% for action in notification.actions %}
{{ action.label }}:
{{ action.label }}
{{ action.url }}{% endfor %}
{{ action.url }}{% endfor %}
---
{% trans "You're receiving this email based on your notification settings." %}
- {% trans "Manage settings" %}: {{ settings_url }}
{% if disable_url %}- {% trans "Disable all notifications" %}: {{ disable_url }}
{% trans "You receive these emails based on your notification settings." %}
{% trans "Click here to view and change your notification settings:" %}
{{ settings_url }}
{% if disable_url %}{% trans "Click here disable all notifications immediately:" %}
{{ disable_url }}
{% endif %}
@@ -1,14 +1,17 @@
{% load i18n %}{% blocktrans %}Hello,
{% load i18n %}
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
The following data shredding job has been completed:
we hereby confirm that the following data shredding job has been completed:
- Organizer: {{ organizer }}
- Event: {{ event }}
- Data selection: {{ shredders }}
- Start time: {{ start_time }}
Organizer: {{ organizer }}
Any data added to the event after the start time may not have been deleted.
Event: {{ event }}
Thanks,
The {{ instance }} Team
Data selection: {{ shredders }}
Start time: {{ start_time }} (new data added after this time might not have been deleted)
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,28 +0,0 @@
{% extends "error.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load static %}
{% block content %}
<h1>{% trans "Please continue in a new tab" %}</h1>
<p class="larger">
{% blocktrans trimmed %}
For security reasons, the following step is only possible in a new tab.
{% endblocktrans %}
</p>
<p class="larger">
{% blocktrans trimmed %}
If the new tab did not open automatically, please click the following button:
{% endblocktrans %}
</p>
<div class="text-center">
<a href="{{ url }}"
class="btn btn-primary btn-lg" target="_blank">
<span class="fa fa-external-link-square"></span>
{% trans "Continue in new tab" %}
</a>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</div>
{% endblock %}
@@ -2,14 +2,13 @@
{% load i18n %}
{% load rich_text %}
{% load static %}
{% load wrap_in %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<h3>
{% blocktrans trimmed with host=hostname|wrap_in:'strong' %}
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %}
{% blocktrans trimmed %}
+2 -3
View File
@@ -22,7 +22,6 @@
import importlib
from django import template
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from pretix.base.models import Event
@@ -45,7 +44,7 @@ def eventsignal(event: Event, signame: str, **kwargs):
_html = []
for receiver, response in signal.send(event, **kwargs):
if response:
_html.append(conditional_escape(response))
_html.append(response)
return mark_safe("".join(_html))
@@ -64,5 +63,5 @@ def signal(signame: str, request, **kwargs):
_html = []
for receiver, response in signal.send(request, **kwargs):
if response:
_html.append(conditional_escape(response))
_html.append(response)
return mark_safe("".join(_html))
+4 -2
View File
@@ -42,6 +42,8 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
from bleach.linkifier import build_email_re
from django import template
from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme
@@ -52,7 +54,6 @@ from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set
from pretix.base.views.redirect import safelink
from pretix.helpers.format import SafeFormatter, format_map
register = template.Library()
@@ -157,7 +158,8 @@ def safelink_callback(attrs, new=False):
"""
url = html.unescape(attrs.get((None, 'href'), '/'))
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
attrs[None, 'href'] = safelink(url)
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
+1 -1
View File
@@ -27,11 +27,11 @@ from django.template import TemplateDoesNotExist, loader
from django.template.loader import get_template
from django.utils.functional import Promise
from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token
from sentry_sdk import last_event_id
from pretix.base.i18n import language
from pretix.base.middleware import get_language_from_request
from pretix.multidomain.middlewares import requires_csrf_token
def csrf_failure(request, reason=""):
+6 -29
View File
@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import urllib.parse
from django.core import signing
@@ -27,8 +26,6 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
logger = logging.getLogger(__name__)
def _is_samesite_referer(request):
referer = request.headers.get('referer')
@@ -45,16 +42,11 @@ def _is_samesite_referer(request):
def redir_view(request):
framebreak = "framebreak" in request.GET
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
signer = signing.Signer(salt='safe-redirect')
try:
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
try:
# Backwards-compatibility for a change in 2026-06, remove after a while
url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
@@ -63,26 +55,11 @@ def redir_view(request):
'url': url,
})
if framebreak:
r = render(request, 'pretixbase/framebreak.html', {
'url': url,
})
r.xframe_options_exempt = True
return r
r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex'
return r
def safelink(url, framebreak=False):
url = str(url)
if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")):
logger.warning('Invalid URL passed to safelink: %r', url)
return '#invalid-url'
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
signer = signing.Signer(salt=salt)
u = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
if framebreak:
u += "&framebreak=true"
return u
def safelink(url):
signer = signing.Signer(salt='safe-redirect')
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
+1 -43
View File
@@ -19,56 +19,14 @@
# 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
import os
from celery import Celery, signals
from django.dispatch import receiver
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
logger = logging.getLogger(__name__)
from django.conf import settings
app = Celery('pretix')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
@receiver(signals.before_task_publish)
def on_before_task_publish(sender, body, exchange, routing_key, headers, properties, declare, retry_policy, **kwargs):
from pretix.helpers.logs import local
trace = getattr(local, 'trace', [])
request_id = getattr(local, 'request_id', None)
if request_id:
trace.append(request_id)
headers["X-Pretix-Trace"] = " ".join(trace)
@receiver(signals.task_received)
def on_task_received(sender, request, **kwargs):
trace = request._request_dict.get("X-Pretix-Trace")
if trace:
logger.info(f"Task {request.id} has trace {trace}")
@receiver(signals.task_prerun)
def on_task_prerun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = task_id
if task.request.headers and "X-Pretix-Trace" in task.request.headers:
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
else:
local.trace = []
local.trace.append(task_id)
@receiver(signals.task_postrun)
def on_task_postrun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = None
local.trace = []
+3 -9
View File
@@ -80,7 +80,7 @@ from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
from pretix.multidomain.urlreverse import (
eventreverse_absolute, get_organizer_domain,
build_absolute_uri, get_organizer_domain,
)
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
@@ -219,7 +219,7 @@ class EventWizardBasicsForm(I18nModelForm):
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
)
self.fields['slug'].widget.prefix = eventreverse_absolute(self.organizer, 'presale:organizer.index')
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index')
self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required
if self.has_subevents:
del self.fields['presale_start']
@@ -625,7 +625,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'max_items_per_order',
'reservation_time',
'contact_mail',
'contact_url',
'show_variations_expanded',
'hide_sold_out',
'meta_noindex',
@@ -672,11 +671,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
base_context = {
'frontpage_text': ['event'],
'presale_has_ended_text': ['event'],
'voucher_explanation_text': ['event'],
'banner_text': ['event'],
'banner_text_bottom': ['event'],
'event_info_text': ['event'],
}
def _resolve_virtual_keys_input(self, data, prefix=''):
@@ -1679,7 +1673,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField(
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
choices=CountriesAndEUAndStates(),
required=False
)
address_type = forms.ChoiceField(
+1 -7
View File
@@ -1342,13 +1342,7 @@ class QuestionAnswerFilterForm(forms.Form):
opqs = opqs.filter(canceled=False)
if fdata.get("item", "") != "":
i = fdata.get("item", "")
if '-' in i:
opqs = opqs.filter(
item_id=i.split('-')[0],
variation_id=i.split('-')[1],
)
else:
opqs = opqs.filter(item_id=i)
opqs = opqs.filter(item_id__in=(i,))
return opqs
+2 -3
View File
@@ -88,7 +88,7 @@ from pretix.control.forms.event import (
)
from pretix.control.forms.widgets import Select2, Select2Multiple
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
class OrganizerForm(I18nModelForm):
@@ -604,7 +604,6 @@ class OrganizerSettingsForm(SettingsForm):
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'contact_url',
'imprint_url',
'organizer_info_text',
'event_list_type',
@@ -792,7 +791,7 @@ class MailSettingsForm(SettingsForm):
}
if 'url' in base_parameters:
placeholders['url'] = eventreverse_absolute(
placeholders['url'] = build_absolute_uri(
self.organizer,
'presale:organizer.customer.activate'
) + '?token=' + get_random_string(30)
+51 -280
View File
@@ -32,20 +32,16 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import copy
import csv
from collections import Counter, namedtuple
from collections import namedtuple
from io import StringIO
from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import EmailValidator
from django.db.models import Count, F, Max
from django.db.models.functions import Upper
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField
from pretix.base.email import get_available_placeholders
@@ -54,10 +50,7 @@ from pretix.base.forms import (
)
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import language
from pretix.base.models import Item, ItemVariation, Quota, SubEvent, Voucher
from pretix.base.models.vouchers import VoucherBulkData
from pretix.base.services.locking import lock_objects
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.models import Item, Voucher
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
from pretix.control.signals import voucher_form_validation
@@ -112,22 +105,15 @@ class VoucherForm(I18nModelForm):
except Item.DoesNotExist:
pass
super().__init__(*args, **kwargs)
if not self.event and self.instance:
self.event = self.instance.event
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
})
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
if instance.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
}),
}
)
@@ -137,19 +123,18 @@ class VoucherForm(I18nModelForm):
del self.fields['subevent']
choices = []
prefix = (self.prefix + '-') if self.prefix else ''
if 'itemvar' in initial or (self.data and prefix + 'itemvar' in self.data):
iv = self.data.get(prefix + 'itemvar', '') or initial.get('itemvar', '') or ''
if 'itemvar' in initial or (self.data and 'itemvar' in self.data):
iv = self.data.get('itemvar') or initial.get('itemvar', '')
if iv.startswith('q-'):
q = self.event.quotas.get(pk=iv[2:])
q = self.instance.event.quotas.get(pk=iv[2:])
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
elif '-' in iv:
itemid, varid = iv.split('-')
i = self.event.items.get(pk=itemid)
i = self.instance.event.items.get(pk=itemid)
v = i.variations.get(pk=varid)
choices.append(('%d-%d' % (i.pk, v.pk), '%s %s' % (str(i), v.value)))
elif iv:
i = self.event.items.get(pk=iv)
i = self.instance.event.items.get(pk=iv)
if i.variations.exists():
choices.append((str(i.pk), _('{product} Any variation').format(product=i)))
else:
@@ -160,8 +145,8 @@ class VoucherForm(I18nModelForm):
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
}),
'data-placeholder': _('All products')
}
@@ -169,7 +154,7 @@ class VoucherForm(I18nModelForm):
self.fields['itemvar'].required = False
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
if self.event.seating_plan or self.event.subevents.filter(seating_plan__isnull=False).exists():
if self.instance.event.seating_plan or self.instance.event.subevents.filter(seating_plan__isnull=False).exists():
self.fields['seat'] = forms.CharField(
label=_("Specific seat ID"),
max_length=255,
@@ -179,45 +164,40 @@ class VoucherForm(I18nModelForm):
help_text=str(self.instance.seat) if self.instance.seat else '',
)
def parse_itemvar(self, data):
try:
itemid = quotaid = None
iv = data.get('itemvar', '')
if iv.startswith('q-'):
quotaid = iv[2:]
elif '-' in iv:
itemid, varid = iv.split('-')
elif iv:
itemid, varid = iv, None
else:
itemid, varid = None, None
if itemid:
item = self.event.items.get(pk=itemid)
if varid:
variation = item.variations.get(pk=varid)
else:
variation = None
quota = None
elif quotaid:
quota = self.event.quotas.get(pk=quotaid)
item = None
variation = None
else:
quota = None
item = None
variation = None
return (item, variation, quota)
except ObjectDoesNotExist:
raise ValidationError(_("Invalid product selected."))
def clean(self):
data = super().clean()
if not self._errors:
self.instance.item, self.instance.variation, self.instance.quota = self.parse_itemvar(self.data)
try:
itemid = quotaid = None
iv = self.data.get('itemvar', '')
if iv.startswith('q-'):
quotaid = iv[2:]
elif '-' in iv:
itemid, varid = iv.split('-')
elif iv:
itemid, varid = iv, None
else:
itemid, varid = None, None
if itemid:
self.instance.item = self.instance.event.items.get(pk=itemid)
if varid:
self.instance.variation = self.instance.item.variations.get(pk=varid)
else:
self.instance.variation = None
self.instance.quota = None
elif quotaid:
self.instance.quota = self.instance.event.quotas.get(pk=quotaid)
self.instance.item = None
self.instance.variation = None
else:
self.instance.quota = None
self.instance.item = None
self.instance.variation = None
except ObjectDoesNotExist:
raise ValidationError(_("Invalid product selected."))
if 'codes' in data:
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
@@ -229,7 +209,7 @@ class VoucherForm(I18nModelForm):
try:
Voucher.clean_item_properties(
data, self.event,
data, self.instance.event,
self.instance.quota, self.instance.item, self.instance.variation,
seats_given=data.get('seat') or data.get('seats'),
block_quota=data.get('block_quota')
@@ -249,7 +229,7 @@ class VoucherForm(I18nModelForm):
try:
Voucher.clean_subevent(
data, self.event
data, self.instance.event
)
except ValidationError as e:
raise ValidationError({"subevent": e.message})
@@ -265,19 +245,19 @@ class VoucherForm(I18nModelForm):
if check_quota:
Voucher.clean_quota_check(
data, cnt, self.initial_instance_data,
self.event, self.instance.quota, self.instance.item, self.instance.variation
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
)
Voucher.clean_voucher_code(data, self.event, self.instance.pk)
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
if 'seat' in self.fields:
if data.get('seat'):
self.instance.seat = Voucher.clean_seat_id(
data, self.instance.item, self.instance.quota, self.event, self.instance.pk
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
)
self.instance.item = self.instance.seat.product
else:
self.instance.seat = None
voucher_form_validation.send(sender=self.event, form=self, data=data)
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
return data
@@ -285,215 +265,6 @@ class VoucherForm(I18nModelForm):
return super().save(commit)
class VoucherBulkEditForm(VoucherForm):
def __init__(self, *args, **kwargs):
self.mixed_values = kwargs.pop('mixed_values')
self.queryset = kwargs.pop('queryset')
super().__init__(**kwargs)
del self.fields["code"]
self.fields.pop("seat", None)
def is_bulk_checked(self, fieldname):
return self.prefix + fieldname in self.data.getlist('_bulk')
def clean(self):
# We skip the parent class because it's not suited for bulk editing and implement custom validation here.
# This does not validate *everything* we validate in VoucherForm. For example, we skip validation that one does
# not create a voucher for an add-on product or that the seat matches the product to save on complexity.
# This is a UX validation only anyway, since one could first create the voucher and then make the product an
# add-on product. However, we need to validate everything that we don't want violated in the database.
data = super(VoucherForm, self).clean()
if self.is_bulk_checked("itemvar"):
data["item"], data["variation"], data["quota"] = self.parse_itemvar(data)
if self.is_bulk_checked("max_usages") and "max_usages" in data:
max_redeemed = self.queryset.aggregate(m=Max("redeemed"))["m"]
if data["max_usages"] < max_redeemed:
raise ValidationError(_(
"You cannot reduce the maximum number of redemptions to %(max_usages)s, because at least one "
"of the selected vouchers has already been redeemed %(max_redeemed)s times."
) % {"max_usages": data["max_usages"], "max_redeemed": max_redeemed})
# Check diff on product and quota usage based on old groups of vouchers
if any(self.is_bulk_checked(k) for k in ("max_usages", "itemvar", "block_quota", "valid_until", "subevent")):
quota_diff = Counter()
current_vouchers = self.queryset.order_by().values(
"item", "variation", "quota", "block_quota", "valid_until", "subevent", "redeemed", "max_usages",
"allow_ignore_quota",
).annotate(c=Count("*"))
item_cache = {i.pk: i for i in Item.objects.filter(pk__in=[c["item"] for c in current_vouchers])}
var_cache = {v.pk: v for v in ItemVariation.objects.filter(pk__in=[c["variation"] for c in current_vouchers])}
quota_cache = {q.pk: q for q in Quota.objects.filter(pk__in=[c["quota"] for c in current_vouchers])}
subevent_cache = {s.pk: s for s in SubEvent.objects.filter(pk__in=[c["subevent"] for c in current_vouchers])}
for current in current_vouchers:
bulk_count = current.pop('c')
current = VoucherBulkData(**current)
# Get quotas that are currently used
if current.item:
current.item = item_cache[current.item]
if current.variation:
current.variation = var_cache[current.variation]
if current.quota:
current.quota = quota_cache[current.quota]
if current.subevent:
current.subevent = subevent_cache[current.subevent]
old_quotas = Voucher.clean_quota_get_ignored(current)
old_amount = max(current.max_usages - current.redeemed, 0) * bulk_count
# Predict state after change
after_change = copy.copy(current)
if self.is_bulk_checked("itemvar") and "itemvar" in data:
after_change.item = data["item"]
after_change.variation = data["variation"]
after_change.quota = data["quota"]
if self.is_bulk_checked("subevent") and "subevent" in data:
after_change.subevent = data["subevent"]
if self.is_bulk_checked("max_usages") and "max_usages" in data:
after_change.max_usages = data["max_usages"]
if self.is_bulk_checked("block_quota") and "block_quota" in data:
after_change.block_quota = data["block_quota"]
if self.is_bulk_checked("valid_until") and "valid_until" in data:
after_change.valid_until = data["valid_until"]
if self.is_bulk_checked("allow_ignore_quota") and "allow_ignore_quota" in data:
after_change.allow_ignore_quota = data["allow_ignore_quota"]
if after_change.quota and self.event.has_subevents and not after_change.subevent:
raise ValidationError(_("You cannot create a voucher that allows selection of a quota but has no date selected."))
if after_change.quota and after_change.subevent and after_change.quota.subevent_id != after_change.subevent.pk:
raise ValidationError(_("The selected quota does not match the selected subevent."))
if after_change.block_quota and self.event.has_subevents and not after_change.subevent:
raise ValidationError(
_('If you want this voucher to block quota, you need to select a specific date.'))
if after_change.block_quota and not after_change.item and not after_change.quota:
raise ValidationError(
_('You need to select a specific product or quota if this voucher should reserve '
'tickets.')
)
if after_change.allow_ignore_quota:
# todo: is this the most useful way to do this?
continue
new_quotas = Voucher.clean_quota_get_ignored(after_change)
new_amount = max(after_change.max_usages - after_change.redeemed, 0) * bulk_count
if new_quotas != old_quotas or new_amount != old_amount:
for q in old_quotas:
quota_diff[q] -= old_amount
for q in new_quotas:
quota_diff[q] += new_amount
if any(v > 0 for q, v in quota_diff.items()):
lock_objects([q for q, v in quota_diff.items() if q.size is not None and v > 0], shared_lock_objects=[self.event])
qa = QuotaAvailability(count_waitinglist=False)
qa.queue(*(q for q, v in quota_diff.items() if v > 0))
qa.compute()
if any(qa.results[q][0] != Quota.AVAILABILITY_OK or (qa.results[q][1] is not None and qa.results[q][1] < required)
for q, required in quota_diff.items() if required > 0):
raise ValidationError(_(
'There is no sufficient quota available to perform this change.'
))
has_seat = self.queryset.filter(seat__isnull=False).exists()
if has_seat:
if self.is_bulk_checked("max_usages"):
raise ValidationError(_(
'Changing the maximum number of usages in bulk is not supported if any of the selected vouchers '
'is assigned a seat.'
))
if self.is_bulk_checked("subevent"):
raise ValidationError(pgettext_lazy(
'subevent',
'Changing the date in bulk is not supported if any of the selected vouchers '
'is assigned a seat.'
))
if self.is_bulk_checked("itemvar") and data["quota"]:
raise ValidationError(_(
'Changing the product to a quota is not supported if any of the selected vouchers '
'is assigned a seat.'
))
if self.is_bulk_checked("valid_until"):
if data["valid_until"] is None or data["valid_until"] >= now():
currently_not_blocked_seats = self.queryset.filter(
seat__isnull=False,
max_usages__gt=F("redeemed"),
valid_until__lt=now(),
)
if self.event.has_subevents:
subevents = self.event.subevents.filter(pk__in=currently_not_blocked_seats.values_list("subevent"))
for se in subevents:
conflicts = currently_not_blocked_seats.filter(
subevent=se
).exclude(
seat_id__in=se.free_seats().values("pk")
)
if conflicts:
raise ValidationError(_(
'This change cannot be completed because not all assigned seats of the vouchers are '
'still available'
))
else:
conflicts = currently_not_blocked_seats.exclude(
seat_id__in=self.event.free_seats().values("pk")
)
if conflicts:
raise ValidationError(_(
'This change cannot be completed because not all assigned seats of the vouchers are '
'still available'
))
return data
def save(self, commit=True):
objs = list(self.queryset)
fields = set()
check_map = {
'price_mode': '__price',
'value': '__price',
}
for k in self.fields:
if not self.is_bulk_checked(check_map.get(k, k)):
continue
if k == 'itemvar':
fields.add("item")
fields.add("variation")
fields.add("quota")
else:
fields.add(k)
for obj in objs:
if k == 'itemvar':
obj.item = self.cleaned_data["item"]
obj.variation = self.cleaned_data["variation"]
obj.quota = self.cleaned_data["quota"]
else:
setattr(obj, k, self.cleaned_data[k])
fields = [f for f in fields if f != 'itemvars']
if fields:
Voucher.objects.bulk_update(objs, fields, 200)
def full_clean(self):
if len(self.data) == 0:
# form wasn't submitted
self._errors = ErrorDict()
return
super().full_clean()
def _post_clean(self):
pass # skip model-level clean
class VoucherBulkForm(VoucherForm):
codes = forms.CharField(
widget=forms.Textarea,
+11 -28
View File
@@ -36,8 +36,7 @@ from urllib.parse import quote, urljoin, urlparse
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.contrib.auth.views import redirect_to_login
from django.http import Http404, HttpResponse
from django.http import Http404
from django.shortcuts import get_object_or_404, resolve_url
from django.template.response import TemplateResponse
from django.urls import get_script_prefix, resolve, reverse
@@ -98,8 +97,6 @@ class PermissionMiddleware:
super().__init__()
def _login_redirect(self, request):
from django.contrib.auth.views import redirect_to_login
# Taken from django/contrib/auth/decorators.py
path = request.build_absolute_uri()
# urlparse chokes on lazy objects in Python 3, force to str
@@ -112,21 +109,10 @@ class PermissionMiddleware:
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
from django.contrib.auth.views import redirect_to_login
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
# It's not useful to return a 302 redirect on a XMLHttpRequest request, because
# the XMLHttpRequest is unable to detect redirects.
return HttpResponse(
"Authentication required",
status=401,
headers={
# Appending ?next= is handled by client, because it should be the top-level context url,
# not the URL called in the background
"X-Login-Url": resolved_login_url,
}
)
return redirect_to_login(path, resolved_login_url, REDIRECT_FIELD_NAME)
return redirect_to_login(
path, resolved_login_url, REDIRECT_FIELD_NAME)
def __call__(self, request):
url = resolve(request.path_info)
@@ -226,17 +212,14 @@ class AuditLogMiddleware:
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if getattr(request.user, "is_hijacked", False):
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
hijacker = get_object_or_404(User, pk=hijack_history[0])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if not ss:
# Staff session expired or not found
logout(request)
return redirect_to_login(request.get_full_path())
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
if ss:
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
else:
ss = request.user.get_active_staff_session(request.session.session_key)
if ss:
+4 -12
View File
@@ -39,8 +39,7 @@ from pretix.base.signals import (
html_page_start = GlobalSignal()
"""
This signal allows you to put code in the beginning of the main page for every
page in the backend. You are expected to return a SafeString containing HTML, or
a string that will be HTML-escaped.
page in the backend. You are expected to return HTML.
The ``sender`` keyword argument will contain the request.
"""
@@ -130,7 +129,7 @@ event_dashboard_top = EventPluginSignal()
Arguments: 'request'
This signal is sent out to include custom HTML in the top part of the the event dashboard.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
Receivers should return HTML.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
An additional keyword argument ``subevent`` *can* contain a sub-event.
@@ -173,7 +172,6 @@ Arguments: 'form'
This signal allows you to add additional HTML to the form that is used for modifying vouchers.
You receive the form object in the ``form`` keyword argument.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -211,7 +209,6 @@ Arguments: 'quota'
This signal allows you to append HTML to a Quota's detail view. You receive the
quota as argument in the ``quota`` keyword argument.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -222,7 +219,6 @@ Arguments: 'subevent'
This signal allows you to append HTML to a SubEvent's detail view. You receive the
subevent as argument in the ``subevent`` keyword argument.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -269,8 +265,7 @@ order_info = EventPluginSignal()
"""
Arguments: ``order``, ``request``
This signal is sent out to display additional information on the order detail page.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
This signal is sent out to display additional information on the order detail page
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
@@ -280,8 +275,7 @@ order_approve_info = EventPluginSignal()
"""
Arguments: ``order``, ``request``
This signal is sent out to display additional information on the order approve page.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
This signal is sent out to display additional information on the order approve page
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
@@ -292,7 +286,6 @@ order_position_buttons = EventPluginSignal()
Arguments: ``order``, ``position``, ``request``
This signal is sent out to display additional buttons for a single position of an order.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
@@ -322,7 +315,6 @@ Arguments: 'request'
This signal is sent out to include template snippets on the settings page of an event
that allows generating a pretix Widget code.
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
A second keyword argument ``request`` will contain the request object.
+22 -23
View File
@@ -38,7 +38,6 @@ from pretix import __version__
from pretix.base.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR
@@ -80,23 +79,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6
story = [
PlainTextParagraph("System report", headlinestyle),
Paragraph("System report", headlinestyle),
Spacer(1, 5 * mm),
PlainTextParagraph("Usage", subheadlinestyle),
Paragraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm),
self._usage_table(),
Spacer(1, 5 * mm),
PlainTextParagraph("Installed versions", subheadlinestyle),
Paragraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm),
self._tech_table(),
Spacer(1, 5 * mm),
PlainTextParagraph("Plugins", subheadlinestyle),
Paragraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm),
PlainTextParagraph(self._get_plugin_versions(), style_small),
Paragraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm),
PlainTextParagraph("Custom templates", subheadlinestyle),
Paragraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm),
PlainTextParagraph(self._get_custom_templates(), style_small),
Paragraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm),
]
@@ -122,13 +121,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
]
tdata = [
[PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
[
PlainTextParagraph("Database engine:", style),
PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
Paragraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
],
]
table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -207,7 +206,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year
tdata = [
[
PlainTextParagraph(l, style_small_head)
Paragraph(l, style_small_head)
for l in (
"Time frame",
"Currency",
@@ -258,19 +257,19 @@ class SysReport(ReportlabExportMixin):
tdata.append(
(
PlainTextParagraph(
Paragraph(
date_format(first_day, "M Y")
+ " "
+ date_format(after_day - timedelta(days=1), "M Y"),
style_small,
),
PlainTextParagraph(c, style_small),
PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
Paragraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
)
)
@@ -56,4 +56,5 @@
</form>
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
<script type="text/plain" id="bad_origin_report_url">{{ bad_origin_report_url }}</script>
<!-- pretix-login-marker -->{# marker required for ajax calls to detect that user session is over #}
{% endblock %}
@@ -1,13 +1,13 @@
{% load i18n %}{% blocktrans with code=code reason=reason instance=instance %}Hello,
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
{{ reason }}
{{ code }}
Don't share this code with anyone. The {{ instance }} team will never ask you for it.
Please do never give this code to another person. Our support team will never ask for this code.
If you didn't request this code, please contact us immediately.
If this code was not requested by you, please contact us immediately.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,15 +1,14 @@
{% load i18n %}{% blocktrans with code=code address=address instance=instance %}Hello,
{% load i18n %}{% blocktrans with code=code instance=instance %}Hello,
Someone requested to use {{ address }} as a sender address on {{ instance }}. Once verified, emails sent from {{ instance }} can show this address as the sender.
someone requested to use {{ address }} as a sender address on {{ instance }}.
This will allow them to send emails that are shown to originate from this email address.
If that was you, please enter the following confirmation code:
If this was you, enter the following code in the setup form:
{{ code }}
{{ code }}
If this was not requested by you, you can safely ignore this email.
Don't share this code with anyone unless you want to authorize them to use this address for this purpose. The {{ instance }} team will never ask you for it.
Best regards,
If you didn't request this, you can safely ignore this email.
Thanks,
The {{ instance }} Team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}
@@ -1,11 +1,9 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
We received a request to reset the password for your {{ instance }} account. To choose a new password, follow the link below:
you requested a new password. Please go to the following page to reset your password:
{{ url }}
If you didn't request this, you can safely ignore this email — your password won't change.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,16 +1,17 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
You've been invited to join a team on {{ instance }}, an event ticket sales platform.
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
- Organizer: {{ organizer }}
- Team: {{ team }}
To accept, follow the link below:
Organizer: {{ organizer }}
Team: {{ team }}
If you want to join that team, just click on the following link:
{{ url }}
If you don't want to join, you can safely ignore this email.
If you do not want to join, you can safely ignore or delete this email.
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,19 +1,13 @@
{% load i18n %}{% blocktrans %}Hello,
{% load i18n %}{% blocktrans with url=url|safe os=source.os_type agent=source.agent_type %}Hello,
We noticed a new sign-in to your {{ instance }} account:
{% endblocktrans %}
- {% trans "Time" %}: {{ when }}
- {% trans "Browser" %}: {{ agent }}
- {% trans "Operating system" %}: {{ os }}
{% if device %}- {% trans "Device" %}: {{ device }}
{% endif %}{% if country %}- {% trans "Country" %}: {{ country }}
{% endif %}
{% blocktrans with url=url|safe %}If it was you, no action is needed.
a login to your {{ instance }} account from an unusual or new location was detected. The login was performed using {{ agent }} on {{ os }} from {{ country }}.
If you don't recognize this sign-in, please change your password immediately:
If this was you, you can safely ignore this email.
If this was not you, we recommend that you change your password in your account settings:
{{ url }}
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -1,15 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
The following changes were made to your {{ instance }} account:
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
If you didn't make these changes, please contact the {{ instance }} support team immediately.
If this change was not performed by you, please contact us immediately.
You can review your account settings here:
You can review and change your account settings here:
{{ url }}
Thanks,
The {{ instance }} Team
Best regards,
Your {{ instance }} team
{% endblocktrans %}
@@ -50,46 +50,6 @@
{% endblocktrans %}
</div>
{% endif %}
{% if dkim_warning %}
<div class="alert alert-danger">
<p>
{{ dkim_warning }}
</p>
<p>
{% trans "Your new DKIM record should be set up as a CNAME record like this:" %}
</p>
<pre><code>{{ dkim_hostname }} CNAME {{ dkim_cname }}</code></pre>
<p>
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
</p>
</div>
{% elif dkim_cname %}
<div class="alert alert-success">
{% blocktrans trimmed %}
We found a DKIM record on your domain for this system. Great!
{% endblocktrans %}
</div>
{% endif %}
{% if dmarc_warning %}
<div class="alert alert-danger">
<p>
{{ dmarc_warning }}
</p>
<p>
{% trans "Your new DMARC record could look like this:" %}
</p>
<pre><code>_dmarc.{{ hostname }} TXT "v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;"</code></pre>
<p>
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
</p>
</div>
{% elif dkim_cname %}
<div class="alert alert-success">
{% blocktrans trimmed %}
We found a DMARC record on your domain for this system. Great!
{% endblocktrans %}
</div>
{% endif %}
{% if verification %}
<h3>{% trans "Verification" %}</h3>
<p>
@@ -110,7 +70,7 @@
</div>
</div>
{% if spf_warning or dkim_warning or dmarc_warning %}
{% if spf_warning %}
<div class="form-group submit-group">
<a href="" class="btn btn-default btn-save">
{% trans "Cancel" %}
@@ -19,7 +19,7 @@
</p>
<ul>
{% for issue in issues %}
<li>{{ issue }}</li>
<li>{{ issue|safe }}</li>
{% endfor %}
</ul>
</div>
@@ -42,7 +42,7 @@
</p>
<ul>
{% for issue in issues %}
<li>{{ issue }}</li>
<li>{{ issue|safe }}</li>
{% endfor %}
</ul>
</div>
@@ -30,7 +30,6 @@
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% bootstrap_field form.all_sales_channels layout="control" %}
@@ -34,7 +34,6 @@
{% endif %}
{% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.contact_url layout="control" %}
{% bootstrap_field sform.organizer_info_text layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %}
{% if sform.allowed_restricted_plugins %}
@@ -19,7 +19,9 @@
{% endif %}
</h1>
{{ layout|json_script:"editor-data" }}
<script type="application/json" id="editor-data">
{{ layout|safe }}
</script>
<div class="row">
<div class="col-md-9">
<div class="panel panel-default panel-pdf-editor">
@@ -560,10 +562,11 @@
</div>
</div>
</div>
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
<script type="text/javascript" src="{% static "ajv/ajv2020.bundle.min.js" %}"></script>
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
<script type="text/javascript" src="{% static "schema/pdf-layout.validate.js" %}"></script>
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_white.png' %}" id="poweredby-white" class="sr-only">
{% for family, styles in fonts.items %}
@@ -1,83 +0,0 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load eventsignal %}
{% load eventurl %}
{% block title %}{% trans "Change multiple vouchers" %}{% endblock %}
{% block inside %}
<h1>
{% trans "Change multiple vouchers" %}
<small>
{% blocktrans trimmed with number=vouchers.count %}
{{ number }} selected
{% endblocktrans %}
</small>
</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<div class="hidden">
{% for v in vouchers %}
<input type="hidden" name="voucher" value="{{ v.pk }}">
{% endfor %}
</div>
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Voucher details" %}</legend>
{% bootstrap_field form.max_usages layout="bulkedit" %}
{% bootstrap_field form.valid_until layout="bulkedit" %}
{% bootstrap_field form.itemvar layout="bulkedit" %}
<div class="bulk-edit-field-group">
<label class="field-toggle">
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__price" {% if form.prefix|add:"__price" in bulk_selected %}checked{% endif %}>
{% trans "change" context "form_bulk" %}
</label>
<div class="field-content">
<div class="form-group">
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
<div class="col-md-5">
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.value show_label=False form_group_class="" %}
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<div class="controls">
<div class="alert alert-info">
{% blocktrans trimmed %}
If you choose "any product" for a specific quota and choose to reserve quota for this
voucher above, the product can still be unavailable to the voucher holder if another quota
associated with the product is sold out!
{% endblocktrans %}
</div>
</div>
</div>
</div>
{% if form.subevent %}
{% bootstrap_field form.subevent layout="bulkedit" %}
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.block_quota layout="bulkedit" %}
{% bootstrap_field form.allow_ignore_quota layout="bulkedit" %}
{% bootstrap_field form.min_usages layout="bulkedit" %}
{% bootstrap_field form.budget addon_after=request.event.currency layout="bulkedit" %}
{% bootstrap_field form.tag layout="bulkedit" %}
{% bootstrap_field form.comment layout="bulkedit" %}
{% bootstrap_field form.show_hidden_items layout="bulkedit" %}
{% bootstrap_field form.all_addons_included layout="bulkedit" %}
{% bootstrap_field form.all_bundles_included layout="bulkedit" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -99,9 +99,6 @@
</p>
<form action="{% url "control:event.vouchers.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
{% csrf_token %}
{% for field in filter_form %}
{{ field.as_hidden }}
{% endfor %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
@@ -147,18 +144,6 @@
{% endif %}
<th></th>
</tr>
{% if "event.vouchers:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
</td>
<td colspan="5">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead>
<tbody>
{% for v in vouchers %}
@@ -226,10 +211,6 @@
<i class="fa fa-trash" aria-hidden="true"></i>
{% trans "Delete selected" %}
</button>
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
formaction="{% url "control:event.vouchers.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
</button>
</div>
{% endif %}
</form>
@@ -49,11 +49,11 @@
<td>
<strong>
{% if t.tag %}
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
{{ t.tag }}
</a>
{% else %}
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '<>'|urlencode }}">
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '<>'|urlencode }}">
{% trans "Empty tag" %}
</a>
{% endif %}
-2
View File
@@ -370,7 +370,6 @@ urlpatterns = [
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'),
re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'),
re_path(r'^vouchers/tags/typeahead$', typeahead.voucher_tag_typeahead, name='event.vouchers.tags.typeahead'),
re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'),
re_path(r'^vouchers/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'),
re_path(r'^vouchers/(?P<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'),
@@ -383,7 +382,6 @@ urlpatterns = [
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
re_path(r'^vouchers/bulk_edit$', vouchers.VoucherBulkUpdateView.as_view(), name='event.vouchers.bulkedit'),
re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'),
re_path(r'^vouchers/import/(?P<file>[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
+1 -2
View File
@@ -243,8 +243,7 @@ def invite(request, token):
if request.user.is_authenticated:
if inv.team.members.filter(pk=request.user.pk).exists():
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
'this team. If you want to add a different user or create a new account, '
'log out and click the invitation link again.').format(inv.team.name))
'this team.').format(inv.team.name))
return redirect('control:index')
else:
with transaction.atomic():
+12 -26
View File
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
from decimal import Decimal
from io import BytesIO
from itertools import groupby
from urllib.parse import urlsplit
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo
import bleach
@@ -64,6 +64,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import NoReverseMatch, reverse
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
@@ -96,9 +97,7 @@ from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.control.views.user import RecentAuthenticationRequiredMixin
from pretix.helpers.database import rolledback_transaction
from pretix.multidomain.urlreverse import (
eventreverse_absolute, get_event_domain,
)
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
from pretix.presale.views.widget import (
version_default as widget_version_default,
)
@@ -952,12 +951,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
context=context,
)
r = HttpResponse(v, content_type='text/html')
r['Content-Security-Policy'] = (
# Plugin-provided email templates will contain inline styles or remote images
# but emails should not contain JS
"style-src 'unsafe-inline'; "
"img-src https: data:"
)
r._csp_ignore = True
return r
else:
raise Http404(_('Unknown email renderer.'))
@@ -1154,11 +1148,8 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if request.POST.get("delete") == "yes":
try:
with transaction.atomic():
Order.gracefully_delete_bulk(
request.event,
request.event.orders.filter(testmode=True),
user=self.request.user
)
for order in request.event.orders.filter(testmode=True):
order.gracefully_delete(user=self.request.user)
except ProtectedError:
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
'created by plug-ins) do not allow it.'))
@@ -1433,16 +1424,11 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
form.instance.custom_rules = json.dumps([
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
], cls=I18nJSONEncoder)
if form.has_changed() or self.formset.has_changed():
change_data = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
if self.formset.has_changed():
change_data["custom_rules"] = [
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
]
if form.has_changed():
self.object.log_action(
'pretix.event.taxrule.changed', user=self.request.user, data=change_data
'pretix.event.taxrule.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
return super().form_valid(form)
@@ -1748,10 +1734,10 @@ class EventQRCode(EventPermissionRequiredMixin, View):
permission = None
def get(self, request, *args, filetype, **kwargs):
url = eventreverse_absolute(request.event, 'presale:event.index')
url = build_absolute_uri(request.event, 'presale:event.index')
if "url" in request.GET:
if request.GET["url"].startswith(url):
if url_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]):
url = request.GET["url"]
else:
raise PermissionDenied("Untrusted URL")
+8 -80
View File
@@ -28,7 +28,7 @@ from django.core.mail import get_connection
from django.shortcuts import redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from pretix.base import email
@@ -41,30 +41,6 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
logger = logging.getLogger(__name__)
def get_cname_record(hostname):
try:
r = dns.resolver.Resolver()
answers = r.resolve(hostname, 'CNAME')
answers = list(answers)
if len(answers) != 1:
logger.exception('Found multiple CNAME records for {}'.format(hostname))
return
return str(answers[0].target).lower()
except:
logger.exception('Could not fetch CNAME record for {}'.format(hostname))
def get_dmarc_record(hostname):
try:
r = dns.resolver.Resolver()
for resp in r.resolve("_dmarc." + hostname, 'TXT'):
data = b''.join(resp.strings).decode()
if 'DMARC1' in data.strip():
return data
except:
logger.exception("Could not fetch DMARC record for {}".format(hostname))
def get_spf_record(hostname):
try:
r = dns.resolver.Resolver()
@@ -73,7 +49,7 @@ def get_spf_record(hostname):
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
return data
except:
logger.exception("Could not fetch SPF record for {}".format(hostname))
logger.exception("Could not fetch SPF record")
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
@@ -192,15 +168,10 @@ class MailSettingsSetupView(TemplateView):
return super().get(request, *args, **kwargs)
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
verify_dns = (
settings.MAIL_CUSTOM_SENDER_SPF_STRING or
(settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR) or
settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED
)
allow_save = (
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
(not verify_dns or self.request.POST.get('state') == 'save')
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
)
if allow_save:
@@ -221,8 +192,8 @@ class MailSettingsSetupView(TemplateView):
spf_warning = None
spf_record = None
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
spf_record = get_spf_record(hostname)
if not spf_record:
spf_warning = _(
@@ -239,56 +210,19 @@ class MailSettingsSetupView(TemplateView):
'this system in the SPF record.'
)
dkim_warning = None
dkim_hostname = None
dkim_cname = None
if settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR:
dkim_hostname = settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR + '._domainkey.' + hostname
cname_target = get_cname_record(dkim_hostname)
dkim_cname = settings.MAIL_CUSTOM_SENDER_DKIM_CNAME
if not dkim_cname.endswith("."):
dkim_cname += "."
if "%s" in dkim_cname:
dkim_cname = dkim_cname.replace("%s", hostname.replace(".", "-").lower())
if not cname_target:
dkim_warning = _(
'We could not find a CNAME record pointing to our DKIM key for domain you are trying to use. '
'This means that there is a very high change most of the emails will be rejected or marked as '
'spam. We strongly recommend setting up DKIM through a CNAME record. You can do so through the '
'DNS settings at the provider you registered your domain with.'
)
elif cname_target != dkim_cname:
dkim_warning = _(
'We found a CNAME record for a DKIM key, but it is not pointing to the right location. '
'This means that there is a very high chance most of the emails will be rejected or marked as '
'spam. You should update the DNS settings of your domain.'
)
dmarc_warning = None
dmarc_record = None
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
dmarc_record = get_dmarc_record(hostname)
if not dmarc_record:
spf_warning = _(
'We did not find DMARC record for your domain. This means that there is a very high chance '
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
'of your domain.'
)
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning and not dkim_warning and not dmarc_warning
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
if verification:
if 'verification' in self.request.POST:
messages.error(request, _('The verification code was incorrect, please try again.'))
else:
self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890')
sender_address = self.simple_form.cleaned_data.get('mail_from')
mail(
sender_address,
gettext('Confirm %(address)s as a sender address') % {'address': sender_address},
self.simple_form.cleaned_data.get('mail_from'),
_('Sender address verification'),
'pretixcontrol/email/email_setup.txt',
{
'code': self.request.session[session_key],
'address': sender_address,
'address': self.simple_form.cleaned_data.get('mail_from'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
None,
@@ -306,12 +240,6 @@ class MailSettingsSetupView(TemplateView):
'spf_warning': spf_warning,
'spf_record': spf_record,
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
'dkim_warning': dkim_warning,
'dkim_hostname': dkim_hostname,
'dkim_cname': dkim_cname,
'dmarc_warning': dmarc_warning,
'dmarc_record': dmarc_record,
'hostname': hostname,
'recp': self.simple_form.cleaned_data.get('mail_from')
},
using=self.template_engine,
+4 -17
View File
@@ -139,7 +139,6 @@ from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.json import CustomJSONEncoder
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -241,7 +240,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
raise NotImplementedError()
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
qs = self.allowed_for(self.get_queryset())
qs = self.allowed_for(self.allowed_for(self.get_queryset()))
total = qs.count()
orders_with_successful_action = 0
for i, o in enumerate(qs):
@@ -395,21 +394,9 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
testmode=True,
)
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
qs = self.allowed_for(self.get_queryset())
total = qs.count()
all_ids = list(qs.values_list("id", flat=True))
orders_with_successful_action = 0
for chunk in chunked_iterable(all_ids, 1000):
Order.gracefully_delete_bulk(
self.request.event,
qs.filter(id__in=chunk),
user=self.request.user,
)
orders_with_successful_action += len(chunk)
self.async_set_progress(orders_with_successful_action / total * 100)
return orders_with_successful_action, total
def execute_single(self, instance, form: forms.Form):
instance.gracefully_delete(user=self.request.user)
return True
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
+9 -13
View File
@@ -139,8 +139,8 @@ from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.urls import mainreverse_absolute
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator
logger = logging.getLogger(__name__)
@@ -1032,16 +1032,14 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
gettext('You\'ve been invited to join %(organizer)s') % {
'organizer': self.request.organizer.name,
},
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
'url': mainreverse_absolute('control:auth.invite', kwargs={
'url': build_global_uri('control:auth.invite', kwargs={
'token': instance.token
})
},
@@ -2853,12 +2851,10 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['redirect_uri'] = eventreverse_absolute(
self.request.organizer, 'presale:organizer.customer.login.return',
kwargs={
'provider': self.object.pk
}
)
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return',
kwargs={
'provider': self.object.pk
})
return ctx
def get_form_kwargs(self):
@@ -3089,7 +3085,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
ctx = self.customer.get_email_context()
token = TokenGenerator().make_token(self.customer)
ctx['url'] = eventreverse_absolute(
ctx['url'] = build_absolute_uri(
self.request.organizer,
'presale:organizer.customer.recoverpw'
) + '?id=' + self.customer.identifier + '&token=' + token
+2 -1
View File
@@ -70,6 +70,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
if 'placeholders' in request.GET:
return self.get_placeholders_help(request)
resp = super().get(request, *args, **kwargs)
resp._csp_ignore = True
return resp
def get_placeholders_help(self, request):
@@ -283,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables()
ctx['images'] = self.get_images()
ctx['layout'] = self.get_current_layout()
ctx['layout'] = json.dumps(self.get_current_layout())
ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize
-1
View File
@@ -537,7 +537,6 @@ class SubEventDetail(EventPermissionRequiredMixin, DetailView):
pcnt=Subquery(
OrderPosition.objects.filter(
subevent=self.object,
order_id=OuterRef("id"),
).values("subevent").annotate(c=Count("*")).values("c")
),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))),
-15
View File
@@ -975,21 +975,6 @@ def subevent_meta_values(request, organizer, event):
})
@event_permission_required('event.vouchers:read')
def voucher_tag_typeahead(request, **kwargs):
q = request.GET.get('q', '')
tags = request.event.vouchers.filter(
tag__isnull=False,
waitinglistentries__isnull=True,
).filter(
tag__icontains=q,
).values_list('tag', flat=True).distinct().order_by('tag')[:10]
return JsonResponse({
'results': [{'name': t} for t in tags]
})
def item_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
+16 -56
View File
@@ -19,23 +19,19 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import hmac
import json
from contextlib import contextmanager
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
logout,
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views import View
@@ -222,13 +218,11 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
staff_session = request.user.get_active_staff_session(request.session.session_key)
self.request.user.log_action('pretix.control.auth.user.impersonated',
user=request.user,
data={
'other': self.kwargs.get("id"),
'other_email': self.object.email,
'staff_session': staff_session.pk,
'other_email': self.object.email
})
oldkey = request.session.session_key
@@ -236,15 +230,7 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
hijacked = self.object
hijack_history = request.session.get("hijack_history", [])
hijack_history.append({
"user": request.user.pk,
# We include the auth_hash, because it is unguessable. So should an attacker gain an attack vector to
# modify hijack_history, they can't just insert or change a user that shouldn't be there. We HMAC it
# again, though, since we also do not want the auth_hash of the admin user to be in the session of an
# unprivileged user to contain the risk if there is some leak of session data.
"auth_hash": salted_hmac(key_salt=b"hijack-history-hash", value=request.session[HASH_SESSION_KEY],
algorithm="sha256", secret=settings.SECRET_KEY).hexdigest(),
})
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
@@ -252,12 +238,6 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
with signals.no_update_last_login(), keep_session_age(request.session):
login(request, hijacked, backend=backend)
request.session.save()
staff_session.logs.create(
method='(NOTE)',
url=f'Begin impersonating user #{hijacked.pk} (request session {oldkey[:8]} -> {request.session.session_key[:8]})',
)
request.session["hijack_history"] = hijack_history
signals.hijack_started.send(
@@ -274,28 +254,13 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
class UserImpersonateStopView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
staff_session_key = request.session['hijacker_session']
prev_session_key = request.session.session_key
impersonated = request.user
hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", [])
hijacked = request.user
prev_session = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
staff_session = hijacker.get_active_staff_session(staff_session_key)
if not staff_session:
raise PermissionDenied
expected_hash = salted_hmac(
key_salt=b"hijack-history-hash",
value=hijacker.get_session_auth_hash(),
algorithm="sha256",
secret=settings.SECRET_KEY
).hexdigest()
if not hmac.compare_digest(expected_hash, prev_session["auth_hash"]):
# Could be an attacker-controlled hijack history, but could also be e.g. a password change of the admin user
# that happened during the hijack session
logout(request)
return redirect_to_login(request.get_full_path())
user_pk = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session):
@@ -310,22 +275,17 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
hijacked=hijacked,
)
request.session.save()
staff_session.session_key = request.session.session_key
staff_session.save()
staff_session.logs.create(
method='(NOTE)',
url=f'Stop impersonating user #{hijacked.pk} (request session {prev_session_key[:8]}, '
f'staff session {staff_session_key[:8]} -> {request.session.session_key[:8]})',
)
ss = request.user.get_active_staff_session(hijs)
if ss:
request.session.save()
ss.session_key = request.session.session_key
ss.save()
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
user=request.user,
data={
'other': hijacked.pk,
'other_email': hijacked.email,
'staff_session': staff_session.pk,
'other': impersonated.pk,
'other_email': impersonated.email
})
return redirect(reverse('control:index'))
+29 -170
View File
@@ -40,11 +40,9 @@ import bleach
from defusedcsv import csv
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import (
BadRequest, PermissionDenied, ValidationError,
)
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import connection, transaction
from django.db.models import Count, Exists, OuterRef, Sum
from django.db.models import Exists, OuterRef, Sum
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
JsonResponse,
@@ -57,7 +55,7 @@ from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views.generic import (
CreateView, FormView, ListView, TemplateView, UpdateView, View,
CreateView, ListView, TemplateView, UpdateView, View,
)
from django_scopes import scopes_disabled
@@ -72,49 +70,17 @@ from pretix.base.services.vouchers import vouchers_send
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncFormView
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
from pretix.control.forms.vouchers import (
VoucherBulkEditForm, VoucherBulkForm, VoucherForm,
)
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import voucher_form_class
from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.models import modelcopy
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.multidomain.urlreverse import build_absolute_uri
class VoucherQueryMixin:
@cached_property
def request_data(self):
if self.request.method == "POST":
return self.request.POST
return self.request.GET
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):
qs = self.request.event.vouchers.exclude(
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
)
if 'voucher' in self.request_data and '__ALL' not in self.request_data:
qs = qs.filter(
id__in=self.request_data.getlist('voucher')
)
elif self.request.method == 'GET' or '__ALL' in self.request_data:
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
else:
raise BadRequest("No vouchers selected")
return qs
@cached_property
def filter_form(self):
return VoucherFilterForm(data=self.request_data, prefix='filter', event=self.request.event)
class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMixin, ListView):
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
model = Voucher
context_object_name = 'vouchers'
template_name = 'pretixcontrol/vouchers/index.html'
@@ -122,15 +88,25 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):
return Voucher.annotate_budget_used(super().get_queryset().select_related(
qs = Voucher.annotate_budget_used(self.request.event.vouchers.exclude(
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
).select_related(
'item', 'variation', 'seat'
))
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
return ctx
@cached_property
def filter_form(self):
return VoucherFilterForm(data=self.request.GET, event=self.request.event)
def get(self, request, *args, **kwargs):
if request.GET.get("download", "") == "yes":
return self._download_csv()
@@ -317,12 +293,6 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
f.disabled = True
return form
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"event": self.request.event,
}
def get_object(self, queryset=None) -> VoucherForm:
url = resolve(self.request.path_info)
try:
@@ -368,7 +338,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
}
if self.object.subevent_id:
url_params['subevent'] = self.object.subevent_id
ctx['url'] = eventreverse_absolute(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
ctx['url'] = build_absolute_uri(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
return ctx
@@ -633,21 +603,26 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
})
class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
class VoucherBulkAction(EventPermissionRequiredMixin, View):
permission = 'event.vouchers:write'
@cached_property
def objects(self):
return self.request.event.vouchers.filter(
id__in=self.request.POST.getlist('voucher')
)
@transaction.atomic
def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'delete':
return render(request, 'pretixcontrol/vouchers/delete_bulk.html', {
'allowed': self.get_queryset().filter(redeemed=0),
'forbidden': self.get_queryset().exclude(redeemed=0),
'allowed': self.objects.filter(redeemed=0),
'forbidden': self.objects.exclude(redeemed=0),
})
elif request.POST.get('action') == 'delete_confirm':
log_entries = []
to_delete = []
to_update = []
for obj in self.get_queryset():
for obj in self.objects:
if obj.allow_delete():
log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
to_delete.append(obj.pk)
@@ -657,14 +632,12 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
'bulk': True
}, save=False))
obj.max_usages = min(obj.redeemed, obj.max_usages)
to_update.append(obj)
obj.save(update_fields=['max_usages'])
if to_delete:
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
Voucher.objects.filter(pk__in=to_delete).delete()
if to_update:
Voucher.objects.bulk_update(to_update, ['max_usages'])
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, _('The selected vouchers have been deleted or disabled.'))
@@ -675,117 +648,3 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/vouchers/bulk_edit.html'
permission = 'event.vouchers:write'
context_object_name = 'vouchers'
form_class = VoucherBulkEditForm
def get_queryset(self):
return super().get_queryset().prefetch_related(None).order_by()
def get(self, request, *args, **kwargs):
return HttpResponse(status=405)
@cached_property
def is_submitted(self):
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
# that behavior
return '_bulk' in self.request.POST
def get_form_kwargs(self):
initial = {}
mixed_values = set()
qs = self.get_queryset().annotate()
fields = (
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', 'max_usages',
'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included', 'all_bundles_included',
'budget',
)
for f in fields:
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
if len(existing_values) == 1:
initial[f] = existing_values[0][f]
elif len(existing_values) > 1:
mixed_values.add(f)
if f == "max_usages":
initial[f] = 1
else:
initial[f] = None
existing_values = list(qs.order_by("item", "variation", "quota").values("item", "variation", "quota").annotate(c=Count('*')))
if len(existing_values) == 1:
i = existing_values[0]
if i["quota"]:
initial["itemvar"] = f'q-{i["quota"]}'
elif i["variation"]:
initial["itemvar"] = f'{i["item"]}-{i["variation"]}'
elif i["item"]:
initial["itemvar"] = f'{i["item"]}'
else:
initial["itemvar"] = None
elif len(existing_values) > 1:
mixed_values.add("itemvar")
initial["itemvar"] = None
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
kwargs['prefix'] = 'bulkedit'
kwargs['initial'] = initial
kwargs['queryset'] = self.get_queryset()
kwargs['mixed_values'] = mixed_values
if not self.is_submitted:
kwargs['data'] = None
kwargs['files'] = None
return kwargs
def get_success_url(self):
return reverse('control:event.vouchers', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def form_valid(self, form):
log_entries = []
# Main form
form.save()
data = {
k: v
for k, v in form.cleaned_data.items()
if k in form.changed_data
}
data['_raw_bulk_data'] = self.request.POST.dict()
for obj in self.get_queryset():
log_entries.append(
obj.log_action('pretix.voucher.changed', data=data, user=self.request.user, save=False)
)
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['vouchers'] = self.get_queryset()
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
return ctx
@transaction.atomic
def post(self, request, *args, **kwargs):
form = self.get_form()
is_valid = (
self.is_submitted and
form.is_valid()
)
if is_valid:
return self.form_valid(form)
else:
if self.is_submitted:
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.form_invalid(form)
-5
View File
@@ -29,8 +29,3 @@ class PretixHelpersConfig(AppConfig):
def ready(self):
from .monkeypatching import monkeypatch_all_at_ready
monkeypatch_all_at_ready()
# Ensure reportlab does not make any calls to the internet or the local disk
from reportlab import rl_config
rl_config.trustedHosts = []
rl_config.trustedSchemes = ['data']
+6 -31
View File
@@ -29,61 +29,36 @@ class EnvOrParserConfig:
self.cp = configparser
def _envkey(self, section, option):
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f"PRETIX_{section}_{option}"
def _file_envkey(self, section, option):
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
return f"FILE__PRETIX_{section}_{option}"
section = re.sub('[^a-zA-Z0-9]', '_', section.upper())
option = re.sub('[^a-zA-Z0-9]', '_', option.upper())
return f'PRETIX_{section}_{option}'
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return f.read().strip()
if self._envkey(section, option) in os.environ:
return os.environ[self._envkey(section, option)]
return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback)
def getint(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return int(f.read().strip())
if self._envkey(section, option) in os.environ:
return int(os.environ[self._envkey(section, option)])
return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback)
def getfloat(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return float(f.read().strip())
if self._envkey(section, option) in os.environ:
return float(os.environ[self._envkey(section, option)])
return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback)
def getboolean(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
if self._file_envkey(section, option) in os.environ:
with open(os.environ[self._file_envkey(section, option)], "r") as f:
return self.cp._convert_to_boolean(f.read().strip())
if self._envkey(section, option) in os.environ:
return self.cp._convert_to_boolean(
os.environ[self._envkey(section, option)]
)
return self.cp.getboolean(
section, option, raw=raw, vars=vars, fallback=fallback
)
return self.cp._convert_to_boolean(os.environ[self._envkey(section, option)])
return self.cp.getboolean(section, option, raw=raw, vars=vars, fallback=fallback)
def has_section(self, section):
if any(k.startswith(self._file_envkey(section, "")) for k in os.environ):
return True
if any(k.startswith(self._envkey(section, "")) for k in os.environ):
if any(k.startswith(self._envkey(section, '')) for k in os.environ):
return True
return self.cp.has_section(section)
def has_option(self, section, option):
if self._file_envkey(section, option) in os.environ:
return True
if self._envkey(section, option) in os.environ:
return True
return self.cp.has_option(section, option)
-37
View File
@@ -19,9 +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 itertools
import re
from http.cookies import Morsel
from django.conf import settings
@@ -50,41 +48,6 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
response.cookies[key]['Partitioned'] = True
def thoroughly_delete_cookie(response, cookie_name, **kwargs):
""" Deletes different possible versions of a cookie (SameSite, Partitioned) """
properties = {"SameSite": ["", 'None'], "Partitioned": ["", True]}
for i, values in enumerate(itertools.product(*properties.values())):
m = Morsel()
m.set(cookie_name, '', '')
m.update(kwargs)
m.update(zip(properties.keys(), values))
m['expires'] = "Thu, 01 Jan 1970 00:00:00 GMT"
response.cookies[f'___DELETECOOKIE__{i}___{cookie_name}'] = m
# Make sure settings a cookie afterwards will add a new item in the dictionary, placing
# it below our deletion headers.
response.cookies.pop(cookie_name, None)
def get_all_values_of_cookie(cookie_header, cookie_name):
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
values = list()
if not cookie_header:
return values
for chunk in cookie_header.split(";"):
if "=" in chunk:
key, val = chunk.split("=", 1)
else:
# Assume an empty name per
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
key, val = "", chunk
key, val = key.strip(), val.strip()
if key == cookie_name:
values.append(val)
return values
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
# Copyright 2019 Google LLC.
# SPDX-License-Identifier: Apache-2.0
+1 -11
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import uuid
from django.core.signals import request_finished
from django.dispatch import receiver
@@ -47,13 +46,6 @@ class RequestIdFilter(logging.Filter):
return True
class SkipNotFoundFilter(logging.Filter):
# Drop the WARNING "Not Found: ..." records django.request emits for 404s
# We have different access logs for that
def filter(self, record):
return getattr(record, 'status_code', None) != 404
class RequestIdMiddleware:
def __init__(self, get_response):
self.get_response = get_response
@@ -66,9 +58,7 @@ class RequestIdMiddleware:
import sentry_sdk
sentry_sdk.set_tag("request_id", request.request_id)
else:
# Web server did not pass a request ID, we still generate one to correlate between django logs and
# celery logs
local.request_id = request.request_id = str(uuid.uuid4())
local.request_id = request.request_id = None
return self.get_response(request)
+1 -23
View File
@@ -27,7 +27,6 @@ from datetime import datetime
from http import cookies
from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
@@ -41,8 +40,6 @@ from urllib3.util.connection import (
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
from pretix.helpers.reportlab import ThumbnailingImageReader
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
@@ -151,14 +148,13 @@ def monkeypatch_urllib3_ssrf_protection():
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked")
if check_ip4 in _cgnat_net:
if ip_addr in _cgnat_net:
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
sock = None
@@ -234,27 +230,9 @@ def monkeypatch_cookie_morsel():
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
def monkeypatch_reportlab_imagereader():
from reportlab.lib import utils
old_init = utils.ImageReader.__init__
def new_init(self, fileName, ident=None): # noqa
if not isinstance(fileName, Image.Image) and not hasattr(fileName, 'read') and not hasattr(fileName, 'str'):
if not isinstance(self, ThumbnailingImageReader):
# ThumbnailingImageReader is only used by us explicitly and not by using <img> in html, so it is safe
raise SuspiciousFileOperation("reportlab should not be reading images from disk")
return types.MethodType(old_init, self)(
fileName, ident
)
utils.ImageReader.__init__ = new_init
def monkeypatch_all_at_ready():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
+1 -14
View File
@@ -25,19 +25,6 @@ import text_unidecode
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
EPC_QR_ALLOWED_CHARS = set(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789/-?:().,'+ "
)
def epc_qr_field(value):
return ''.join(
char for char in text_unidecode.unidecode(str(value or ''))
if char in EPC_QR_ALLOWED_CHARS
)
def dotdecimal(value):
return str(value).replace(",", ".")
@@ -90,7 +77,7 @@ def euro_epc_qr(
return {
"id": "girocode",
"label": "EPC-QR",
"qr_data": "\n".join(epc_qr_field(d) for d in [
"qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [
"BCD", # Service Tag: BCD
"002", # Version: V2
"2", # Character set: ISO 8859-1
-39
View File
@@ -20,19 +20,14 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts
@@ -75,20 +70,6 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
}))
def normalize_text(text: str) -> str:
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs):
if style is None:
@@ -106,8 +87,6 @@ class FontFallbackParagraph(Paragraph):
if not text:
return True
font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all(
ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text
@@ -123,24 +102,6 @@ class FontFallbackParagraph(Paragraph):
return family
class PlainTextParagraph(FontFallbackParagraph):
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
if not isinstance(text, str):
if hasattr(text, '__html__'):
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
text = str(text)
# Normalize unicode and apply reshaping
text = normalize_text(text)
# Escape any HTML in the text
text = escape(text)
if linebreaks:
text = text.strip().replace("\n", "<br />\n")
super().__init__(text, style, *args, **kwargs)
def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
+5 -10
View File
@@ -27,7 +27,6 @@ from django.conf import settings
from django.contrib.auth import login as auth_login
from django.contrib.gis import geoip2
from django.core.cache import cache
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
@@ -36,7 +35,7 @@ from geoip2.errors import AddressNotFoundError
from pretix.base.i18n import language
from pretix.base.services.mail import mail
from pretix.helpers.http import get_client_ip
from pretix.helpers.urls import mainreverse_absolute
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
@@ -170,17 +169,13 @@ def handle_login_source(user, request):
with language(user.locale):
mail(
user.email,
_('New sign-in to your account'),
_('Login from new source detected'),
'pretixcontrol/email/login_notice.txt',
{
'when': date_format(src.last_seen, 'DATETIME_FORMAT'),
'agent': src.agent_type,
'os': src.os_type,
# ua-parser returns "Other" for unidentified desktop devices.
'device': src.device_type if src.device_type and src.device_type != 'Other' else None,
'country': Country(str(country)).name if country else None,
'source': src,
'country': Country(str(country)).name if country else _('Unknown country'),
'instance': settings.PRETIX_INSTANCE_NAME,
'url': mainreverse_absolute('control:user.settings')
'url': build_absolute_uri('control:user.settings')
},
event=None,
user=user,
@@ -1,33 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from django import template
from django.utils.html import format_html
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter
def wrap_in(content, tag_name):
return format_html(f'<{tag_name}>{{}}</{tag_name}>', content)
-10
View File
@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import warnings
from urllib.parse import urljoin
from django.conf import settings
@@ -27,15 +26,6 @@ from django.urls import reverse
def build_absolute_uri(urlname, args=None, kwargs=None):
warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with ',
DeprecationWarning
)
return mainreverse_absolute(urlname, args, kwargs)
def mainreverse_absolute(urlname, args=None, kwargs=None):
from pretix.multidomain import maindomain_urlconf
return urljoin(settings.SITE_URL, reverse(urlname, args=args, kwargs=kwargs, urlconf=maindomain_urlconf))

Some files were not shown because too many files have changed in this diff Show More