mirror of
https://github.com/pretix/pretix.git
synced 2026-08-05 09:57:49 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9579504c1e |
+2
-1
@@ -10,7 +10,8 @@ tests:
|
|||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- 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:
|
except:
|
||||||
- '/^v.*$/'
|
- '/^v.*$/'
|
||||||
pypi:
|
pypi:
|
||||||
|
|||||||
@@ -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:
|
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
|
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::
|
as its first argument and can be used like this::
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Working with the code
|
|||||||
---------------------
|
---------------------
|
||||||
If you do not have a recent installation of ``nodejs``, install it now::
|
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
|
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::
|
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
||||||
|
|||||||
Generated
+91
-109
@@ -370,14 +370,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.2"
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -427,9 +427,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.133.0",
|
"version": "0.129.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
|
||||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
"integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -758,9 +758,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
"integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -775,9 +775,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
"integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -792,9 +792,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
|
||||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
"integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -809,9 +809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
|
||||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
"integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -826,9 +826,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
|
||||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
"integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -843,16 +843,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
"integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -863,16 +860,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
|
||||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
"integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -883,16 +877,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
"integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -903,16 +894,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
"integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -923,16 +911,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
|
||||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
"integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -943,16 +928,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
|
||||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
"integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -963,9 +945,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
|
||||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
"integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -980,9 +962,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
|
||||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
"integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -999,9 +981,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
|
||||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
"integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1016,9 +998,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
|
||||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
"integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3176,9 +3158,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3352,9 +3334,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3371,7 +3353,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -3628,14 +3610,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
|
||||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
"integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.133.0",
|
"@oxc-project/types": "=0.129.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -3644,27 +3626,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.3",
|
"@rolldown/binding-android-arm64": "1.0.0",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
"@rolldown/binding-darwin-arm64": "1.0.0",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
"@rolldown/binding-darwin-x64": "1.0.0",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
"@rolldown/binding-freebsd-x64": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
"@rolldown/binding-linux-arm64-musl": "1.0.0",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
"@rolldown/binding-linux-x64-gnu": "1.0.0",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
"@rolldown/binding-linux-x64-musl": "1.0.0",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
"@rolldown/binding-openharmony-arm64": "1.0.0",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
"@rolldown/binding-wasm32-wasi": "1.0.0",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
"@rolldown/binding-win32-x64-msvc": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
|
||||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
"integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -4343,9 +4325,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4483,17 +4465,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.16",
|
"version": "8.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
|
||||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
"integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.14",
|
||||||
"rolldown": "1.0.3",
|
"rolldown": "1.0.0",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
+9
-10
@@ -33,8 +33,8 @@ dependencies = [
|
|||||||
"bleach==6.4.*",
|
"bleach==6.4.*",
|
||||||
"celery==5.6.*",
|
"celery==5.6.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=49.0.0",
|
"cryptography>=48.0.1",
|
||||||
"css-inline==0.21.*",
|
"css-inline==0.20.*",
|
||||||
"defusedcsv>=3.0.0",
|
"defusedcsv>=3.0.0",
|
||||||
"dnspython==2.*",
|
"dnspython==2.*",
|
||||||
"Django[argon2]==5.2.*",
|
"Django[argon2]==5.2.*",
|
||||||
@@ -53,8 +53,7 @@ dependencies = [
|
|||||||
"django-oauth-toolkit==2.3.*",
|
"django-oauth-toolkit==2.3.*",
|
||||||
"django-otp==1.7.*",
|
"django-otp==1.7.*",
|
||||||
"django-phonenumber-field==8.4.*",
|
"django-phonenumber-field==8.4.*",
|
||||||
"django-querytagger==0.0.3",
|
"django-redis==6.0.*",
|
||||||
"django-redis==7.0.*",
|
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.7.*",
|
"django-statici18n==2.7.*",
|
||||||
"djangorestframework==3.17.*",
|
"djangorestframework==3.17.*",
|
||||||
@@ -77,7 +76,7 @@ dependencies = [
|
|||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.13.*",
|
"PyJWT==2.13.*",
|
||||||
"phonenumberslite==9.0.*",
|
"phonenumberslite==9.0.*",
|
||||||
"Pillow==12.3.*",
|
"Pillow==12.2.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==7.35.*",
|
"protobuf==7.35.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
@@ -92,9 +91,9 @@ dependencies = [
|
|||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==8.2",
|
"qrcode==8.2",
|
||||||
"redis==7.4.*",
|
"redis==7.4.*",
|
||||||
"reportlab==5.0.*",
|
"reportlab==4.5.*",
|
||||||
"requests==2.34.*",
|
"requests==2.32.*",
|
||||||
"sentry-sdk==2.64.*",
|
"sentry-sdk==2.62.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
@@ -102,7 +101,7 @@ dependencies = [
|
|||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
"ua-parser==1.0.*",
|
"ua-parser==1.0.*",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==3.0.*",
|
"webauthn==2.7.*",
|
||||||
"zeep==4.3.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -126,7 +125,7 @@ dev = [
|
|||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.8.*",
|
"pytest-xdist==3.8.*",
|
||||||
"pytest-playwright",
|
"pytest-playwright",
|
||||||
"pytest==9.1.*",
|
"pytest==9.0.*",
|
||||||
"playwright",
|
"playwright",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ localecompile:
|
|||||||
./manage.py compilemessages
|
./manage.py compilemessages
|
||||||
|
|
||||||
localegen:
|
localegen:
|
||||||
./manage.py makemessages --keep-pot --add-location file --ignore "pretix/static/npm_dir/*" $(LNGS)
|
./manage.py makemessages --keep-pot --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 -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
|
staticfiles: npminstall npmbuild jsi18n
|
||||||
./manage.py collectstatic --noinput
|
./manage.py collectstatic --noinput
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2026.7.0.dev0"
|
__version__ = "2026.6.0.dev0"
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ ALL_LANGUAGES = [
|
|||||||
('sv', _('Swedish')),
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('es-419', _('Spanish (Latin America)')),
|
('es-419', _('Spanish (Latin America)')),
|
||||||
('th', _('Thai')),
|
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ from pretix.base.settings import (
|
|||||||
LazyI18nStringList, validate_event_settings,
|
LazyI18nStringList, validate_event_settings,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import api_event_settings_fields
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_event_url(self, event):
|
def get_event_url(self, event):
|
||||||
return eventreverse_absolute(event, 'presale:event.index')
|
return build_absolute_uri(event, 'presale:event.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
@@ -747,7 +747,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'reservation_time',
|
'reservation_time',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'contact_url',
|
|
||||||
'show_variations_expanded',
|
'show_variations_expanded',
|
||||||
'hide_sold_out',
|
'hide_sold_out',
|
||||||
'meta_noindex',
|
'meta_noindex',
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ from pretix.base.settings import (
|
|||||||
)
|
)
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.helpers.countries import CachedCountries
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -757,7 +757,7 @@ class PaymentURLField(serializers.URLField):
|
|||||||
def to_representation(self, instance: OrderPayment):
|
def to_representation(self, instance: OrderPayment):
|
||||||
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||||
return None
|
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,
|
'order': instance.order.code,
|
||||||
'secret': instance.order.secret,
|
'secret': instance.order.secret,
|
||||||
'payment': instance.pk,
|
'payment': instance.pk,
|
||||||
@@ -806,7 +806,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class OrderURLField(serializers.URLField):
|
class OrderURLField(serializers.URLField):
|
||||||
def to_representation(self, instance: Order):
|
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,
|
'order': instance.code,
|
||||||
'secret': instance.secret,
|
'secret': instance.secret,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
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 import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
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_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
|
||||||
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
||||||
)
|
)
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class OrganizerSerializer(I18nAwareModelSerializer):
|
|||||||
slug = serializers.CharField(read_only=True)
|
slug = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def get_organizer_url(self, organizer):
|
def get_organizer_url(self, organizer):
|
||||||
return eventreverse_absolute(organizer, 'presale:organizer.index')
|
return build_absolute_uri(organizer, 'presale:organizer.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organizer
|
model = Organizer
|
||||||
@@ -492,16 +492,14 @@ class TeamInviteSerializer(serializers.ModelSerializer):
|
|||||||
def _send_invite(self, instance):
|
def _send_invite(self, instance):
|
||||||
mail(
|
mail(
|
||||||
instance.email,
|
instance.email,
|
||||||
gettext('You\'ve been invited to join %(organizer)s') % {
|
_('Account invitation'),
|
||||||
'organizer': self.context['organizer'].name,
|
|
||||||
},
|
|
||||||
'pretixcontrol/email/invitation.txt',
|
'pretixcontrol/email/invitation.txt',
|
||||||
{
|
{
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'user': self,
|
'user': self,
|
||||||
'organizer': self.context['organizer'].name,
|
'organizer': self.context['organizer'].name,
|
||||||
'team': instance.team.name,
|
'team': instance.team.name,
|
||||||
'url': mainreverse_absolute('control:auth.invite', kwargs={
|
'url': build_global_uri('control:auth.invite', kwargs={
|
||||||
'token': instance.token
|
'token': instance.token
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -576,7 +574,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'customer_accounts_require_login_for_order_access',
|
'customer_accounts_require_login_for_order_access',
|
||||||
'invoice_regenerate_allowed',
|
'invoice_regenerate_allowed',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'contact_url',
|
|
||||||
'imprint_url',
|
'imprint_url',
|
||||||
'organizer_info_text',
|
'organizer_info_text',
|
||||||
'event_list_type',
|
'event_list_type',
|
||||||
|
|||||||
@@ -872,7 +872,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'media_policy': e.media_policy,
|
'media_policy': e.media_policy,
|
||||||
'media_type': e.media_type,
|
'media_type': e.media_type,
|
||||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||||
'reason': e.code,
|
|
||||||
'reason_explanation': e.msg,
|
'reason_explanation': e.msg,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
except CheckInError as e:
|
except CheckInError as e:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
|
||||||
payload = {
|
payload = {
|
||||||
'iss': eventreverse_absolute(client.organizer, 'presale:organizer.index').rstrip('/'),
|
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'),
|
||||||
'aud': client.client_id,
|
'aud': client.client_id,
|
||||||
'exp': int(expires.timestamp()),
|
'exp': int(expires.timestamp()),
|
||||||
'iat': int(time.time()),
|
'iat': int(time.time()),
|
||||||
|
|||||||
@@ -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.models import Checkin, InvoiceAddress, Order, Question
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
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):
|
def get_answer(op, question_identifier=None):
|
||||||
@@ -545,7 +545,7 @@ def get_data_fields(event, for_model=None):
|
|||||||
_("Order link"),
|
_("Order link"),
|
||||||
Question.TYPE_STRING,
|
Question.TYPE_STRING,
|
||||||
None,
|
None,
|
||||||
lambda order: eventreverse_absolute(
|
lambda order: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order', kwargs={
|
'presale:event.order', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -560,7 +560,7 @@ def get_data_fields(event, for_model=None):
|
|||||||
_("Ticket link"),
|
_("Ticket link"),
|
||||||
Question.TYPE_STRING,
|
Question.TYPE_STRING,
|
||||||
None,
|
None,
|
||||||
lambda op: eventreverse_absolute(
|
lambda op: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': op.order.code,
|
'order': op.order.code,
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ logger = logging.getLogger('pretix.base.email')
|
|||||||
|
|
||||||
T = TypeVar("T", bound=EmailBackend)
|
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:
|
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -255,15 +253,12 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
|
|||||||
|
|
||||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||||
ip_addr = ipaddress.ip_address(sa[0])
|
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:
|
if ip_addr.is_multicast:
|
||||||
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
||||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||||
raise socket.error(f"Request to local address {sa[0]} blocked")
|
raise socket.error(f"Request to local address {sa[0]} blocked")
|
||||||
if ip_addr.is_private:
|
if ip_addr.is_private:
|
||||||
raise socket.error(f"Request to private address {sa[0]} blocked")
|
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
|
sock = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ from ...control.forms.filter import get_all_payment_providers
|
|||||||
from ...helpers import GroupConcat
|
from ...helpers import GroupConcat
|
||||||
from ...helpers.iter import chunked_iterable
|
from ...helpers.iter import chunked_iterable
|
||||||
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
|
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 (
|
from ..exporter import (
|
||||||
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||||
)
|
)
|
||||||
@@ -429,7 +429,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
row.append(
|
row.append(
|
||||||
eventreverse_absolute(order.event, 'presale:event.order', kwargs={
|
build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
})
|
})
|
||||||
@@ -855,7 +855,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]))
|
]))
|
||||||
|
|
||||||
row.append(
|
row.append(
|
||||||
eventreverse_absolute(order.event, 'presale:event.order.position', kwargs={
|
build_absolute_uri(order.event, 'presale:event.order.position', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': op.web_secret,
|
'secret': op.web_secret,
|
||||||
'position': op.positionid
|
'position': op.positionid
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ from django.db.models import QuerySet
|
|||||||
from django.forms import Select, widgets
|
from django.forms import Select, widgets
|
||||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import lazy
|
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
@@ -325,21 +324,16 @@ class WrappedPhonePrefixSelect(Select):
|
|||||||
initial = None
|
initial = None
|
||||||
|
|
||||||
def __init__(self, initial=None):
|
def __init__(self, initial=None):
|
||||||
def _get_choices():
|
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)()
|
|
||||||
|
|
||||||
|
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={
|
super().__init__(choices=choices, attrs={
|
||||||
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
|
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
|
||||||
'autocomplete': 'tel-country-code',
|
'autocomplete': 'tel-country-code',
|
||||||
@@ -1404,7 +1398,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
elif self.validate_vat_id and vat_id_applicable:
|
elif self.validate_vat_id and vat_id_applicable:
|
||||||
try:
|
try:
|
||||||
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
|
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
|
self.instance.vat_id = data['vat_id'] = normalized_id
|
||||||
except VATIDFinalError as e:
|
except VATIDFinalError as e:
|
||||||
if self.all_optional:
|
if self.all_optional:
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import unicodedata
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
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.signals import register_invoice_renderers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.reportlab import (
|
from pretix.helpers.reportlab import (
|
||||||
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
|
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
|
||||||
normalize_text, register_ttf_font_if_new, reshaper,
|
reshaper,
|
||||||
)
|
)
|
||||||
from pretix.presale.style import get_fonts
|
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']))
|
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||||
|
|
||||||
def _normalize(self, text):
|
def _normalize(self, text):
|
||||||
# alias kept for plugin compatibility
|
# reportlab does not support unicode combination characters
|
||||||
return normalize_text(text)
|
# 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):
|
def _upper(self, val):
|
||||||
# We uppercase labels, but not in every language
|
# We uppercase labels, but not in every language
|
||||||
@@ -339,15 +351,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||||
|
|
||||||
def _clean_text(self, text, tags=None):
|
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
|
return self._normalize(bleach.clean(
|
||||||
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
|
text,
|
||||||
# tags.
|
tags=set(tags) if tags else set()
|
||||||
return self._normalize(
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
bleach.clean(
|
|
||||||
text,
|
|
||||||
tags=set(tags) if tags else set()
|
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PaidMarker(Flowable):
|
class PaidMarker(Flowable):
|
||||||
@@ -398,7 +405,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
invoice_to_top = 52 * mm
|
invoice_to_top = 52 * mm
|
||||||
|
|
||||||
def _draw_invoice_to(self, canvas):
|
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.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||||
p_size = p.wrap(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)
|
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
|
invoice_from_top = 17 * mm
|
||||||
|
|
||||||
def _draw_invoice_from(self, canvas):
|
def _draw_invoice_from(self, canvas):
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
self.invoice.full_invoice_from,
|
self._clean_text(self.invoice.full_invoice_from),
|
||||||
style=self.stylesheet['InvoiceFrom']
|
style=self.stylesheet['InvoiceFrom']
|
||||||
)
|
)
|
||||||
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
||||||
@@ -540,12 +548,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_event(self, canvas):
|
def _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(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)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
|
|
||||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
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)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
@@ -563,7 +572,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
else:
|
else:
|
||||||
p_str = shorten(self.invoice.event.name)
|
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.wrapOn(canvas, self.event_width, self.event_height)
|
||||||
p_size = p.wrap(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])
|
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()
|
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
|
||||||
if type_info_text:
|
if type_info_text:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
type_info_text,
|
type_info_text,
|
||||||
self.stylesheet['WarningBlock']
|
self.stylesheet['WarningBlock']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.custom_field:
|
if self.invoice.custom_field:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
'{}: {}'.format(
|
'{}: {}'.format(
|
||||||
str(self.invoice.event.settings.invoice_address_custom_field),
|
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
|
||||||
self.invoice.custom_field,
|
self._clean_text(self.invoice.custom_field),
|
||||||
),
|
),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.internal_reference:
|
if self.invoice.internal_reference:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
|
||||||
reference=self.invoice.internal_reference,
|
reference=self._clean_text(self.invoice.internal_reference),
|
||||||
),
|
)),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_vat_id:
|
if self.invoice.invoice_to_vat_id:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
|
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
|
||||||
|
self._clean_text(self.invoice.invoice_to_vat_id),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_beneficiary:
|
if self.invoice.invoice_to_beneficiary:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
|
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
|
||||||
|
self._clean_text(self.invoice.invoice_to_beneficiary),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -696,11 +707,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
story = [
|
story = [
|
||||||
NextPageTemplate('FirstPage'),
|
NextPageTemplate('FirstPage'),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
(
|
self._normalize(
|
||||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||||
else pgettext('invoice', 'Invoice')
|
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']
|
self.stylesheet['Heading1']
|
||||||
),
|
),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
@@ -722,17 +733,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
]
|
]
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
else:
|
else:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
|
|
||||||
def _group_key(line):
|
def _group_key(line):
|
||||||
@@ -769,8 +780,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
max_height = self.stylesheet['Normal'].leading * 5
|
max_height = self.stylesheet['Normal'].leading * 5
|
||||||
p_style = self.stylesheet['Normal']
|
p_style = self.stylesheet['Normal']
|
||||||
for __ in range(1000):
|
for __ in range(1000):
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
curr_description,
|
self._clean_text(curr_description, tags=['br']),
|
||||||
p_style
|
p_style
|
||||||
)
|
)
|
||||||
h = p.wrap(max_width, doc.height)[1]
|
h = p.wrap(max_width, doc.height)[1]
|
||||||
@@ -851,7 +862,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
# Group together at the end of the invoice
|
# Group together at the end of the invoice
|
||||||
request_show_service_date = period_line
|
request_show_service_date = period_line
|
||||||
elif period_line:
|
elif period_line:
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
period_line,
|
period_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
@@ -863,7 +874,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
net_price=money_filter(net_value, self.invoice.event.currency),
|
net_price=money_filter(net_value, self.invoice.event.currency),
|
||||||
gross_price=money_filter(gross_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,
|
single_price_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
@@ -872,11 +883,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
description_p_list.pop(0),
|
description_p_list.pop(0),
|
||||||
str(len(lines)),
|
str(len(lines)),
|
||||||
localize(tax_rate) + " %",
|
localize(tax_rate) + " %",
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
@@ -893,14 +904,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
single_price_line = pgettext('invoice', 'Single price: {price}').format(
|
single_price_line = pgettext('invoice', 'Single price: {price}').format(
|
||||||
price=money_filter(gross_value, self.invoice.event.currency),
|
price=money_filter(gross_value, self.invoice.event.currency),
|
||||||
)
|
)
|
||||||
description_p_list.append(PlainTextParagraph(
|
description_p_list.append(FontFallbackParagraph(
|
||||||
single_price_line,
|
single_price_line,
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
tdata.append((
|
tdata.append((
|
||||||
description_p_list.pop(0),
|
description_p_list.pop(0),
|
||||||
str(len(lines)),
|
str(len(lines)),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||||
self.stylesheet['NormalRight']
|
self.stylesheet['NormalRight']
|
||||||
),
|
),
|
||||||
@@ -933,12 +944,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata.append([
|
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)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
tdata.append([
|
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)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -947,12 +958,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
pending_sum = self.invoice.order.pending_sum
|
pending_sum = self.invoice.order.pending_sum
|
||||||
if pending_sum != total:
|
if pending_sum != total:
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(pending_sum, self.invoice.event.currency)]
|
[money_filter(pending_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
@@ -969,12 +980,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
s=Sum('amount')
|
s=Sum('amount')
|
||||||
)['s'] or Decimal('0.00')
|
)['s'] or Decimal('0.00')
|
||||||
tdata.append(
|
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 ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
tdata.append(
|
tdata.append(
|
||||||
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
|
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
|
||||||
(['', '', ''] if has_taxes else ['']) +
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
||||||
)
|
)
|
||||||
@@ -997,14 +1008,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
story.append(Spacer(1, 10 * mm))
|
story.append(Spacer(1, 10 * mm))
|
||||||
|
|
||||||
if request_show_service_date:
|
if request_show_service_date:
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
|
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.payment_provider_text:
|
if self.invoice.payment_provider_text:
|
||||||
story.append(FontFallbackParagraph(
|
story.append(FontFallbackParagraph(
|
||||||
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
|
self._normalize(self.invoice.payment_provider_text),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1028,10 +1039,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||||
]
|
]
|
||||||
thead = [
|
thead = [
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||||
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||||
''
|
''
|
||||||
]
|
]
|
||||||
tdata = [thead]
|
tdata = [thead]
|
||||||
@@ -1042,7 +1053,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
continue
|
continue
|
||||||
tax = taxvalue_map[idx]
|
tax = taxvalue_map[idx]
|
||||||
tdata.append([
|
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 - tax, self.invoice.event.currency),
|
||||||
money_filter(gross, self.invoice.event.currency),
|
money_filter(gross, self.invoice.event.currency),
|
||||||
money_filter(tax, self.invoice.event.currency),
|
money_filter(tax, self.invoice.event.currency),
|
||||||
@@ -1061,7 +1072,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
table.setStyle(TableStyle(tstyledata))
|
table.setStyle(TableStyle(tstyledata))
|
||||||
story.append(Spacer(5 * mm, 5 * mm))
|
story.append(Spacer(5 * mm, 5 * mm))
|
||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||||
table
|
table
|
||||||
]))
|
]))
|
||||||
|
|
||||||
@@ -1078,7 +1089,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
net = gross - tax
|
net = gross - tax
|
||||||
|
|
||||||
tdata.append([
|
tdata.append([
|
||||||
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
|
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||||
fmt(net), fmt(gross), fmt(tax), ''
|
fmt(net), fmt(gross), fmt(tax), ''
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1087,13 +1098,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
Spacer(1, height=2 * mm),
|
Spacer(1, height=2 * mm),
|
||||||
PlainTextParagraph(
|
FontFallbackParagraph(
|
||||||
pgettext(
|
self._normalize(pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, this corresponds to:'
|
'{date}, this corresponds to:'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
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']
|
self.stylesheet['Fineprint']
|
||||||
),
|
),
|
||||||
Spacer(1, height=3 * mm),
|
Spacer(1, height=3 * mm),
|
||||||
@@ -1102,14 +1113,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||||
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
||||||
story.append(Spacer(1, 5 * mm))
|
story.append(Spacer(1, 5 * mm))
|
||||||
story.append(PlainTextParagraph(
|
story.append(FontFallbackParagraph(self._normalize(
|
||||||
pgettext(
|
pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, the invoice total corresponds to {total}.'
|
'{date}, the invoice total corresponds to {total}.'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||||
total=fmt(foreign_total)),
|
total=fmt(foreign_total))),
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1151,8 +1162,11 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
def _draw_invoice_from(self, canvas):
|
def _draw_invoice_from(self, canvas):
|
||||||
if not self.invoice.address_invoice_from:
|
if not self.invoice.address_invoice_from:
|
||||||
return
|
return
|
||||||
c = self.invoice.address_invoice_from.strip().split('\n')
|
c = [
|
||||||
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
|
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.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)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||||
super()._draw_invoice_from(canvas)
|
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)
|
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
||||||
]
|
]
|
||||||
|
|
||||||
p = PlainTextParagraph(
|
p = FontFallbackParagraph(
|
||||||
date_format(self.invoice.date, "DATE_FORMAT"),
|
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)
|
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)
|
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
||||||
@@ -1269,7 +1283,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
|
|||||||
i = []
|
i = []
|
||||||
|
|
||||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
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(
|
pgettext('invoice', 'Event date: {date_range}').format(
|
||||||
date_range=self.invoice.event.get_date_range_display(),
|
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.core.management.base import BaseCommand
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||||
from django_querytagger.tagging import with_tag
|
|
||||||
|
|
||||||
from pretix.helpers.periodic import SKIPPED
|
from pretix.helpers.periodic import SKIPPED
|
||||||
|
|
||||||
@@ -83,8 +82,7 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
# Check if the DB connection is still good, it might be closed if the previous task took too long.
|
# Check if the DB connection is still good, it might be closed if the previous task took too long.
|
||||||
close_old_connections()
|
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:
|
except Exception as err:
|
||||||
if isinstance(err, KeyboardInterrupt):
|
if isinstance(err, KeyboardInterrupt):
|
||||||
raise err
|
raise err
|
||||||
|
|||||||
@@ -282,12 +282,10 @@ def metric_values():
|
|||||||
|
|
||||||
# Throwaway metrics
|
# Throwaway metrics
|
||||||
exact_tables = [
|
exact_tables = [
|
||||||
Order, Invoice, Event, Organizer
|
Order, OrderPosition, Invoice, Event, Organizer
|
||||||
]
|
]
|
||||||
for m in apps.get_models(): # Count all models
|
for m in apps.get_models(): # Count all models
|
||||||
if issubclass(m, OrderPosition):
|
if any(issubclass(m, p) for p in exact_tables):
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.all.count()
|
|
||||||
elif any(issubclass(m, p) for p in exact_tables):
|
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
|
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = m.objects.count()
|
||||||
else:
|
else:
|
||||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
||||||
|
|||||||
+103
-85
@@ -19,8 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from urllib.parse import urlparse, urlsplit
|
from urllib.parse import urlparse, urlsplit
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
@@ -45,8 +43,6 @@ from pretix.multidomain.urlreverse import (
|
|||||||
)
|
)
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_supported = None
|
_supported = None
|
||||||
|
|
||||||
|
|
||||||
@@ -69,16 +65,48 @@ def get_supported_language(requested_language, allowed_languages, default_langua
|
|||||||
return language
|
return language
|
||||||
|
|
||||||
|
|
||||||
class LocaleMiddleware(MiddlewareMixin):
|
class BaseLocaleMiddleware(MiddlewareMixin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This middleware sets the correct locale and timezone
|
This is a reduced LocaleMiddleware that uses only information contained in the WSGI request data
|
||||||
for a request.
|
to figure out the language (cookie and browser settings). We need it to have a consistent language
|
||||||
|
for error pages that are generated from the middleware stack before we know e.g. which user is logged
|
||||||
|
in or which event is selected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_request(self, request: HttpRequest):
|
||||||
|
language = get_language_from_early_request(request)
|
||||||
|
translation.activate(language)
|
||||||
|
set_region(None)
|
||||||
|
request.LANGUAGE_CODE = language
|
||||||
|
timezone.deactivate()
|
||||||
|
|
||||||
|
def process_response(self, request: HttpRequest, response: HttpResponse):
|
||||||
|
language = translation.get_language()
|
||||||
|
patch_vary_headers(response, ('Accept-Language',))
|
||||||
|
if 'Content-Language' not in response:
|
||||||
|
response['Content-Language'] = language
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class LocaleMiddleware(MiddlewareMixin):
|
||||||
|
"""
|
||||||
|
This is the full LocaleMiddleware that uses all available information to figure out the correct
|
||||||
|
language for the request using all available sources, in this order of priority:
|
||||||
|
|
||||||
|
- Backend: User settings
|
||||||
|
- Language cookie
|
||||||
|
- Frontend: Customer account settings
|
||||||
|
- Browser settings
|
||||||
|
- Frontend: Event/Organizer settings
|
||||||
|
- System default
|
||||||
|
|
||||||
|
It needs to run late in the middleware stack to have all information available for these steps.
|
||||||
|
For some cases, it is even ran a second time since the event is sometimes only figured out after the
|
||||||
|
middleware stack (can happen for plugin views).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_request(self, request: HttpRequest):
|
def process_request(self, request: HttpRequest):
|
||||||
language = get_language_from_request(request)
|
language = get_language_from_request(request)
|
||||||
region = None
|
|
||||||
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it
|
# 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
|
# 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.
|
# set and can be taken into account for the decision.
|
||||||
@@ -99,16 +127,15 @@ class LocaleMiddleware(MiddlewareMixin):
|
|||||||
if '-' not in language and settings_holder.settings.region:
|
if '-' not in language and settings_holder.settings.region:
|
||||||
language += '-' + settings_holder.settings.region
|
language += '-' + settings_holder.settings.region
|
||||||
if settings_holder.settings.region:
|
if settings_holder.settings.region:
|
||||||
region = settings_holder.settings.region
|
set_region(settings_holder.settings.region)
|
||||||
else:
|
else:
|
||||||
gs = global_settings_object(request)
|
gs = global_settings_object(request)
|
||||||
if '-' not in language and gs.settings.region:
|
if '-' not in language and gs.settings.region:
|
||||||
language += '-' + gs.settings.region
|
language += '-' + gs.settings.region
|
||||||
if gs.settings.region:
|
if gs.settings.region:
|
||||||
region = gs.settings.region
|
set_region(gs.settings.region)
|
||||||
|
|
||||||
translation.activate(language)
|
translation.activate(language)
|
||||||
set_region(region)
|
|
||||||
request.LANGUAGE_CODE = get_language_without_region()
|
request.LANGUAGE_CODE = get_language_without_region()
|
||||||
|
|
||||||
tzname = None
|
tzname = None
|
||||||
@@ -188,6 +215,24 @@ def get_default_language():
|
|||||||
return settings.LANGUAGE_CODE
|
return settings.LANGUAGE_CODE
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_from_early_request(request: HttpRequest) -> str:
|
||||||
|
"""
|
||||||
|
Analyzes the request to find what language the user wants the system to
|
||||||
|
show using only WSGI-available information. Only languages listed in
|
||||||
|
settings.LANGUAGES are taken into account. If the user requests a sublanguage
|
||||||
|
where we have a main language, we send out the main language.
|
||||||
|
"""
|
||||||
|
global _supported
|
||||||
|
if _supported is None:
|
||||||
|
_supported = OrderedDict(settings.LANGUAGES)
|
||||||
|
|
||||||
|
return (
|
||||||
|
get_language_from_cookie(request)
|
||||||
|
or get_language_from_browser(request)
|
||||||
|
or get_default_language()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_language_from_request(request: HttpRequest) -> str:
|
def get_language_from_request(request: HttpRequest) -> str:
|
||||||
"""
|
"""
|
||||||
Analyzes the request to find what language the user wants the system to
|
Analyzes the request to find what language the user wants the system to
|
||||||
@@ -202,7 +247,6 @@ def get_language_from_request(request: HttpRequest) -> str:
|
|||||||
if request.path.startswith(get_script_prefix() + 'control'):
|
if request.path.startswith(get_script_prefix() + 'control'):
|
||||||
return (
|
return (
|
||||||
get_language_from_user_settings(request)
|
get_language_from_user_settings(request)
|
||||||
or get_language_from_customer_settings(request)
|
|
||||||
or get_language_from_cookie(request)
|
or get_language_from_cookie(request)
|
||||||
or get_language_from_browser(request)
|
or get_language_from_browser(request)
|
||||||
or get_language_from_event(request)
|
or get_language_from_event(request)
|
||||||
@@ -227,26 +271,7 @@ def _parse_csp(header):
|
|||||||
return h
|
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):
|
def _render_csp(h):
|
||||||
_sanitize_csp(h)
|
|
||||||
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
|
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
|
||||||
|
|
||||||
|
|
||||||
@@ -266,7 +291,21 @@ def _merge_csp(a, b):
|
|||||||
|
|
||||||
|
|
||||||
class SecurityMiddleware(MiddlewareMixin):
|
class SecurityMiddleware(MiddlewareMixin):
|
||||||
|
CSP_EXEMPT = (
|
||||||
|
'/api/v1/docs/',
|
||||||
|
)
|
||||||
|
|
||||||
def process_response(self, request, resp):
|
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:
|
if settings.DEBUG and resp.status_code >= 400:
|
||||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||||
# pages
|
# pages
|
||||||
@@ -277,15 +316,18 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
# https://github.com/pretix/pretix/issues/765
|
# https://github.com/pretix/pretix/issues/765
|
||||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||||
|
|
||||||
if not getattr(resp, '_csp_ignore', False):
|
img_src = []
|
||||||
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
|
gs = global_settings_object(request)
|
||||||
elif 'Content-Security-Policy' in resp:
|
if gs.settings.leaflet_tiles:
|
||||||
del resp['Content-Security-Policy']
|
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||||
|
|
||||||
return resp
|
font_src = set()
|
||||||
|
if hasattr(request, 'event'):
|
||||||
def _build_csp(self, request, resp):
|
for font in get_fonts(request.event, pdf_support_required=False).values():
|
||||||
url = resolve(request.path_info)
|
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 = {
|
h = {
|
||||||
'default-src': ["{static}"],
|
'default-src': ["{static}"],
|
||||||
@@ -294,8 +336,8 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
'frame-src': ['{static}'],
|
'frame-src': ['{static}'],
|
||||||
'style-src': ["{static}", "{media}"],
|
'style-src': ["{static}", "{media}"],
|
||||||
'connect-src': ["{dynamic}", "{media}"],
|
'connect-src': ["{dynamic}", "{media}"],
|
||||||
'img-src': ["{static}", "{media}", "data:"],
|
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||||
'font-src': ["{static}"],
|
'font-src': ["{static}"] + list(font_src),
|
||||||
'media-src': ["{static}", "data:"],
|
'media-src': ["{static}", "data:"],
|
||||||
# form-action is not only used to match on form actions, but also on URLs
|
# 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
|
# form-actions redirect to. In the context of e.g. payment providers or
|
||||||
@@ -304,13 +346,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
|
'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:
|
if settings.VITE_DEV_MODE:
|
||||||
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
||||||
h['style-src'] += ["'unsafe-inline'"]
|
h['style-src'] += ["'unsafe-inline'"]
|
||||||
@@ -322,7 +357,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
if not settings.VITE_DEV_MODE:
|
if not settings.VITE_DEV_MODE:
|
||||||
# can't have 'unsafe-inline' and nonce at the same time
|
# can't have 'unsafe-inline' and nonce at the same time
|
||||||
h['style-src'].append(nonce)
|
h['style-src'].append(nonce)
|
||||||
|
|
||||||
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
||||||
if (
|
if (
|
||||||
url.url_name == "event.order.pay.change" or
|
url.url_name == "event.order.pay.change" or
|
||||||
@@ -331,32 +365,27 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
h['script-src'].append('https://pay.google.com')
|
h['script-src'].append('https://pay.google.com')
|
||||||
h['frame-src'].append('https://pay.google.com')
|
h['frame-src'].append('https://pay.google.com')
|
||||||
h['connect-src'].append('https://google.com/pay')
|
h['connect-src'].append('https://google.com/pay')
|
||||||
|
|
||||||
if settings.LOG_CSP:
|
if settings.LOG_CSP:
|
||||||
h['report-uri'] = ["/csp_report/"]
|
h['report-uri'] = ["/csp_report/"]
|
||||||
|
|
||||||
if 'Content-Security-Policy' in resp:
|
if 'Content-Security-Policy' in resp:
|
||||||
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
||||||
|
|
||||||
if settings.CSP_ADDITIONAL_HEADER:
|
if settings.CSP_ADDITIONAL_HEADER:
|
||||||
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
|
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
|
||||||
|
|
||||||
placeholders = {
|
staticdomain = "'self'"
|
||||||
"{static}": ["'self'"],
|
dynamicdomain = "'self'"
|
||||||
"{dynamic}": ["'self'"],
|
mediadomain = "'self'"
|
||||||
"{media}": ["'self'"],
|
|
||||||
}
|
|
||||||
if settings.MEDIA_URL.startswith('http'):
|
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'):
|
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.startswith('http'):
|
||||||
if settings.SITE_URL.find('/', 9) > 0:
|
if settings.SITE_URL.find('/', 9) > 0:
|
||||||
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||||
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||||
else:
|
else:
|
||||||
placeholders["{static}"].append(settings.SITE_URL)
|
staticdomain += " " + settings.SITE_URL
|
||||||
placeholders["{dynamic}"].append(settings.SITE_URL)
|
dynamicdomain += " " + settings.SITE_URL
|
||||||
|
|
||||||
if hasattr(request, 'organizer') and request.organizer:
|
if hasattr(request, 'organizer') and request.organizer:
|
||||||
if hasattr(request, 'event') and request.event:
|
if hasattr(request, 'event') and request.event:
|
||||||
@@ -367,29 +396,18 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||||
placeholders["{dynamic}"].append(domain)
|
dynamicdomain += " " + domain
|
||||||
|
|
||||||
for k, v in h.items():
|
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
|
||||||
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
|
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
|
return resp
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class RejectInvalidInputMiddleware(MiddlewareMixin):
|
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),
|
|
||||||
]
|
|
||||||
-46
@@ -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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -57,7 +57,7 @@ from django_otp.models import Device
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
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.countries import FastCountryField
|
||||||
from ...helpers.u2f import pub_key_from_der, websafe_decode
|
from ...helpers.u2f import pub_key_from_der, websafe_decode
|
||||||
@@ -373,12 +373,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
|
|
||||||
mail(
|
mail(
|
||||||
email or self.email,
|
email or self.email,
|
||||||
_('Changes to your account'),
|
_('Account information changed'),
|
||||||
'pretixcontrol/email/security_notice.txt',
|
'pretixcontrol/email/security_notice.txt',
|
||||||
{
|
{
|
||||||
'user': self,
|
'user': self,
|
||||||
'messages': msg,
|
'messages': msg,
|
||||||
'url': mainreverse_absolute('control:user.settings'),
|
'url': build_absolute_uri('control:user.settings'),
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
},
|
},
|
||||||
event=None,
|
event=None,
|
||||||
@@ -400,13 +400,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
|
|
||||||
with language(self.locale):
|
with language(self.locale):
|
||||||
if reason == 'email_change':
|
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,
|
old_email=self.email, new_email=email,
|
||||||
))
|
))
|
||||||
elif reason == 'email_verify':
|
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,
|
email=self.email,
|
||||||
instance=settings.PRETIX_INSTANCE_NAME,
|
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid confirmation code reason')
|
raise Exception('Invalid confirmation code reason')
|
||||||
@@ -419,7 +418,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
}
|
}
|
||||||
mail(
|
mail(
|
||||||
email or self.email,
|
email or self.email,
|
||||||
_('Your confirmation code'),
|
_('pretix confirmation code'),
|
||||||
'pretixcontrol/email/confirmation_code.txt',
|
'pretixcontrol/email/confirmation_code.txt',
|
||||||
{
|
{
|
||||||
'user': self,
|
'user': self,
|
||||||
@@ -463,13 +462,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
|
|
||||||
mail(
|
mail(
|
||||||
self.email,
|
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
|
||||||
_('Reset your password'),
|
|
||||||
'pretixcontrol/email/forgot.txt',
|
|
||||||
{
|
{
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'user': self,
|
'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)))
|
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
||||||
},
|
},
|
||||||
None, locale=self.locale, user=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)
|
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)
|
Returns whether or not a user has an active staff session (formerly known as superuser session)
|
||||||
with the given session key.
|
with the given session key.
|
||||||
"""
|
"""
|
||||||
return self.get_active_staff_session(session_key) is not None
|
return self.get_active_staff_session(session_key) is not None
|
||||||
|
|
||||||
def get_active_staff_session(self, session_key):
|
def get_active_staff_session(self, session_key=None):
|
||||||
if not self.is_staff or not session_key:
|
if not self.is_staff:
|
||||||
return None
|
return None
|
||||||
if not hasattr(self, '_staff_session_cache'):
|
if not hasattr(self, '_staff_session_cache'):
|
||||||
self._staff_session_cache = {}
|
self._staff_session_cache = {}
|
||||||
if session_key not in self._staff_session_cache:
|
if session_key not in self._staff_session_cache:
|
||||||
sess = StaffSession.objects.filter(
|
qs = StaffSession.objects.filter(
|
||||||
user=self, date_end__isnull=True, session_key=session_key
|
user=self, date_end__isnull=True
|
||||||
).first()
|
)
|
||||||
|
if session_key:
|
||||||
|
qs = qs.filter(session_key=session_key)
|
||||||
|
sess = qs.first()
|
||||||
if sess:
|
if sess:
|
||||||
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
||||||
sess.date_end = now()
|
sess.date_end = now()
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class Customer(LoggedModel):
|
|||||||
|
|
||||||
def send_security_notice(self, message, email=None):
|
def send_security_notice(self, message, email=None):
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with language(self.locale):
|
with language(self.locale):
|
||||||
@@ -178,7 +178,7 @@ class Customer(LoggedModel):
|
|||||||
{
|
{
|
||||||
**self.get_email_context(),
|
**self.get_email_context(),
|
||||||
'message': str(message),
|
'message': str(message),
|
||||||
'url': eventreverse_absolute(self.organizer, 'presale:organizer.customer.index')
|
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
|
||||||
},
|
},
|
||||||
customer=self,
|
customer=self,
|
||||||
organizer=self.organizer,
|
organizer=self.organizer,
|
||||||
@@ -299,12 +299,12 @@ class Customer(LoggedModel):
|
|||||||
|
|
||||||
def send_activation_mail(self):
|
def send_activation_mail(self):
|
||||||
from pretix.base.services.mail import mail
|
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
|
from pretix.presale.forms.customer import TokenGenerator
|
||||||
|
|
||||||
ctx = self.get_email_context()
|
ctx = self.get_email_context()
|
||||||
token = TokenGenerator().make_token(self)
|
token = TokenGenerator().make_token(self)
|
||||||
ctx['url'] = eventreverse_absolute(
|
ctx['url'] = build_absolute_uri(
|
||||||
self.organizer,
|
self.organizer,
|
||||||
'presale:organizer.customer.activate'
|
'presale:organizer.customer.activate'
|
||||||
) + '?id=' + self.identifier + '&token=' + token
|
) + '?id=' + self.identifier + '&token=' + token
|
||||||
@@ -395,7 +395,7 @@ class AttendeeProfile(models.Model):
|
|||||||
self.company,
|
self.company,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(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:
|
for a in self.answers:
|
||||||
value = a.get('value')
|
value = a.get('value')
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import warnings
|
|||||||
from collections import Counter, OrderedDict, defaultdict
|
from collections import Counter, OrderedDict, defaultdict
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -80,16 +79,10 @@ from pretix.helpers.thumb import get_thumbnail
|
|||||||
from ..settings import settings_hierarkey
|
from ..settings import settings_hierarkey
|
||||||
from .organizer import Organizer, Team
|
from .organizer import Organizer, Team
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from hierarkey.proxy import HierarkeyProxy
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EventMixin:
|
class EventMixin:
|
||||||
if TYPE_CHECKING:
|
|
||||||
settings: HierarkeyProxy
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
|
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.')})
|
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
|
@property
|
||||||
def social_image(self):
|
def social_image(self):
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
img = None
|
img = None
|
||||||
logo_file = self.settings.get('logo_image', as_type=str, default='')[7:]
|
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}')
|
logger.exception(f'Failed to create thumbnail of {logo_file}')
|
||||||
img = default_storage.url(logo_file)
|
img = default_storage.url(logo_file)
|
||||||
if img:
|
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):
|
def _seats(self, ignore_voucher=None):
|
||||||
from .seating import Seat
|
from .seating import Seat
|
||||||
@@ -890,8 +883,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
|
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.
|
# 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.
|
# 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
|
# Calling set_active_plugins() results in defaults being created while actually data
|
||||||
@@ -906,7 +897,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
self.save()
|
self.save()
|
||||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||||
|
|
||||||
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)
|
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
|
||||||
|
|
||||||
if not self.all_sales_channels:
|
if not self.all_sales_channels:
|
||||||
@@ -920,15 +911,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
for emv in EventMetaValue.objects.filter(event=other):
|
for emv in EventMetaValue.objects.filter(event=other):
|
||||||
emv.pk = None
|
emv.pk = None
|
||||||
emv.event = self
|
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)
|
emv.save(force_insert=True)
|
||||||
|
|
||||||
for fl in EventFooterLink.objects.filter(event=other):
|
for fl in EventFooterLink.objects.filter(event=other):
|
||||||
@@ -982,13 +964,13 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if i.tax_rule_id:
|
if i.tax_rule_id:
|
||||||
i.tax_rule = tax_map[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.grant_membership_type = None
|
||||||
|
|
||||||
i.save() # no force_insert since i.picture.save could have already inserted
|
i.save() # no force_insert since i.picture.save could have already inserted
|
||||||
i.log_action('pretix.object.cloned')
|
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)
|
i.require_membership_types.set(require_membership_types)
|
||||||
|
|
||||||
if not i.all_sales_channels:
|
if not i.all_sales_channels:
|
||||||
@@ -1003,7 +985,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
v._prefetched_objects_cache = {}
|
v._prefetched_objects_cache = {}
|
||||||
v.save(force_insert=True)
|
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)
|
v.require_membership_types.set(require_membership_types)
|
||||||
if not v.all_sales_channels:
|
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]))
|
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():
|
for mp in self.organizer.meta_properties.all():
|
||||||
if mp.required and not self.meta_data.get(mp.name):
|
if mp.required and not self.meta_data.get(mp.name):
|
||||||
issues.append(format_html(
|
issues.append(
|
||||||
'<a href="{href}{href_hash}">{text}</a>',
|
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
|
||||||
text=gettext('You need to fill the meta parameter "{property}".').format(property=mp.name),
|
property=mp.name,
|
||||||
href=reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
a_attr='href="%s#id_prop-%d-value"' % (
|
||||||
href_hash=f'#id_prop-{mp.pk}-value',
|
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
||||||
))
|
mp.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
responses = event_live_issues.send(self)
|
responses = event_live_issues.send(self)
|
||||||
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
||||||
@@ -1884,8 +1869,6 @@ class EventMetaValue(LoggedModel):
|
|||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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)
|
super().save(*args, **kwargs)
|
||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
"Organizer",
|
"Organizer",
|
||||||
related_name="orders",
|
related_name="orders",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
Event,
|
Event,
|
||||||
@@ -327,7 +329,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
default="line",
|
default="line",
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
|
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Order")
|
verbose_name = _("Order")
|
||||||
@@ -352,60 +354,38 @@ class Order(LockModel, LoggedModel):
|
|||||||
def _transaction_key_reset(self):
|
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
|
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):
|
def gracefully_delete(self, user=None, auth=None):
|
||||||
|
from . import GiftCard, GiftCardTransaction, Membership, Voucher
|
||||||
|
|
||||||
if not self.testmode:
|
if not self.testmode:
|
||||||
raise TypeError("Only test mode orders can be deleted.")
|
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):
|
def email_confirm_secret(self):
|
||||||
return self.tagged_secret("email_confirm", 9)
|
return self.tagged_secret("email_confirm", 9)
|
||||||
@@ -1695,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
|||||||
self.company,
|
self.company,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(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]
|
lines = [r.strip() for r in lines if r]
|
||||||
return '\n'.join(lines).strip()
|
return '\n'.join(lines).strip()
|
||||||
@@ -2539,6 +2519,8 @@ class OrderPosition(AbstractPosition):
|
|||||||
"Organizer",
|
"Organizer",
|
||||||
related_name="order_positions",
|
related_name="order_positions",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
Order,
|
Order,
|
||||||
@@ -2595,7 +2577,7 @@ class OrderPosition(AbstractPosition):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
all = ScopedManager(organizer='organizer')
|
all = ScopedManager(organizer='order__event__organizer')
|
||||||
objects = ActivePositionManager()
|
objects = ActivePositionManager()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -3434,7 +3416,7 @@ class InvoiceAddress(models.Model):
|
|||||||
self.name,
|
self.name,
|
||||||
self.street,
|
self.street,
|
||||||
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
|
(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.vat_id,
|
||||||
self.custom_field,
|
self.custom_field,
|
||||||
self.internal_reference,
|
self.internal_reference,
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import operator
|
|||||||
import string
|
import string
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import pytz_deprecation_shim
|
import pytz_deprecation_shim
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -62,9 +61,6 @@ from ...helpers.permission_migration import (
|
|||||||
from ..settings import settings_hierarkey
|
from ..settings import settings_hierarkey
|
||||||
from .auth import User
|
from .auth import User
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from hierarkey.proxy import HierarkeyProxy
|
|
||||||
|
|
||||||
|
|
||||||
@settings_hierarkey.add(cache_namespace='organizer')
|
@settings_hierarkey.add(cache_namespace='organizer')
|
||||||
class Organizer(LoggedModel):
|
class Organizer(LoggedModel):
|
||||||
@@ -82,9 +78,6 @@ class Organizer(LoggedModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
settings_namespace = 'organizer'
|
settings_namespace = 'organizer'
|
||||||
if TYPE_CHECKING:
|
|
||||||
settings: HierarkeyProxy
|
|
||||||
|
|
||||||
name = models.CharField(max_length=200,
|
name = models.CharField(max_length=200,
|
||||||
verbose_name=_("Name"))
|
verbose_name=_("Name"))
|
||||||
slug = models.CharField(
|
slug = models.CharField(
|
||||||
|
|||||||
@@ -118,10 +118,7 @@ class SeatingPlan(LoggedModel):
|
|||||||
for zi, z in enumerate(self.layout_data['zones']):
|
for zi, z in enumerate(self.layout_data['zones']):
|
||||||
zpos = (z['position']['x'], z['position']['y'])
|
zpos = (z['position']['x'], z['position']['y'])
|
||||||
for ri, r in enumerate(z['rows']):
|
for ri, r in enumerate(z['rows']):
|
||||||
rpos = (
|
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y'])
|
||||||
zpos[0] + r.get('position', {}).get('x', 0),
|
|
||||||
zpos[1] + r.get('position', {}).get('y', 0),
|
|
||||||
)
|
|
||||||
row_label = None
|
row_label = None
|
||||||
if r.get('row_label'):
|
if r.get('row_label'):
|
||||||
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
|
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
|
||||||
@@ -150,8 +147,8 @@ class SeatingPlan(LoggedModel):
|
|||||||
zone=z['name'],
|
zone=z['name'],
|
||||||
category=s['category'],
|
category=s['category'],
|
||||||
sorting_rank=rank,
|
sorting_rank=rank,
|
||||||
x=rpos[0] + s.get('position', {}).get('x', 0),
|
x=rpos[0] + s['position']['x'],
|
||||||
y=rpos[1] + s.get('position', {}).get('y', 0),
|
y=rpos[1] + s['position']['y'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,8 @@
|
|||||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
import datetime
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from decimal import ROUND_HALF_UP, Decimal
|
from decimal import ROUND_HALF_UP, Decimal
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -423,33 +421,27 @@ class Voucher(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_affected_quotas(quota, item, variation, subevent):
|
def clean_quota_get_ignored(old_instance):
|
||||||
if quota:
|
quotas = set()
|
||||||
return {quota}
|
was_valid = old_instance and (
|
||||||
elif item and variation:
|
old_instance.valid_until is None or old_instance.valid_until >= now()
|
||||||
return set(variation.quotas.filter(subevent=subevent))
|
)
|
||||||
elif item and not item.has_variations:
|
if old_instance and old_instance.block_quota and was_valid:
|
||||||
return set(item.quotas.filter(subevent=subevent))
|
if old_instance.quota:
|
||||||
elif item and item.has_variations:
|
quotas.add(old_instance.quota)
|
||||||
return set(
|
elif old_instance.variation:
|
||||||
Quota.objects.filter(
|
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
||||||
pk__in=Quota.variations.through.objects.filter(
|
elif old_instance.item:
|
||||||
itemvariation__item=item,
|
if old_instance.item.has_variations:
|
||||||
quota__subevent=subevent,
|
quotas |= set(
|
||||||
).values('quota_id')
|
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
|
||||||
)
|
itemvariation__item=old_instance.item,
|
||||||
)
|
quota__subevent=old_instance.subevent,
|
||||||
else:
|
).values('quota_id'))
|
||||||
return set()
|
)
|
||||||
|
else:
|
||||||
@staticmethod
|
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
||||||
def clean_quota_get_ignored(voucher_data: Union["VoucherBulkData", "Voucher"]):
|
return quotas
|
||||||
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()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
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'):
|
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.'))
|
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 quota:
|
||||||
if not new_quotas:
|
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 '
|
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||||
'tickets.'))
|
'tickets.'))
|
||||||
|
|
||||||
@@ -638,16 +644,3 @@ class Voucher(LoggedModel):
|
|||||||
]
|
]
|
||||||
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
||||||
return ops
|
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
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
from pretix.base.models import Event, LogEntry
|
from pretix.base.models import Event, LogEntry
|
||||||
from pretix.base.signals import register_notification_types
|
from pretix.base.signals import register_notification_types
|
||||||
from pretix.base.templatetags.money import money_filter
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
_ALL_TYPES = None
|
_ALL_TYPES = None
|
||||||
@@ -170,7 +170,7 @@ class ParametrizedOrderNotificationType(NotificationType):
|
|||||||
def build_notification(self, logentry: LogEntry):
|
def build_notification(self, logentry: LogEntry):
|
||||||
order = logentry.content_object
|
order = logentry.content_object
|
||||||
|
|
||||||
order_url = mainreverse_absolute(
|
order_url = build_absolute_uri(
|
||||||
'control:event.order',
|
'control:event.order',
|
||||||
kwargs={
|
kwargs={
|
||||||
'organizer': logentry.event.organizer.slug,
|
'organizer': logentry.event.organizer.slug,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ from pretix.helpers import OF_SELF
|
|||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import format_map
|
||||||
from pretix.helpers.money import DecimalTextInput
|
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 import get_cart
|
||||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
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'):
|
if not self.settings.get('_hidden_seed'):
|
||||||
self.settings.set('_hidden_seed', get_random_string(64))
|
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(),
|
'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -834,7 +834,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
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
|
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.
|
the payment process. This is the place to actually move the money if applicable.
|
||||||
@@ -936,7 +936,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
Will be called if the *event administrator* views the details of a payment.
|
Will be called if the *event administrator* views the details of a payment.
|
||||||
|
|
||||||
It should return a SafeString containing HTML code, with information regarding the current payment
|
It should return HTML code containing information regarding the current payment
|
||||||
status and, if applicable, next steps.
|
status and, if applicable, next steps.
|
||||||
|
|
||||||
The default implementation returns an empty string.
|
The default implementation returns an empty string.
|
||||||
@@ -961,7 +961,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
Will be called if the *event administrator* views the details of a refund.
|
Will be called if the *event administrator* views the details of a refund.
|
||||||
|
|
||||||
It should return a SafeString containing HTML code, with information regarding the current refund
|
It should return HTML code containing information regarding the current refund
|
||||||
status and, if applicable, next steps.
|
status and, if applicable, next steps.
|
||||||
|
|
||||||
The default implementation returns an empty string.
|
The default implementation returns an empty string.
|
||||||
|
|||||||
+6
-16
@@ -77,7 +77,6 @@ from reportlab.platypus import Paragraph
|
|||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
|
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.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import layout_image_variables, layout_text_variables
|
from pretix.base.signals import layout_image_variables, layout_text_variables
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
@@ -373,11 +372,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": _("Atlantis"),
|
"editor_sample": _("Atlantis"),
|
||||||
"evaluate": lambda op, order, ev: str(getattr(order.invoice_address.country, 'name', '')) if getattr(order, 'invoice_address', None) else ''
|
"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", {
|
("addons", {
|
||||||
"label": _("List of Add-Ons"),
|
"label": _("List of Add-Ons"),
|
||||||
"editor_sample": _("Add-on 1\n2x Add-on 2"),
|
"editor_sample": _("Add-on 1\n2x Add-on 2"),
|
||||||
@@ -402,7 +396,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": _("Event organizer info text"),
|
"editor_sample": _("Event organizer info text"),
|
||||||
"evaluate": lambda op, order, ev: str(order.event.settings.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", {
|
("now_date", {
|
||||||
"label": _("Printing date"),
|
"label": _("Printing date"),
|
||||||
"editor_sample": _("2017-05-31"),
|
"editor_sample": _("2017-05-31"),
|
||||||
@@ -667,14 +665,6 @@ def get_images(event):
|
|||||||
def get_variables(event):
|
def get_variables(event):
|
||||||
v = copy.copy(DEFAULT_VARIABLES)
|
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]
|
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||||
|
|
||||||
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
|
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
|
||||||
@@ -1072,7 +1062,7 @@ class Renderer:
|
|||||||
except:
|
except:
|
||||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
|
||||||
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
|
p = Paragraph(text, style=style)
|
||||||
return p, ad, lineheight
|
return p, ad, lineheight
|
||||||
|
|
||||||
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
|
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Subquery
|
||||||
from django.utils.crypto import get_random_string
|
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")
|
confirmation_code = get_random_string(8, allowed_chars="01234567890")
|
||||||
mail(
|
mail(
|
||||||
user.email,
|
user.email,
|
||||||
subject=gettext('Confirm event cancellation and bulk refund'),
|
subject=gettext('Bulk-refund confirmation'),
|
||||||
template='pretixbase/email/cancel_confirm.txt',
|
template='pretixbase/email/cancel_confirm.txt',
|
||||||
context={
|
context={
|
||||||
"event": str(event),
|
"event": str(event),
|
||||||
"amount": money_filter(refund_total, event.currency),
|
"amount": money_filter(refund_total, event.currency),
|
||||||
"confirmation_code": confirmation_code,
|
"confirmation_code": confirmation_code,
|
||||||
"instance": settings.PRETIX_INSTANCE_NAME,
|
|
||||||
},
|
},
|
||||||
locale=user.locale,
|
locale=user.locale,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ from pretix.base.signals import (
|
|||||||
)
|
)
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers import OF_SELF, repeatable_reads_transaction
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -340,13 +340,12 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
|||||||
if schedule.owner.is_active:
|
if schedule.owner.is_active:
|
||||||
mail(
|
mail(
|
||||||
email=schedule.owner.email,
|
email=schedule.owner.email,
|
||||||
subject=gettext('Scheduled export failed'),
|
subject=gettext('Export failed'),
|
||||||
template='pretixbase/email/export_failed.txt',
|
template='pretixbase/email/export_failed.txt',
|
||||||
context={
|
context={
|
||||||
'configuration_url': config_url,
|
'configuration_url': config_url,
|
||||||
'reason': msg,
|
'reason': msg,
|
||||||
'soft': soft,
|
'soft': soft,
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
|
||||||
},
|
},
|
||||||
event=context if isinstance(context, Event) else None,
|
event=context if isinstance(context, Event) else None,
|
||||||
organizer=context.organizer if isinstance(context, Event) else context,
|
organizer=context.organizer if isinstance(context, Event) else context,
|
||||||
@@ -456,7 +455,7 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
|
|||||||
schedule,
|
schedule,
|
||||||
organizer,
|
organizer,
|
||||||
exporter,
|
exporter,
|
||||||
mainreverse_absolute(
|
build_absolute_uri(
|
||||||
'control:organizer.export',
|
'control:organizer.export',
|
||||||
kwargs={
|
kwargs={
|
||||||
'organizer': organizer.slug,
|
'organizer': organizer.slug,
|
||||||
@@ -482,7 +481,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
|
|||||||
schedule,
|
schedule,
|
||||||
event,
|
event,
|
||||||
exporter,
|
exporter,
|
||||||
mainreverse_absolute(
|
build_absolute_uri(
|
||||||
'control:event.orders.export',
|
'control:event.orders.export',
|
||||||
kwargs={
|
kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ from pretix.helpers.format import (
|
|||||||
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||||
)
|
)
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
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
|
from pretix.presale.ical import get_private_icals
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.mail')
|
logger = logging.getLogger('pretix.base.mail')
|
||||||
@@ -997,7 +997,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
|||||||
body_plain += _(
|
body_plain += _(
|
||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).replace("\n", "\r\n").format(
|
||||||
orderurl=eventreverse_absolute(
|
orderurl=build_absolute_uri(
|
||||||
order.event, 'presale:event.order.position', kwargs={
|
order.event, 'presale:event.order.position', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
@@ -1013,7 +1013,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
|||||||
body_plain += _(
|
body_plain += _(
|
||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).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.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ from typing import List
|
|||||||
from django.conf import settings as django_settings
|
from django.conf import settings as django_settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
from django.utils.timezone import now
|
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.i18n import language
|
||||||
from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
|
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
|
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||||
# shorter. We'll see what works better in reality…
|
# shorter. We'll see what works better in reality…
|
||||||
vouchers = []
|
vouchers = []
|
||||||
codes = set()
|
|
||||||
lock_seats = []
|
lock_seats = []
|
||||||
for i, record in enumerate(data):
|
for i, record in enumerate(data):
|
||||||
try:
|
try:
|
||||||
@@ -270,14 +268,6 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
|
|
||||||
if not record.get("code"):
|
if not record.get("code"):
|
||||||
raise ValidationError(_('A voucher cannot be created without a 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(
|
Voucher.clean_item_properties(
|
||||||
record,
|
record,
|
||||||
event,
|
event,
|
||||||
@@ -296,22 +286,8 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
lock_seats.append(voucher.seat)
|
lock_seats.append(voucher.seat)
|
||||||
except (ValidationError, ImportError) as e:
|
except (ValidationError, ImportError) as e:
|
||||||
raise DataImportError(
|
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():
|
with transaction.atomic():
|
||||||
# We don't support quotas here, so we only need to lock if seats are in use
|
# 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 = []
|
save_logentries = []
|
||||||
for v in vouchers:
|
for v in vouchers:
|
||||||
try:
|
v.save()
|
||||||
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.')
|
|
||||||
)
|
|
||||||
save_logentries.append(v.log_action(
|
save_logentries.append(v.log_action(
|
||||||
'pretix.voucher.added',
|
'pretix.voucher.added',
|
||||||
user=user,
|
user=user,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
|||||||
from pretix.base.signals import notification
|
from pretix.base.signals import notification
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.celery import get_task_priority
|
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)
|
@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,
|
'site_url': settings.SITE_URL,
|
||||||
'color': settings.PRETIX_PRIMARY_COLOR,
|
'color': settings.PRETIX_PRIMARY_COLOR,
|
||||||
'notification': notification,
|
'notification': notification,
|
||||||
'settings_url': mainreverse_absolute(
|
'settings_url': build_absolute_uri(
|
||||||
'control:user.settings.notifications',
|
'control:user.settings.notifications',
|
||||||
),
|
),
|
||||||
'disable_url': mainreverse_absolute(
|
'disable_url': build_absolute_uri(
|
||||||
'control:user.settings.notifications.off',
|
'control:user.settings.notifications.off',
|
||||||
kwargs={
|
kwargs={
|
||||||
'token': user.notifications_token,
|
'token': user.notifications_token,
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ def get_best_name(position_or_address, parts=False):
|
|||||||
|
|
||||||
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
||||||
def base_placeholders(sender, **kwargs):
|
def base_placeholders(sender, **kwargs):
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
def _event_sample(event):
|
def _event_sample(event):
|
||||||
if event.has_subevents:
|
if event.has_subevents:
|
||||||
@@ -388,14 +388,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
lambda event: LazyDate(now() + timedelta(days=15))
|
lambda event: LazyDate(now() + timedelta(days=15))
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
'hash': order.email_confirm_secret()
|
'hash': order.email_confirm_secret()
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -406,7 +406,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleButtonPlaceholder(
|
SimpleButtonPlaceholder(
|
||||||
'url_button', ['order', 'event'],
|
'url_button', ['order', 'event'],
|
||||||
url_func=lambda order, event: eventreverse_absolute(
|
url_func=lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -415,7 +415,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
text_func=lambda order, event: _("View order details"),
|
text_func=lambda order, event: _("View order details"),
|
||||||
sample_url_func=lambda event: eventreverse_absolute(
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -426,13 +426,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
sample_text_func=lambda event: _("View order details"),
|
sample_text_func=lambda event: _("View order details"),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.modify', kwargs={
|
'presale:event.order.modify', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.modify', kwargs={
|
'presale:event.order.modify', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -441,13 +441,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_products_change', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.change', kwargs={
|
'presale:event.order.change', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.change', kwargs={
|
'presale:event.order.change', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -456,13 +456,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_cancel', ['order', 'event'], lambda order, event: eventreverse_absolute(
|
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.cancel', kwargs={
|
'presale:event.order.cancel', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.cancel', kwargs={
|
'presale:event.order.cancel', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -471,7 +471,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['event', 'position'], lambda event, position: eventreverse_absolute(
|
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position',
|
'presale:event.order.position',
|
||||||
kwargs={
|
kwargs={
|
||||||
@@ -480,7 +480,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -491,7 +491,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleButtonPlaceholder(
|
SimpleButtonPlaceholder(
|
||||||
'url_button', ['event', 'position'],
|
'url_button', ['event', 'position'],
|
||||||
url_func=lambda event, position: eventreverse_absolute(
|
url_func=lambda event, position: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
@@ -500,7 +500,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
text_func=lambda event, position: _("View registration details"),
|
text_func=lambda event, position: _("View registration details"),
|
||||||
sample_url_func=lambda event: eventreverse_absolute(
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position', kwargs={
|
'presale:event.order.position', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -511,14 +511,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
sample_text_func=lambda event: _("View registration details"),
|
sample_text_func=lambda event: _("View registration details"),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
|
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.modify', kwargs={
|
'presale:event.order.position.modify', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.modify', kwargs={
|
'presale:event.order.position.modify', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -528,14 +528,14 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_products_change', ['position', 'event'], lambda position, event: eventreverse_absolute(
|
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.change', kwargs={
|
'presale:event.order.position.change', kwargs={
|
||||||
'order': position.order.code,
|
'order': position.order.code,
|
||||||
'secret': position.web_secret,
|
'secret': position.web_secret,
|
||||||
'position': position.positionid
|
'position': position.positionid
|
||||||
}
|
}
|
||||||
), lambda event: eventreverse_absolute(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.order.position.change', kwargs={
|
'presale:event.order.position.change', kwargs={
|
||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
@@ -581,20 +581,20 @@ def base_placeholders(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_remove', ['waiting_list_voucher', 'event'],
|
'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'
|
event, 'presale:event.waitinglist.remove'
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.waitinglist.remove',
|
'presale:event.waitinglist.remove',
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['waiting_list_voucher', 'event'],
|
'url', ['waiting_list_voucher', 'event'],
|
||||||
lambda waiting_list_voucher, event: eventreverse_absolute(
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
lambda event: eventreverse_absolute(
|
lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
'presale:event.redeem',
|
'presale:event.redeem',
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
@@ -611,7 +611,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||||
'* {} - {}'.format(
|
'* {} - {}'.format(
|
||||||
order.full_code,
|
order.full_code,
|
||||||
eventreverse_absolute(event, 'presale:event.order.open', kwargs={
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
@@ -623,7 +623,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
), lambda event: '\n' + '\n\n'.join(
|
), lambda event: '\n' + '\n\n'.join(
|
||||||
'* {} - {}'.format(
|
'* {} - {}'.format(
|
||||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
'{}-{}'.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,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
'order': order['code'],
|
'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>
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
'voucher_url_list', ['event', 'voucher_list'],
|
'voucher_url_list', ['event', 'voucher_list'],
|
||||||
lambda event, voucher_list: ' \n'.join([
|
lambda event, voucher_list: ' \n'.join([
|
||||||
eventreverse_absolute(
|
build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + c
|
) + '?voucher=' + c
|
||||||
for c in voucher_list
|
for c in voucher_list
|
||||||
]),
|
]),
|
||||||
lambda event: ' \n'.join([
|
lambda event: ' \n'.join([
|
||||||
eventreverse_absolute(
|
build_absolute_uri(
|
||||||
event, 'presale:event.redeem'
|
event, 'presale:event.redeem'
|
||||||
) + '?voucher=' + c
|
) + '?voucher=' + c
|
||||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||||
@@ -676,10 +676,10 @@ def base_placeholders(sender, **kwargs):
|
|||||||
inline=False,
|
inline=False,
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
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,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.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,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from django.conf import settings
|
|||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.timezone import now
|
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.i18n import language
|
||||||
from pretix.base.models import CachedFile, Event, User, cachedfile_name
|
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:
|
if user:
|
||||||
with language(user.locale):
|
with language(user.locale):
|
||||||
event_name = str(event.name)
|
|
||||||
mail(
|
mail(
|
||||||
user.email,
|
user.email,
|
||||||
gettext('Data shredding completed for %(event)s') % {'event': event_name},
|
_('Data shredding completed'),
|
||||||
'pretixbase/email/shred_completed.txt',
|
'pretixbase/email/shred_completed.txt',
|
||||||
{
|
{
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'user': user,
|
'user': user,
|
||||||
'organizer': event.organizer.name,
|
'organizer': event.organizer.name,
|
||||||
'event': event_name,
|
'event': str(event.name),
|
||||||
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
|
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
|
||||||
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
|
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from pretix.base.services.mail import mail
|
|||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
from pretix.base.signals import periodic_task
|
from pretix.base.signals import periodic_task
|
||||||
from pretix.celery_app import app
|
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)
|
@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')
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1276,7 +1276,7 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'write_permission': 'event.settings.invoicing:write',
|
'write_permission': 'event.settings.invoicing:write',
|
||||||
'form_kwargs': dict(
|
'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 "
|
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 "
|
"recommend to leave this option turned off and always issue a new invoice if a change needs "
|
||||||
"to be made."),
|
"to be made."),
|
||||||
@@ -1924,6 +1924,8 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Hide all past dates from calendar"),
|
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': {
|
'allow_modifications': {
|
||||||
@@ -2284,17 +2286,6 @@ DEFAULTS = {
|
|||||||
help_text=_("We'll show this publicly to allow attendees to contact you.")
|
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': {
|
'imprint_url': {
|
||||||
'default': None,
|
'default': None,
|
||||||
'type': str,
|
'type': str,
|
||||||
|
|||||||
@@ -535,9 +535,8 @@ EventPluginRegistry = PluginAwareRegistry # for backwards compatibility
|
|||||||
event_live_issues = EventPluginSignal()
|
event_live_issues = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to determine whether an event can be taken live. If you want to
|
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
|
prevent the event from going live, return a string that will be displayed to the user
|
||||||
as a SafeString containing HTML, or a string that will be HTML-escaped). If you don't,
|
as the error message. If you don't, your receiver should return ``None``.
|
||||||
your receiver should return ``None``.
|
|
||||||
|
|
||||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
|
|
||||||
{% block custom_header %}{% endblock %}
|
{% block custom_header %}{% endblock %}
|
||||||
{% if css_theme %}
|
{% if css_theme %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
||||||
@@ -21,5 +20,6 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
<script src="{% static "pretixbase/js/errors.js" %}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
{% load i18n %}
|
{% 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 "Event" %}: {{ event }}
|
||||||
- {% trans "Estimated refund" %}: **{{ amount }}**
|
|
||||||
|
|
||||||
{% 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 %}
|
**{{ confirmation_code }}**
|
||||||
|
|
||||||
{% blocktrans with instance=instance %}Thanks,
|
|
||||||
The {{ instance }} Team{% endblocktrans %}
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
{% load i18n %}
|
{% 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 }}
|
{{ configuration_url }}
|
||||||
|
|
||||||
{% blocktrans with instance=instance %}Thanks,
|
|
||||||
The {{ instance }} Team{% endblocktrans %}
|
|
||||||
|
|||||||
@@ -52,13 +52,13 @@
|
|||||||
<table cellpadding="20"><tr><td>
|
<table cellpadding="20"><tr><td>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div class="content">
|
<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 }}">
|
<a href="{{ settings_url }}">
|
||||||
{% trans "Manage settings" %}
|
{% trans "Click here to view and change your notification settings" %}
|
||||||
</a>
|
</a>
|
||||||
{% if disable_url %}<br>
|
{% if disable_url %}<br>
|
||||||
<a href="{{ disable_url }}">
|
<a href="{{ disable_url }}">
|
||||||
{% trans "Disable all notifications" %}
|
{% trans "Click here disable all notifications immediately." %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{{ notification.title }}{% if notification.detail %}
|
{{ 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 }}
|
{{ attr.title }}: {{ attr.value }}{% endfor %}{% for action in notification.actions %}
|
||||||
{% endfor %}{% endif %}{% for action in notification.actions %}
|
|
||||||
|
|
||||||
{{ action.label }}:
|
{{ action.label }}
|
||||||
|
{{ action.url }}{% endfor %}
|
||||||
|
|
||||||
{{ action.url }}{% endfor %}
|
{% 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:" %}
|
||||||
{% trans "You're receiving this email based on your notification settings." %}
|
{{ disable_url }}
|
||||||
|
|
||||||
- {% trans "Manage settings" %}: {{ settings_url }}
|
|
||||||
{% if disable_url %}- {% trans "Disable all notifications" %}: {{ disable_url }}
|
|
||||||
{% endif %}
|
{% 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 }}
|
Organizer: {{ organizer }}
|
||||||
- Event: {{ event }}
|
|
||||||
- Data selection: {{ shredders }}
|
|
||||||
- Start time: {{ start_time }}
|
|
||||||
|
|
||||||
Any data added to the event after the start time may not have been deleted.
|
Event: {{ event }}
|
||||||
|
|
||||||
Thanks,
|
Data selection: {{ shredders }}
|
||||||
The {{ instance }} Team
|
|
||||||
|
Start time: {{ start_time }} (new data added after this time might not have been deleted)
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
Your {{ instance }} team
|
||||||
{% endblocktrans %}
|
{% 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 i18n %}
|
||||||
{% load rich_text %}
|
{% load rich_text %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load wrap_in %}
|
|
||||||
{% block title %}{% trans "Redirect" %}{% endblock %}
|
{% block title %}{% trans "Redirect" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<i class="fa fa-link fa-fw big-icon"></i>
|
<i class="fa fa-link fa-fw big-icon"></i>
|
||||||
<div class="error-details">
|
<div class="error-details">
|
||||||
<h1>{% trans "Redirect" %}</h1>
|
<h1>{% trans "Redirect" %}</h1>
|
||||||
<h3>
|
<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 }}.
|
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.html import conditional_escape
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
@@ -45,7 +44,7 @@ def eventsignal(event: Event, signame: str, **kwargs):
|
|||||||
_html = []
|
_html = []
|
||||||
for receiver, response in signal.send(event, **kwargs):
|
for receiver, response in signal.send(event, **kwargs):
|
||||||
if response:
|
if response:
|
||||||
_html.append(conditional_escape(response))
|
_html.append(response)
|
||||||
return mark_safe("".join(_html))
|
return mark_safe("".join(_html))
|
||||||
|
|
||||||
|
|
||||||
@@ -64,5 +63,5 @@ def signal(signame: str, request, **kwargs):
|
|||||||
_html = []
|
_html = []
|
||||||
for receiver, response in signal.send(request, **kwargs):
|
for receiver, response in signal.send(request, **kwargs):
|
||||||
if response:
|
if response:
|
||||||
_html.append(conditional_escape(response))
|
_html.append(response)
|
||||||
return mark_safe("".join(_html))
|
return mark_safe("".join(_html))
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
|
|||||||
from bleach.linkifier import build_email_re
|
from bleach.linkifier import build_email_re
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core import signing
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
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 markdown.treeprocessors import UnescapeTreeprocessor
|
||||||
from tlds import tld_set
|
from tlds import tld_set
|
||||||
|
|
||||||
from pretix.base.views.redirect import safelink
|
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@@ -157,7 +158,8 @@ def safelink_callback(attrs, new=False):
|
|||||||
"""
|
"""
|
||||||
url = html.unescape(attrs.get((None, 'href'), '/'))
|
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:'):
|
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, 'target'] = '_blank'
|
||||||
attrs[None, 'rel'] = 'noopener'
|
attrs[None, 'rel'] = 'noopener'
|
||||||
return attrs
|
return attrs
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
@@ -27,8 +26,6 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_samesite_referer(request):
|
def _is_samesite_referer(request):
|
||||||
referer = request.headers.get('referer')
|
referer = request.headers.get('referer')
|
||||||
@@ -45,16 +42,11 @@ def _is_samesite_referer(request):
|
|||||||
|
|
||||||
|
|
||||||
def redir_view(request):
|
def redir_view(request):
|
||||||
framebreak = "framebreak" in request.GET
|
signer = signing.Signer(salt='safe-redirect')
|
||||||
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
|
|
||||||
try:
|
try:
|
||||||
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
|
url = signer.unsign(request.GET.get('url', ''))
|
||||||
except signing.BadSignature:
|
except signing.BadSignature:
|
||||||
try:
|
return HttpResponseBadRequest('Invalid parameter')
|
||||||
# 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')
|
|
||||||
|
|
||||||
if not _is_samesite_referer(request):
|
if not _is_samesite_referer(request):
|
||||||
u = urllib.parse.urlparse(url)
|
u = urllib.parse.urlparse(url)
|
||||||
@@ -63,26 +55,11 @@ def redir_view(request):
|
|||||||
'url': url,
|
'url': url,
|
||||||
})
|
})
|
||||||
|
|
||||||
if framebreak:
|
|
||||||
r = render(request, 'pretixbase/framebreak.html', {
|
|
||||||
'url': url,
|
|
||||||
})
|
|
||||||
r.xframe_options_exempt = True
|
|
||||||
return r
|
|
||||||
|
|
||||||
r = HttpResponseRedirect(url)
|
r = HttpResponseRedirect(url)
|
||||||
r['X-Robots-Tag'] = 'noindex'
|
r['X-Robots-Tag'] = 'noindex'
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def safelink(url, framebreak=False):
|
def safelink(url):
|
||||||
url = str(url)
|
signer = signing.Signer(salt='safe-redirect')
|
||||||
if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")):
|
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||||
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
|
|
||||||
|
|||||||
@@ -19,56 +19,14 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from celery import Celery, signals
|
from celery import Celery
|
||||||
from django.dispatch import receiver
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
app = Celery('pretix')
|
app = Celery('pretix')
|
||||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
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 "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 = []
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ from pretix.control.forms.widgets import Select2
|
|||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
|
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
|
||||||
from pretix.multidomain.urlreverse import (
|
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.plugins.banktransfer.payment import BankTransfer
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
@@ -219,7 +219,7 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||||
'Sample Conference Center\nHeidelberg, Germany'
|
'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
|
self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required
|
||||||
if self.has_subevents:
|
if self.has_subevents:
|
||||||
del self.fields['presale_start']
|
del self.fields['presale_start']
|
||||||
@@ -625,7 +625,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
|||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'reservation_time',
|
'reservation_time',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'contact_url',
|
|
||||||
'show_variations_expanded',
|
'show_variations_expanded',
|
||||||
'hide_sold_out',
|
'hide_sold_out',
|
||||||
'meta_noindex',
|
'meta_noindex',
|
||||||
@@ -672,11 +671,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
|||||||
|
|
||||||
base_context = {
|
base_context = {
|
||||||
'frontpage_text': ['event'],
|
'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=''):
|
def _resolve_virtual_keys_input(self, data, prefix=''):
|
||||||
@@ -1679,7 +1673,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
|
|||||||
|
|
||||||
class TaxRuleLineForm(I18nForm):
|
class TaxRuleLineForm(I18nForm):
|
||||||
country = LazyTypedChoiceField(
|
country = LazyTypedChoiceField(
|
||||||
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
|
choices=CountriesAndEUAndStates(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
address_type = forms.ChoiceField(
|
address_type = forms.ChoiceField(
|
||||||
|
|||||||
@@ -1342,13 +1342,7 @@ class QuestionAnswerFilterForm(forms.Form):
|
|||||||
opqs = opqs.filter(canceled=False)
|
opqs = opqs.filter(canceled=False)
|
||||||
if fdata.get("item", "") != "":
|
if fdata.get("item", "") != "":
|
||||||
i = fdata.get("item", "")
|
i = fdata.get("item", "")
|
||||||
if '-' in i:
|
opqs = opqs.filter(item_id__in=(i,))
|
||||||
opqs = opqs.filter(
|
|
||||||
item_id=i.split('-')[0],
|
|
||||||
variation_id=i.split('-')[1],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
opqs = opqs.filter(item_id=i)
|
|
||||||
|
|
||||||
return opqs
|
return opqs
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ from pretix.control.forms.event import (
|
|||||||
)
|
)
|
||||||
from pretix.control.forms.widgets import Select2, Select2Multiple
|
from pretix.control.forms.widgets import Select2, Select2Multiple
|
||||||
from pretix.multidomain.models import KnownDomain
|
from pretix.multidomain.models import KnownDomain
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
class OrganizerForm(I18nModelForm):
|
class OrganizerForm(I18nModelForm):
|
||||||
@@ -604,7 +604,6 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
'customer_accounts_require_login_for_order_access',
|
'customer_accounts_require_login_for_order_access',
|
||||||
'invoice_regenerate_allowed',
|
'invoice_regenerate_allowed',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'contact_url',
|
|
||||||
'imprint_url',
|
'imprint_url',
|
||||||
'organizer_info_text',
|
'organizer_info_text',
|
||||||
'event_list_type',
|
'event_list_type',
|
||||||
@@ -792,7 +791,7 @@ class MailSettingsForm(SettingsForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if 'url' in base_parameters:
|
if 'url' in base_parameters:
|
||||||
placeholders['url'] = eventreverse_absolute(
|
placeholders['url'] = build_absolute_uri(
|
||||||
self.organizer,
|
self.organizer,
|
||||||
'presale:organizer.customer.activate'
|
'presale:organizer.customer.activate'
|
||||||
) + '?token=' + get_random_string(30)
|
) + '?token=' + get_random_string(30)
|
||||||
|
|||||||
@@ -32,20 +32,16 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import copy
|
|
||||||
import csv
|
import csv
|
||||||
from collections import Counter, namedtuple
|
from collections import namedtuple
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import EmailValidator
|
from django.core.validators import EmailValidator
|
||||||
from django.db.models import Count, F, Max
|
|
||||||
from django.db.models.functions import Upper
|
from django.db.models.functions import Upper
|
||||||
from django.forms.utils import ErrorDict
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|
||||||
from django_scopes.forms import SafeModelChoiceField
|
from django_scopes.forms import SafeModelChoiceField
|
||||||
|
|
||||||
from pretix.base.email import get_available_placeholders
|
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.forms.widgets import format_placeholders_help_text
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Item, ItemVariation, Quota, SubEvent, Voucher
|
from pretix.base.models import Item, 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.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||||
from pretix.control.signals import voucher_form_validation
|
from pretix.control.signals import voucher_form_validation
|
||||||
@@ -112,22 +105,15 @@ class VoucherForm(I18nModelForm):
|
|||||||
except Item.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
super().__init__(*args, **kwargs)
|
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={
|
if instance.event.has_subevents:
|
||||||
'event': self.event.slug,
|
self.fields['subevent'].queryset = instance.event.subevents.all()
|
||||||
'organizer': self.event.organizer.slug,
|
|
||||||
})
|
|
||||||
|
|
||||||
if self.event.has_subevents:
|
|
||||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
|
||||||
self.fields['subevent'].widget = Select2(
|
self.fields['subevent'].widget = Select2(
|
||||||
attrs={
|
attrs={
|
||||||
'data-model-select2': 'event',
|
'data-model-select2': 'event',
|
||||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
'event': self.event.slug,
|
'event': instance.event.slug,
|
||||||
'organizer': self.event.organizer.slug,
|
'organizer': instance.event.organizer.slug,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -137,19 +123,18 @@ class VoucherForm(I18nModelForm):
|
|||||||
del self.fields['subevent']
|
del self.fields['subevent']
|
||||||
|
|
||||||
choices = []
|
choices = []
|
||||||
prefix = (self.prefix + '-') if self.prefix else ''
|
if 'itemvar' in initial or (self.data and 'itemvar' in self.data):
|
||||||
if 'itemvar' in initial or (self.data and prefix + 'itemvar' in self.data):
|
iv = self.data.get('itemvar') or initial.get('itemvar', '')
|
||||||
iv = self.data.get(prefix + 'itemvar', '') or initial.get('itemvar', '') or ''
|
|
||||||
if iv.startswith('q-'):
|
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)))
|
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
||||||
elif '-' in iv:
|
elif '-' in iv:
|
||||||
itemid, varid = iv.split('-')
|
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)
|
v = i.variations.get(pk=varid)
|
||||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||||
elif iv:
|
elif iv:
|
||||||
i = self.event.items.get(pk=iv)
|
i = self.instance.event.items.get(pk=iv)
|
||||||
if i.variations.exists():
|
if i.variations.exists():
|
||||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i)))
|
choices.append((str(i.pk), _('{product} – Any variation').format(product=i)))
|
||||||
else:
|
else:
|
||||||
@@ -160,8 +145,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
attrs={
|
attrs={
|
||||||
'data-model-select2': 'generic',
|
'data-model-select2': 'generic',
|
||||||
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
|
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
|
||||||
'event': self.event.slug,
|
'event': instance.event.slug,
|
||||||
'organizer': self.event.organizer.slug,
|
'organizer': instance.event.organizer.slug,
|
||||||
}),
|
}),
|
||||||
'data-placeholder': _('All products')
|
'data-placeholder': _('All products')
|
||||||
}
|
}
|
||||||
@@ -169,7 +154,7 @@ class VoucherForm(I18nModelForm):
|
|||||||
self.fields['itemvar'].required = False
|
self.fields['itemvar'].required = False
|
||||||
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
|
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(
|
self.fields['seat'] = forms.CharField(
|
||||||
label=_("Specific seat ID"),
|
label=_("Specific seat ID"),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
@@ -179,45 +164,40 @@ class VoucherForm(I18nModelForm):
|
|||||||
help_text=str(self.instance.seat) if self.instance.seat else '',
|
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):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
if not self._errors:
|
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:
|
if 'codes' in data:
|
||||||
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
||||||
@@ -229,7 +209,7 @@ class VoucherForm(I18nModelForm):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Voucher.clean_item_properties(
|
Voucher.clean_item_properties(
|
||||||
data, self.event,
|
data, self.instance.event,
|
||||||
self.instance.quota, self.instance.item, self.instance.variation,
|
self.instance.quota, self.instance.item, self.instance.variation,
|
||||||
seats_given=data.get('seat') or data.get('seats'),
|
seats_given=data.get('seat') or data.get('seats'),
|
||||||
block_quota=data.get('block_quota')
|
block_quota=data.get('block_quota')
|
||||||
@@ -249,7 +229,7 @@ class VoucherForm(I18nModelForm):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Voucher.clean_subevent(
|
Voucher.clean_subevent(
|
||||||
data, self.event
|
data, self.instance.event
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise ValidationError({"subevent": e.message})
|
raise ValidationError({"subevent": e.message})
|
||||||
@@ -265,19 +245,19 @@ class VoucherForm(I18nModelForm):
|
|||||||
if check_quota:
|
if check_quota:
|
||||||
Voucher.clean_quota_check(
|
Voucher.clean_quota_check(
|
||||||
data, cnt, self.initial_instance_data,
|
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 'seat' in self.fields:
|
||||||
if data.get('seat'):
|
if data.get('seat'):
|
||||||
self.instance.seat = Voucher.clean_seat_id(
|
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
|
self.instance.item = self.instance.seat.product
|
||||||
else:
|
else:
|
||||||
self.instance.seat = None
|
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
|
return data
|
||||||
|
|
||||||
@@ -285,215 +265,6 @@ class VoucherForm(I18nModelForm):
|
|||||||
return super().save(commit)
|
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):
|
class VoucherBulkForm(VoucherForm):
|
||||||
codes = forms.CharField(
|
codes = forms.CharField(
|
||||||
widget=forms.Textarea,
|
widget=forms.Textarea,
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ from urllib.parse import quote, urljoin, urlparse
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.http import Http404
|
||||||
from django.http import Http404, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404, resolve_url
|
from django.shortcuts import get_object_or_404, resolve_url
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import get_script_prefix, resolve, reverse
|
from django.urls import get_script_prefix, resolve, reverse
|
||||||
@@ -98,8 +97,6 @@ class PermissionMiddleware:
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def _login_redirect(self, request):
|
def _login_redirect(self, request):
|
||||||
from django.contrib.auth.views import redirect_to_login
|
|
||||||
|
|
||||||
# Taken from django/contrib/auth/decorators.py
|
# Taken from django/contrib/auth/decorators.py
|
||||||
path = request.build_absolute_uri()
|
path = request.build_absolute_uri()
|
||||||
# urlparse chokes on lazy objects in Python 3, force to str
|
# 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
|
if ((not login_scheme or login_scheme == current_scheme) and
|
||||||
(not login_netloc or login_netloc == current_netloc)):
|
(not login_netloc or login_netloc == current_netloc)):
|
||||||
path = request.get_full_path()
|
path = request.get_full_path()
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
|
|
||||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
return redirect_to_login(
|
||||||
# It's not useful to return a 302 redirect on a XMLHttpRequest request, because
|
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||||
# 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)
|
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
url = resolve(request.path_info)
|
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 request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
|
||||||
if getattr(request.user, "is_hijacked", False):
|
if getattr(request.user, "is_hijacked", False):
|
||||||
hijack_history = request.session.get('hijack_history', 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'))
|
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
|
||||||
if not ss:
|
if ss:
|
||||||
# Staff session expired or not found
|
ss.logs.create(
|
||||||
logout(request)
|
url=request.path,
|
||||||
return redirect_to_login(request.get_full_path())
|
method=request.method,
|
||||||
ss.logs.create(
|
impersonating=request.user
|
||||||
url=request.path,
|
)
|
||||||
method=request.method,
|
|
||||||
impersonating=request.user
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
ss = request.user.get_active_staff_session(request.session.session_key)
|
ss = request.user.get_active_staff_session(request.session.session_key)
|
||||||
if ss:
|
if ss:
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ from pretix.base.signals import (
|
|||||||
html_page_start = GlobalSignal()
|
html_page_start = GlobalSignal()
|
||||||
"""
|
"""
|
||||||
This signal allows you to put code in the beginning of the main page for every
|
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
|
page in the backend. You are expected to return HTML.
|
||||||
a string that will be HTML-escaped.
|
|
||||||
|
|
||||||
The ``sender`` keyword argument will contain the request.
|
The ``sender`` keyword argument will contain the request.
|
||||||
"""
|
"""
|
||||||
@@ -130,7 +129,7 @@ event_dashboard_top = EventPluginSignal()
|
|||||||
Arguments: 'request'
|
Arguments: 'request'
|
||||||
|
|
||||||
This signal is sent out to include custom HTML in the top part of the the event dashboard.
|
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.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
An additional keyword argument ``subevent`` *can* contain a sub-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.
|
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.
|
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.
|
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
|
This signal allows you to append HTML to a Quota's detail view. You receive the
|
||||||
quota as argument in the ``quota`` keyword argument.
|
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.
|
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
|
This signal allows you to append HTML to a SubEvent's detail view. You receive the
|
||||||
subevent as argument in the ``subevent`` keyword argument.
|
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.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
@@ -269,8 +265,7 @@ order_info = EventPluginSignal()
|
|||||||
"""
|
"""
|
||||||
Arguments: ``order``, ``request``
|
Arguments: ``order``, ``request``
|
||||||
|
|
||||||
This signal is sent out to display additional information on the order detail page.
|
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.
|
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
Additionally, the argument ``order`` and ``request`` are available.
|
Additionally, the argument ``order`` and ``request`` are available.
|
||||||
@@ -280,8 +275,7 @@ order_approve_info = EventPluginSignal()
|
|||||||
"""
|
"""
|
||||||
Arguments: ``order``, ``request``
|
Arguments: ``order``, ``request``
|
||||||
|
|
||||||
This signal is sent out to display additional information on the order approve page.
|
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.
|
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
Additionally, the argument ``order`` and ``request`` are available.
|
Additionally, the argument ``order`` and ``request`` are available.
|
||||||
@@ -292,7 +286,6 @@ order_position_buttons = EventPluginSignal()
|
|||||||
Arguments: ``order``, ``position``, ``request``
|
Arguments: ``order``, ``position``, ``request``
|
||||||
|
|
||||||
This signal is sent out to display additional buttons for a single position of an order.
|
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.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
Additionally, the argument ``order`` and ``request`` are available.
|
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
|
This signal is sent out to include template snippets on the settings page of an event
|
||||||
that allows generating a pretix Widget code.
|
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.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
A second keyword argument ``request`` will contain the request object.
|
A second keyword argument ``request`` will contain the request object.
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ from pretix import __version__
|
|||||||
from pretix.base.models import Order, OrderPayment, Transaction
|
from pretix.base.models import Order, OrderPayment, Transaction
|
||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.reportlab import PlainTextParagraph
|
|
||||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||||
from pretix.settings import DATA_DIR
|
from pretix.settings import DATA_DIR
|
||||||
|
|
||||||
@@ -80,23 +79,23 @@ class SysReport(ReportlabExportMixin):
|
|||||||
style_small.fontSize = 6
|
style_small.fontSize = 6
|
||||||
|
|
||||||
story = [
|
story = [
|
||||||
PlainTextParagraph("System report", headlinestyle),
|
Paragraph("System report", headlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Usage", subheadlinestyle),
|
Paragraph("Usage", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
self._usage_table(),
|
self._usage_table(),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Installed versions", subheadlinestyle),
|
Paragraph("Installed versions", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
self._tech_table(),
|
self._tech_table(),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Plugins", subheadlinestyle),
|
Paragraph("Plugins", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph(self._get_plugin_versions(), style_small),
|
Paragraph(self._get_plugin_versions(), style_small),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph("Custom templates", subheadlinestyle),
|
Paragraph("Custom templates", subheadlinestyle),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
PlainTextParagraph(self._get_custom_templates(), style_small),
|
Paragraph(self._get_custom_templates(), style_small),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -122,13 +121,13 @@ class SysReport(ReportlabExportMixin):
|
|||||||
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
||||||
]
|
]
|
||||||
tdata = [
|
tdata = [
|
||||||
[PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
||||||
[PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
|
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
|
||||||
[PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
|
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
|
||||||
[PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
|
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
|
||||||
[
|
[
|
||||||
PlainTextParagraph("Database engine:", style),
|
Paragraph("Database engine:", style),
|
||||||
PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
|
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
||||||
@@ -207,7 +206,7 @@ class SysReport(ReportlabExportMixin):
|
|||||||
year_last = now().year
|
year_last = now().year
|
||||||
tdata = [
|
tdata = [
|
||||||
[
|
[
|
||||||
PlainTextParagraph(l, style_small_head)
|
Paragraph(l, style_small_head)
|
||||||
for l in (
|
for l in (
|
||||||
"Time frame",
|
"Time frame",
|
||||||
"Currency",
|
"Currency",
|
||||||
@@ -258,19 +257,19 @@ class SysReport(ReportlabExportMixin):
|
|||||||
|
|
||||||
tdata.append(
|
tdata.append(
|
||||||
(
|
(
|
||||||
PlainTextParagraph(
|
Paragraph(
|
||||||
date_format(first_day, "M Y")
|
date_format(first_day, "M Y")
|
||||||
+ " – "
|
+ " – "
|
||||||
+ date_format(after_day - timedelta(days=1), "M Y"),
|
+ date_format(after_day - timedelta(days=1), "M Y"),
|
||||||
style_small,
|
style_small,
|
||||||
),
|
),
|
||||||
PlainTextParagraph(c, style_small),
|
Paragraph(c, style_small),
|
||||||
PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
|
Paragraph(str(orders_count), style_small) if i == 0 else "",
|
||||||
PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
||||||
PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
|
Paragraph(str(testmode_count), style_small) if i == 0 else "",
|
||||||
PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
||||||
PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
|
Paragraph(str(revenue_data.get("c") or 0), style_small),
|
||||||
PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -56,4 +56,5 @@
|
|||||||
</form>
|
</form>
|
||||||
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
|
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
|
||||||
<script type="text/plain" id="bad_origin_report_url">{{ bad_origin_report_url }}</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% load escapejson %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" action="" method="post" id="webauthn-form">
|
<form class="form-signin" action="" method="post" id="webauthn-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -31,7 +30,8 @@
|
|||||||
</form>
|
</form>
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<script type="text/json" id="webauthn-login">
|
<script type="text/json" id="webauthn-login">
|
||||||
{{ jsondata|escapejson }}
|
{{ jsondata|safe }}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
|
|||||||
@@ -66,7 +66,6 @@
|
|||||||
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
|
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
|
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/errors.js" %}"></script>
|
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{{ html_head|safe }}
|
{{ html_head|safe }}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}
|
{{ reason }}
|
||||||
|
|
||||||
{{ code }}
|
{{ 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,
|
Best regards,
|
||||||
The {{ instance }} Team
|
Your {{ instance }} team
|
||||||
{% endblocktrans %}
|
{% 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.
|
Your {{ instance }} team
|
||||||
|
{% endblocktrans %}
|
||||||
Thanks,
|
|
||||||
The {{ instance }} Team
|
|
||||||
{% endblocktrans %}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
{% 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 }}
|
{{ url }}
|
||||||
|
|
||||||
If you didn't request this, you can safely ignore this email — your password won't change.
|
Best regards,
|
||||||
|
Your {{ instance }} team
|
||||||
Thanks,
|
|
||||||
The {{ instance }} Team
|
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
|
{% 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 }}
|
Organizer: {{ organizer }}
|
||||||
- Team: {{ team }}
|
Team: {{ team }}
|
||||||
|
|
||||||
To accept, follow the link below:
|
|
||||||
|
|
||||||
|
If you want to join that team, just click on the following link:
|
||||||
{{ url }}
|
{{ 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,
|
Best regards,
|
||||||
The {{ instance }} Team
|
|
||||||
|
Your {{ instance }} team
|
||||||
{% endblocktrans %}
|
{% 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:
|
a login to your {{ instance }} account from an unusual or new location was detected. The login was performed using {{ agent }} on {{ os }} from {{ country }}.
|
||||||
{% 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.
|
|
||||||
|
|
||||||
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 }}
|
{{ url }}
|
||||||
|
|
||||||
Thanks,
|
Best regards,
|
||||||
The {{ instance }} Team
|
Your {{ instance }} team
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
|
{% 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 }}
|
{{ 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 }}
|
{{ url }}
|
||||||
|
|
||||||
Thanks,
|
Best regards,
|
||||||
The {{ instance }} Team
|
Your {{ instance }} team
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for issue in issues %}
|
{% for issue in issues %}
|
||||||
<li>{{ issue }}</li>
|
<li>{{ issue|safe }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for issue in issues %}
|
{% for issue in issues %}
|
||||||
<li>{{ issue }}</li>
|
<li>{{ issue|safe }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
{% bootstrap_field form.date_admission layout="control" %}
|
{% bootstrap_field form.date_admission layout="control" %}
|
||||||
{% bootstrap_field form.currency layout="control" %}
|
{% bootstrap_field form.currency layout="control" %}
|
||||||
{% bootstrap_field sform.contact_mail 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 sform.imprint_url layout="control" %}
|
||||||
{% bootstrap_field form.is_public layout="control" %}
|
{% bootstrap_field form.is_public layout="control" %}
|
||||||
{% bootstrap_field form.all_sales_channels layout="control" %}
|
{% bootstrap_field form.all_sales_channels layout="control" %}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load money %}
|
{% load money %}
|
||||||
{% load wrap_in %}
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans "Cancel order" %}
|
{% trans "Cancel order" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
{% if form.cancellation_fee %}
|
{% if form.cancellation_fee %}
|
||||||
{% if fee %}
|
{% if fee %}
|
||||||
{% with fee|money:request.event.currency as f %}
|
{% with fee|money:request.event.currency as f %}
|
||||||
<p>{% blocktrans trimmed with fee=f|wrap_in:"strong" %}
|
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
|
||||||
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
||||||
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
|
|||||||
@@ -903,7 +903,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="1"></td>
|
<td colspan="1"></td>
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
{{ p.html_info }}
|
{{ p.html_info|safe }}
|
||||||
{% if staff_session %}
|
{% if staff_session %}
|
||||||
<p>
|
<p>
|
||||||
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
|
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
|
||||||
@@ -1018,7 +1018,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if r.html_info %}
|
{% if r.html_info %}
|
||||||
{{ r.html_info }}
|
{{ r.html_info|safe }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if staff_session %}
|
{% if staff_session %}
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load escapejson %}
|
|
||||||
{% block inner %}
|
{% block inner %}
|
||||||
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
|
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
|
||||||
</li>
|
</li>
|
||||||
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
||||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script><br>
|
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
|
||||||
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
||||||
<br>
|
<br>
|
||||||
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
|
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field sform.imprint_url layout="control" %}
|
{% bootstrap_field sform.imprint_url layout="control" %}
|
||||||
{% bootstrap_field sform.contact_mail 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.organizer_info_text layout="control" %}
|
||||||
{% bootstrap_field sform.event_team_provisioning layout="control" %}
|
{% bootstrap_field sform.event_team_provisioning layout="control" %}
|
||||||
{% if sform.allowed_restricted_plugins %}
|
{% if sform.allowed_restricted_plugins %}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{{ layout|json_script:"editor-data" }}
|
<script type="application/json" id="editor-data">
|
||||||
|
{{ layout|safe }}
|
||||||
|
</script>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="panel panel-default panel-pdf-editor">
|
<div class="panel panel-default panel-pdf-editor">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{% extends "pretixcontrol/base.html" %}
|
{% extends "pretixcontrol/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load escapejson %}
|
|
||||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
||||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script>
|
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
|
||||||
<p>
|
<p>
|
||||||
<a data-toggle="collapse" href="#no_scan">
|
<a data-toggle="collapse" href="#no_scan">
|
||||||
{% trans "Can't scan the barcode?" %}
|
{% trans "Can't scan the barcode?" %}
|
||||||
@@ -82,4 +81,9 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
<script type="text/json" id="qrdata">
|
||||||
|
{{ qrdata|safe }}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% load escapejson %}
|
|
||||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||||
@@ -27,7 +26,9 @@
|
|||||||
{% trans "Device registration failed." %}
|
{% trans "Device registration failed." %}
|
||||||
</div>
|
</div>
|
||||||
<script type="text/json" id="webauthn-enroll">
|
<script type="text/json" id="webauthn-enroll">
|
||||||
{{ jsondata|escapejson }}
|
{{ jsondata|safe }}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load escapejson %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" id="webauthn-form" action="" method="post">
|
<form class="form-signin" id="webauthn-form" action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
|
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<script type="text/json" id="webauthn-login">
|
<script type="text/json" id="webauthn-login">
|
||||||
{{ jsondata|escapejson }}
|
{{ jsondata|safe }}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
|
|||||||
@@ -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>
|
</p>
|
||||||
<form action="{% url "control:event.vouchers.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
<form action="{% url "control:event.vouchers.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in filter_form %}
|
|
||||||
{{ field.as_hidden }}
|
|
||||||
{% endfor %}
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-quotas">
|
<table class="table table-hover table-quotas">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -147,18 +144,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for v in vouchers %}
|
{% for v in vouchers %}
|
||||||
@@ -226,10 +211,6 @@
|
|||||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
{% trans "Delete selected" %}
|
{% trans "Delete selected" %}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -49,11 +49,11 @@
|
|||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
{% if t.tag %}
|
{% 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 }}
|
{{ t.tag }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% 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" %}
|
{% trans "Empty tag" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -370,7 +370,6 @@ urlpatterns = [
|
|||||||
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
|
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/$', 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/$', 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/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/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'),
|
||||||
re_path(r'^vouchers/(?P<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'),
|
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$', 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_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_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/$', 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'^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(),
|
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlparse, urlsplit
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
@@ -64,6 +64,7 @@ from django.shortcuts import get_object_or_404, redirect
|
|||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import conditional_escape, format_html
|
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.safestring import mark_safe
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
|
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.mailsetup import MailSettingsSetupView
|
||||||
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
||||||
from pretix.helpers.database import rolledback_transaction
|
from pretix.helpers.database import rolledback_transaction
|
||||||
from pretix.multidomain.urlreverse import (
|
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
|
||||||
eventreverse_absolute, get_event_domain,
|
|
||||||
)
|
|
||||||
from pretix.presale.views.widget import (
|
from pretix.presale.views.widget import (
|
||||||
version_default as widget_version_default,
|
version_default as widget_version_default,
|
||||||
)
|
)
|
||||||
@@ -1149,11 +1148,8 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
|||||||
if request.POST.get("delete") == "yes":
|
if request.POST.get("delete") == "yes":
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
Order.gracefully_delete_bulk(
|
for order in request.event.orders.filter(testmode=True):
|
||||||
request.event,
|
order.gracefully_delete(user=self.request.user)
|
||||||
request.event.orders.filter(testmode=True),
|
|
||||||
user=self.request.user
|
|
||||||
)
|
|
||||||
except ProtectedError:
|
except ProtectedError:
|
||||||
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
|
messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
|
||||||
'created by plug-ins) do not allow it.'))
|
'created by plug-ins) do not allow it.'))
|
||||||
@@ -1428,16 +1424,11 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
|
|||||||
form.instance.custom_rules = json.dumps([
|
form.instance.custom_rules = json.dumps([
|
||||||
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
||||||
], cls=I18nJSONEncoder)
|
], cls=I18nJSONEncoder)
|
||||||
if form.has_changed() or self.formset.has_changed():
|
if form.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
|
|
||||||
]
|
|
||||||
self.object.log_action(
|
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)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@@ -1743,10 +1734,10 @@ class EventQRCode(EventPermissionRequiredMixin, View):
|
|||||||
permission = None
|
permission = None
|
||||||
|
|
||||||
def get(self, request, *args, filetype, **kwargs):
|
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 "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"]
|
url = request.GET["url"]
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied("Untrusted URL")
|
raise PermissionDenied("Untrusted URL")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from django.core.mail import get_connection
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from pretix.base import email
|
from pretix.base import email
|
||||||
@@ -216,14 +216,13 @@ class MailSettingsSetupView(TemplateView):
|
|||||||
messages.error(request, _('The verification code was incorrect, please try again.'))
|
messages.error(request, _('The verification code was incorrect, please try again.'))
|
||||||
else:
|
else:
|
||||||
self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890')
|
self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890')
|
||||||
sender_address = self.simple_form.cleaned_data.get('mail_from')
|
|
||||||
mail(
|
mail(
|
||||||
sender_address,
|
self.simple_form.cleaned_data.get('mail_from'),
|
||||||
gettext('Confirm %(address)s as a sender address') % {'address': sender_address},
|
_('Sender address verification'),
|
||||||
'pretixcontrol/email/email_setup.txt',
|
'pretixcontrol/email/email_setup.txt',
|
||||||
{
|
{
|
||||||
'code': self.request.session[session_key],
|
'code': self.request.session[session_key],
|
||||||
'address': sender_address,
|
'address': self.simple_form.cleaned_data.get('mail_from'),
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ from pretix.helpers import OF_SELF
|
|||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
from pretix.helpers.hierarkey import clean_filename
|
||||||
from pretix.helpers.iter import chunked_iterable
|
|
||||||
from pretix.helpers.json import CustomJSONEncoder
|
from pretix.helpers.json import CustomJSONEncoder
|
||||||
from pretix.helpers.safedownload import check_token
|
from pretix.helpers.safedownload import check_token
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
@@ -241,7 +240,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
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()
|
total = qs.count()
|
||||||
orders_with_successful_action = 0
|
orders_with_successful_action = 0
|
||||||
for i, o in enumerate(qs):
|
for i, o in enumerate(qs):
|
||||||
@@ -395,21 +394,9 @@ class OrderDeleteBulkActionView(BaseOrderBulkActionView):
|
|||||||
testmode=True,
|
testmode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
def execute_single(self, instance, form: forms.Form):
|
||||||
qs = self.allowed_for(self.get_queryset())
|
instance.gracefully_delete(user=self.request.user)
|
||||||
total = qs.count()
|
return True
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
@@ -551,10 +538,10 @@ class OrderDetail(OrderView):
|
|||||||
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
||||||
for p in ctx['payments']:
|
for p in ctx['payments']:
|
||||||
if p.payment_provider:
|
if p.payment_provider:
|
||||||
p.html_info = p.payment_provider.payment_control_render(self.request, p) or ""
|
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
|
||||||
for r in ctx['refunds']:
|
for r in ctx['refunds']:
|
||||||
if r.payment_provider:
|
if r.payment_provider:
|
||||||
r.html_info = r.payment_provider.refund_control_render(self.request, r) or ""
|
r.html_info = (r.payment_provider.refund_control_render(self.request, r) or "").strip()
|
||||||
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
||||||
ctx['comment_form'] = CommentForm(initial={
|
ctx['comment_form'] = CommentForm(initial={
|
||||||
'comment': self.order.comment,
|
'comment': self.order.comment,
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ from pretix.helpers import OF_SELF, GroupConcat
|
|||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
from pretix.helpers.urls import mainreverse_absolute
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.forms.customer import TokenGenerator
|
from pretix.presale.forms.customer import TokenGenerator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1032,16 +1032,14 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
|||||||
def _send_invite(self, instance):
|
def _send_invite(self, instance):
|
||||||
mail(
|
mail(
|
||||||
instance.email,
|
instance.email,
|
||||||
gettext('You\'ve been invited to join %(organizer)s') % {
|
_('Account invitation'),
|
||||||
'organizer': self.request.organizer.name,
|
|
||||||
},
|
|
||||||
'pretixcontrol/email/invitation.txt',
|
'pretixcontrol/email/invitation.txt',
|
||||||
{
|
{
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||||
'user': self,
|
'user': self,
|
||||||
'organizer': self.request.organizer.name,
|
'organizer': self.request.organizer.name,
|
||||||
'team': instance.team.name,
|
'team': instance.team.name,
|
||||||
'url': mainreverse_absolute('control:auth.invite', kwargs={
|
'url': build_global_uri('control:auth.invite', kwargs={
|
||||||
'token': instance.token
|
'token': instance.token
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -2853,12 +2851,10 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['redirect_uri'] = eventreverse_absolute(
|
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return',
|
||||||
self.request.organizer, 'presale:organizer.customer.login.return',
|
kwargs={
|
||||||
kwargs={
|
'provider': self.object.pk
|
||||||
'provider': self.object.pk
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
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)
|
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
|
||||||
ctx = self.customer.get_email_context()
|
ctx = self.customer.get_email_context()
|
||||||
token = TokenGenerator().make_token(self.customer)
|
token = TokenGenerator().make_token(self.customer)
|
||||||
ctx['url'] = eventreverse_absolute(
|
ctx['url'] = build_absolute_uri(
|
||||||
self.request.organizer,
|
self.request.organizer,
|
||||||
'presale:organizer.customer.recoverpw'
|
'presale:organizer.customer.recoverpw'
|
||||||
) + '?id=' + self.customer.identifier + '&token=' + token
|
) + '?id=' + self.customer.identifier + '&token=' + token
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
ctx['pdf'] = self.get_current_background()
|
ctx['pdf'] = self.get_current_background()
|
||||||
ctx['variables'] = self.get_variables()
|
ctx['variables'] = self.get_variables()
|
||||||
ctx['images'] = self.get_images()
|
ctx['images'] = self.get_images()
|
||||||
ctx['layout'] = self.get_current_layout()
|
ctx['layout'] = json.dumps(self.get_current_layout())
|
||||||
ctx['title'] = self.title
|
ctx['title'] = self.title
|
||||||
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
||||||
ctx['maxfilesize'] = self.maxfilesize
|
ctx['maxfilesize'] = self.maxfilesize
|
||||||
|
|||||||
@@ -537,7 +537,6 @@ class SubEventDetail(EventPermissionRequiredMixin, DetailView):
|
|||||||
pcnt=Subquery(
|
pcnt=Subquery(
|
||||||
OrderPosition.objects.filter(
|
OrderPosition.objects.filter(
|
||||||
subevent=self.object,
|
subevent=self.object,
|
||||||
order_id=OuterRef("id"),
|
|
||||||
).values("subevent").annotate(c=Count("*")).values("c")
|
).values("subevent").annotate(c=Count("*")).values("c")
|
||||||
),
|
),
|
||||||
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))),
|
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))),
|
||||||
|
|||||||
@@ -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):
|
def item_meta_values(request, organizer, event):
|
||||||
q = request.GET.get('q')
|
q = request.GET.get('q')
|
||||||
propname = request.GET.get('property')
|
propname = request.GET.get('property')
|
||||||
|
|||||||
@@ -19,23 +19,19 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import hmac
|
|
||||||
import json
|
import json
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import (
|
from django.contrib.auth import (
|
||||||
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
|
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
|
||||||
logout,
|
|
||||||
)
|
)
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
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.db import transaction
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
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.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
@@ -222,13 +218,11 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
|
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',
|
self.request.user.log_action('pretix.control.auth.user.impersonated',
|
||||||
user=request.user,
|
user=request.user,
|
||||||
data={
|
data={
|
||||||
'other': self.kwargs.get("id"),
|
'other': self.kwargs.get("id"),
|
||||||
'other_email': self.object.email,
|
'other_email': self.object.email
|
||||||
'staff_session': staff_session.pk,
|
|
||||||
})
|
})
|
||||||
oldkey = request.session.session_key
|
oldkey = request.session.session_key
|
||||||
|
|
||||||
@@ -236,15 +230,7 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
|||||||
hijacked = self.object
|
hijacked = self.object
|
||||||
|
|
||||||
hijack_history = request.session.get("hijack_history", [])
|
hijack_history = request.session.get("hijack_history", [])
|
||||||
hijack_history.append({
|
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
|
||||||
"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(),
|
|
||||||
})
|
|
||||||
|
|
||||||
backend = get_used_backend(request)
|
backend = get_used_backend(request)
|
||||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
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):
|
with signals.no_update_last_login(), keep_session_age(request.session):
|
||||||
login(request, hijacked, backend=backend)
|
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
|
request.session["hijack_history"] = hijack_history
|
||||||
|
|
||||||
signals.hijack_started.send(
|
signals.hijack_started.send(
|
||||||
@@ -274,28 +254,13 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
|||||||
class UserImpersonateStopView(LoginRequiredMixin, View):
|
class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
staff_session_key = request.session['hijacker_session']
|
impersonated = request.user
|
||||||
prev_session_key = request.session.session_key
|
|
||||||
|
hijs = request.session['hijacker_session']
|
||||||
hijack_history = request.session.get("hijack_history", [])
|
hijack_history = request.session.get("hijack_history", [])
|
||||||
hijacked = request.user
|
hijacked = request.user
|
||||||
prev_session = hijack_history.pop()
|
user_pk = hijack_history.pop()
|
||||||
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
|
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
|
||||||
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())
|
|
||||||
|
|
||||||
backend = get_used_backend(request)
|
backend = get_used_backend(request)
|
||||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
||||||
with signals.no_update_last_login(), keep_session_age(request.session):
|
with signals.no_update_last_login(), keep_session_age(request.session):
|
||||||
@@ -310,22 +275,17 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
|
|||||||
hijacked=hijacked,
|
hijacked=hijacked,
|
||||||
)
|
)
|
||||||
|
|
||||||
request.session.save()
|
ss = request.user.get_active_staff_session(hijs)
|
||||||
staff_session.session_key = request.session.session_key
|
if ss:
|
||||||
staff_session.save()
|
request.session.save()
|
||||||
|
ss.session_key = request.session.session_key
|
||||||
staff_session.logs.create(
|
ss.save()
|
||||||
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]})',
|
|
||||||
)
|
|
||||||
|
|
||||||
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
|
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
|
||||||
user=request.user,
|
user=request.user,
|
||||||
data={
|
data={
|
||||||
'other': hijacked.pk,
|
'other': impersonated.pk,
|
||||||
'other_email': hijacked.email,
|
'other_email': impersonated.email
|
||||||
'staff_session': staff_session.pk,
|
|
||||||
})
|
})
|
||||||
return redirect(reverse('control:index'))
|
return redirect(reverse('control:index'))
|
||||||
|
|
||||||
|
|||||||
@@ -40,11 +40,9 @@ import bleach
|
|||||||
from defusedcsv import csv
|
from defusedcsv import csv
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import (
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
BadRequest, PermissionDenied, ValidationError,
|
|
||||||
)
|
|
||||||
from django.db import connection, transaction
|
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 (
|
from django.http import (
|
||||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
@@ -57,7 +55,7 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
CreateView, FormView, ListView, TemplateView, UpdateView, View,
|
CreateView, ListView, TemplateView, UpdateView, View,
|
||||||
)
|
)
|
||||||
from django_scopes import scopes_disabled
|
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.templatetags.rich_text import markdown_compile_email
|
||||||
from pretix.base.views.tasks import AsyncFormView
|
from pretix.base.views.tasks import AsyncFormView
|
||||||
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
||||||
from pretix.control.forms.vouchers import (
|
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||||
VoucherBulkEditForm, VoucherBulkForm, VoucherForm,
|
|
||||||
)
|
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from pretix.control.signals import voucher_form_class
|
from pretix.control.signals import voucher_form_class
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import PaginationMixin
|
||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
|
||||||
class VoucherQueryMixin:
|
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||||
|
|
||||||
@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):
|
|
||||||
model = Voucher
|
model = Voucher
|
||||||
context_object_name = 'vouchers'
|
context_object_name = 'vouchers'
|
||||||
template_name = 'pretixcontrol/vouchers/index.html'
|
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
|
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
||||||
def get_queryset(self):
|
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'
|
'item', 'variation', 'seat'
|
||||||
))
|
))
|
||||||
|
if self.filter_form.is_valid():
|
||||||
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return VoucherFilterForm(data=self.request.GET, event=self.request.event)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.GET.get("download", "") == "yes":
|
if request.GET.get("download", "") == "yes":
|
||||||
return self._download_csv()
|
return self._download_csv()
|
||||||
@@ -317,12 +293,6 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
f.disabled = True
|
f.disabled = True
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
return {
|
|
||||||
**super().get_form_kwargs(),
|
|
||||||
"event": self.request.event,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> VoucherForm:
|
def get_object(self, queryset=None) -> VoucherForm:
|
||||||
url = resolve(self.request.path_info)
|
url = resolve(self.request.path_info)
|
||||||
try:
|
try:
|
||||||
@@ -368,7 +338,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
}
|
}
|
||||||
if self.object.subevent_id:
|
if self.object.subevent_id:
|
||||||
url_params['subevent'] = 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
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -633,21 +603,26 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
|
class VoucherBulkAction(EventPermissionRequiredMixin, View):
|
||||||
permission = 'event.vouchers:write'
|
permission = 'event.vouchers:write'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def objects(self):
|
||||||
|
return self.request.event.vouchers.filter(
|
||||||
|
id__in=self.request.POST.getlist('voucher')
|
||||||
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if request.POST.get('action') == 'delete':
|
if request.POST.get('action') == 'delete':
|
||||||
return render(request, 'pretixcontrol/vouchers/delete_bulk.html', {
|
return render(request, 'pretixcontrol/vouchers/delete_bulk.html', {
|
||||||
'allowed': self.get_queryset().filter(redeemed=0),
|
'allowed': self.objects.filter(redeemed=0),
|
||||||
'forbidden': self.get_queryset().exclude(redeemed=0),
|
'forbidden': self.objects.exclude(redeemed=0),
|
||||||
})
|
})
|
||||||
elif request.POST.get('action') == 'delete_confirm':
|
elif request.POST.get('action') == 'delete_confirm':
|
||||||
log_entries = []
|
log_entries = []
|
||||||
to_delete = []
|
to_delete = []
|
||||||
to_update = []
|
for obj in self.objects:
|
||||||
for obj in self.get_queryset():
|
|
||||||
if obj.allow_delete():
|
if obj.allow_delete():
|
||||||
log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
|
log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
|
||||||
to_delete.append(obj.pk)
|
to_delete.append(obj.pk)
|
||||||
@@ -657,14 +632,12 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
|
|||||||
'bulk': True
|
'bulk': True
|
||||||
}, save=False))
|
}, save=False))
|
||||||
obj.max_usages = min(obj.redeemed, obj.max_usages)
|
obj.max_usages = min(obj.redeemed, obj.max_usages)
|
||||||
to_update.append(obj)
|
obj.save(update_fields=['max_usages'])
|
||||||
|
|
||||||
if to_delete:
|
if to_delete:
|
||||||
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
|
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
|
||||||
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
|
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
|
||||||
Voucher.objects.filter(pk__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)
|
LogEntry.bulk_create_and_postprocess(log_entries)
|
||||||
messages.success(request, _('The selected vouchers have been deleted or disabled.'))
|
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,
|
'organizer': self.request.event.organizer.slug,
|
||||||
'event': self.request.event.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)
|
|
||||||
|
|||||||
@@ -29,8 +29,3 @@ class PretixHelpersConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
from .monkeypatching import monkeypatch_all_at_ready
|
from .monkeypatching import monkeypatch_all_at_ready
|
||||||
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']
|
|
||||||
|
|||||||
@@ -29,61 +29,36 @@ class EnvOrParserConfig:
|
|||||||
self.cp = configparser
|
self.cp = configparser
|
||||||
|
|
||||||
def _envkey(self, section, option):
|
def _envkey(self, section, option):
|
||||||
section = re.sub("[^a-zA-Z0-9]", "_", section.upper())
|
section = re.sub('[^a-zA-Z0-9]', '_', section.upper())
|
||||||
option = re.sub("[^a-zA-Z0-9]", "_", option.upper())
|
option = re.sub('[^a-zA-Z0-9]', '_', option.upper())
|
||||||
return f"PRETIX_{section}_{option}"
|
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}"
|
|
||||||
|
|
||||||
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
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:
|
if self._envkey(section, option) in os.environ:
|
||||||
return os.environ[self._envkey(section, option)]
|
return os.environ[self._envkey(section, option)]
|
||||||
return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback)
|
return self.cp.get(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
|
|
||||||
def getint(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
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:
|
if self._envkey(section, option) in os.environ:
|
||||||
return int(os.environ[self._envkey(section, option)])
|
return int(os.environ[self._envkey(section, option)])
|
||||||
return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback)
|
return self.cp.getint(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
|
|
||||||
def getfloat(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
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:
|
if self._envkey(section, option) in os.environ:
|
||||||
return float(os.environ[self._envkey(section, option)])
|
return float(os.environ[self._envkey(section, option)])
|
||||||
return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback)
|
return self.cp.getfloat(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
|
|
||||||
def getboolean(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
|
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:
|
if self._envkey(section, option) in os.environ:
|
||||||
return self.cp._convert_to_boolean(
|
return self.cp._convert_to_boolean(os.environ[self._envkey(section, option)])
|
||||||
os.environ[self._envkey(section, option)]
|
return self.cp.getboolean(section, option, raw=raw, vars=vars, fallback=fallback)
|
||||||
)
|
|
||||||
return self.cp.getboolean(
|
|
||||||
section, option, raw=raw, vars=vars, fallback=fallback
|
|
||||||
)
|
|
||||||
|
|
||||||
def has_section(self, section):
|
def has_section(self, section):
|
||||||
if any(k.startswith(self._file_envkey(section, "")) for k in os.environ):
|
if any(k.startswith(self._envkey(section, '')) for k in os.environ):
|
||||||
return True
|
|
||||||
if any(k.startswith(self._envkey(section, "")) for k in os.environ):
|
|
||||||
return True
|
return True
|
||||||
return self.cp.has_section(section)
|
return self.cp.has_section(section)
|
||||||
|
|
||||||
def has_option(self, section, option):
|
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:
|
if self._envkey(section, option) in os.environ:
|
||||||
return True
|
return True
|
||||||
return self.cp.has_option(section, option)
|
return self.cp.has_option(section, option)
|
||||||
|
|||||||
@@ -47,5 +47,5 @@ def escapejson(value):
|
|||||||
|
|
||||||
@keep_lazy(str, SafeText)
|
@keep_lazy(str, SafeText)
|
||||||
def escapejson_attr(value):
|
def escapejson_attr(value):
|
||||||
"""Hex encodes characters for use in a html attribute."""
|
"""Hex encodes characters for use in a html attributw script."""
|
||||||
return mark_safe(force_str(value).translate(_json_escapes_attr))
|
return mark_safe(force_str(value).translate(_json_escapes_attr))
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.core.signals import request_finished
|
from django.core.signals import request_finished
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -47,13 +46,6 @@ class RequestIdFilter(logging.Filter):
|
|||||||
return True
|
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:
|
class RequestIdMiddleware:
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
@@ -66,9 +58,7 @@ class RequestIdMiddleware:
|
|||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
sentry_sdk.set_tag("request_id", request.request_id)
|
sentry_sdk.set_tag("request_id", request.request_id)
|
||||||
else:
|
else:
|
||||||
# Web server did not pass a request ID, we still generate one to correlate between django logs and
|
local.request_id = request.request_id = None
|
||||||
# celery logs
|
|
||||||
local.request_id = request.request_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ from datetime import datetime
|
|||||||
from http import cookies
|
from http import cookies
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import SuspiciousFileOperation
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib3.connection import HTTPConnection, HTTPSConnection
|
from urllib3.connection import HTTPConnection, HTTPSConnection
|
||||||
@@ -41,8 +40,6 @@ from urllib3.util.connection import (
|
|||||||
)
|
)
|
||||||
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
||||||
|
|
||||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
|
||||||
|
|
||||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
_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):
|
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||||
ip_addr = ipaddress.ip_address(sa[0])
|
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:
|
if ip_addr.is_multicast:
|
||||||
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
|
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
|
||||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||||
raise HTTPError(f"Request to local address {sa[0]} blocked")
|
raise HTTPError(f"Request to local address {sa[0]} blocked")
|
||||||
if ip_addr.is_private:
|
if ip_addr.is_private:
|
||||||
raise HTTPError(f"Request to private address {sa[0]} blocked")
|
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")
|
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
@@ -234,27 +230,9 @@ def monkeypatch_cookie_morsel():
|
|||||||
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
|
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():
|
def monkeypatch_all_at_ready():
|
||||||
monkeypatch_vobject_performance()
|
monkeypatch_vobject_performance()
|
||||||
monkeypatch_pillow_safer()
|
monkeypatch_pillow_safer()
|
||||||
monkeypatch_requests_timeout()
|
monkeypatch_requests_timeout()
|
||||||
monkeypatch_urllib3_ssrf_protection()
|
monkeypatch_urllib3_ssrf_protection()
|
||||||
monkeypatch_cookie_morsel()
|
monkeypatch_cookie_morsel()
|
||||||
monkeypatch_reportlab_imagereader()
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user