mirror of
https://github.com/pretix/pretix.git
synced 2026-09-16 16:44:42 +00:00
Compare commits
146
Commits
stable
...
questionnaires
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54726d6fcd | ||
|
|
8e3cc12357 | ||
|
|
53a8a7954d | ||
|
|
59cebf3ee8 | ||
|
|
3648ef28d7 | ||
|
|
689b99c3aa | ||
|
|
6c40daa863 | ||
|
|
efdc83f72a | ||
|
|
8b10534c74 | ||
|
|
bcacff2ee7 | ||
|
|
47096f7f1b | ||
|
|
c240ebcb8e | ||
|
|
ce3e59e020 | ||
|
|
4a9da40028 | ||
|
|
eab1efd5f4 | ||
|
|
c7290b614e | ||
|
|
1aea5633eb | ||
|
|
86cc719651 | ||
|
|
742a69f09f | ||
|
|
c05dee7af5 | ||
|
|
e399a49c8b | ||
|
|
b9b951e56b | ||
|
|
88bb1f4008 | ||
|
|
24e7db7ddd | ||
|
|
96ed6fe1a6 | ||
|
|
d85e52c83e | ||
|
|
d9b5fa5b16 | ||
|
|
fb948cc7f4 | ||
|
|
c358f412fd | ||
|
|
b78de6386b | ||
|
|
50c00b47ae | ||
|
|
6025c7942f | ||
|
|
23a3412154 | ||
|
|
3635f6fb0c | ||
|
|
2aadfa2ce4 | ||
|
|
dd0d78242e | ||
|
|
42c5de895c | ||
|
|
e91718e73b | ||
|
|
2c15d8b074 | ||
|
|
edb4069e18 | ||
|
|
dc7d5c6029 | ||
|
|
caa6fb187b | ||
|
|
7d93cae2a9 | ||
|
|
58a58eff83 | ||
|
|
a84a02c298 | ||
|
|
4390403b9a | ||
|
|
9d53cf840b | ||
|
|
023f9104ef | ||
|
|
e6572344ca | ||
|
|
2e8e6a6b07 | ||
|
|
19cd0a7f43 | ||
|
|
aa583a291f | ||
|
|
1414c22eb6 | ||
|
|
0a6b8c0493 | ||
|
|
4b4a301e6e | ||
|
|
d7aae65777 | ||
|
|
dbd971cc22 | ||
|
|
6d4aba6e3d | ||
|
|
4755e7af0e | ||
|
|
f476ce7696 | ||
|
|
bf85789efd | ||
|
|
3fbd63e80a | ||
|
|
2af60b499f | ||
|
|
b74371bef1 | ||
|
|
9f8ba28cda | ||
|
|
44a7bfceab | ||
|
|
5af9be6ef0 | ||
|
|
b809d93bdc | ||
|
|
d5a8d71ed3 | ||
|
|
d481574d42 | ||
|
|
0593172146 | ||
|
|
64a5361dba | ||
|
|
5622c1ddaa | ||
|
|
73c29fc3b9 | ||
|
|
c73f2f77cf | ||
|
|
55a0db3a32 | ||
|
|
a47768312b | ||
|
|
cf272bf575 | ||
|
|
f3e0e7f4ac | ||
|
|
5a06f9416c | ||
|
|
606e3f1c0f | ||
|
|
e38ae32028 | ||
|
|
b74e2fa3e1 | ||
|
|
c79a7002a6 | ||
|
|
1ebbae2b5b | ||
|
|
7db3b47889 | ||
|
|
b7ac8fc4ef | ||
|
|
c1a830e54d | ||
|
|
2178dee6db | ||
|
|
266104f4af | ||
|
|
d10afbd080 | ||
|
|
1e78bfbc4c | ||
|
|
08356feea1 | ||
|
|
b124cb6aad | ||
|
|
cb5182f21c | ||
|
|
011487404c | ||
|
|
7703c04954 | ||
|
|
58f331ba1f | ||
|
|
91a3993cef | ||
|
|
4969c12c0a | ||
|
|
0b319d201c | ||
|
|
eceeb9caa7 | ||
|
|
3c17d05d05 | ||
|
|
1c17c35b16 | ||
|
|
f9ea9de078 | ||
|
|
4dd6e1a19f | ||
|
|
4cb9d76281 | ||
|
|
f8cc31b120 | ||
|
|
b241adb7d0 | ||
|
|
7b1558b22e | ||
|
|
488f731396 | ||
|
|
b8b02de283 | ||
|
|
c4a5a9a84d | ||
|
|
4e5fbacf6d | ||
|
|
7fe31634e6 | ||
|
|
f25c233e91 | ||
|
|
5cf28f1b81 | ||
|
|
d08216d8c5 | ||
|
|
4a28689690 | ||
|
|
958f75b109 | ||
|
|
81f58456e5 | ||
|
|
4d9dfa88fe | ||
|
|
ea792e76b2 | ||
|
|
5c448279f0 | ||
|
|
e3fa1aed7f | ||
|
|
b164cf44c0 | ||
|
|
7bc79337c3 | ||
|
|
5d8ba189fa | ||
|
|
1ea1c1789c | ||
|
|
1a6edae769 | ||
|
|
c4950e714d | ||
|
|
407728cc55 | ||
|
|
9fe25544c1 | ||
|
|
8133061fe1 | ||
|
|
288ac50600 | ||
|
|
e7657a3dd3 | ||
|
|
b659534772 | ||
|
|
c11ebb1391 | ||
|
|
b20557a996 | ||
|
|
e4a0bba3bc | ||
|
|
c369ba5f60 | ||
|
|
2c876057bf | ||
|
|
b70c1b02c2 | ||
|
|
01d736361d | ||
|
|
7627e4b548 | ||
|
|
541e7412e1 |
@@ -0,0 +1,2 @@
|
||||
# Format pre-vue code with eslint where possible (2026-09-10)
|
||||
d85e52c83ed3639e040372fd0052e842e4899d90
|
||||
@@ -0,0 +1,43 @@
|
||||
name: SBOM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, sbom ]
|
||||
tags: [ 'v.*' ]
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Submission
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.x'
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install -y gettext unzip
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install -U "prisma-sbom-submit[python]"
|
||||
- name: Create SBOM
|
||||
run: NPM=$(which npm) prisma-sbom-submit collect . sbom.json
|
||||
- name: Submit SBOM
|
||||
run: prisma-sbom-submit upload --server https://prisma.pretix.com sbom.json
|
||||
env:
|
||||
PRISMA_UPLOAD_TOKEN: ${{ secrets.PRISMA_UPLOAD_TOKEN }}
|
||||
@@ -123,7 +123,24 @@ jobs:
|
||||
working-directory: ./src
|
||||
run: make all compress
|
||||
- name: Install Playwright browsers
|
||||
run: playwright install
|
||||
run: playwright install --with-deps
|
||||
- name: Run E2E tests
|
||||
working-directory: ./src
|
||||
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10
|
||||
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10 --tracing=retain-on-failure
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: test-results/
|
||||
- name: Log trace instructions
|
||||
if: steps.check-traces.outputs.found == 'true'
|
||||
run: |
|
||||
{
|
||||
echo "## 🎭 Playwright traces available"
|
||||
echo ""
|
||||
echo "Some tests failed or retried and produced traces."
|
||||
echo ""
|
||||
echo "1. Download the **playwright-traces-${{ github.run_id }}** artifact from this run (link in the **Summary** tab, under Artifacts)."
|
||||
echo "2. Unzip it."
|
||||
echo "3. Go to https://trace.playwright.dev and drag \`trace.zip\` into the page — or run \`npx playwright show-trace trace.zip\` locally."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -71,6 +71,8 @@ Checking a ticket in
|
||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
|
||||
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
|
||||
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
|
||||
object will not reflect the simulated changes.
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
||||
@@ -566,7 +566,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://pretix.eu",
|
||||
"region": "DE",
|
||||
…
|
||||
}
|
||||
|
||||
@@ -579,12 +579,14 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url":
|
||||
"region":
|
||||
{
|
||||
"value": "https://pretix.eu",
|
||||
"label": "Imprint URL",
|
||||
"value": "DE",
|
||||
"label": "Region",
|
||||
"readonly": false,
|
||||
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
|
||||
"help_text": "Will be used to determine date and time formatting as well as default country for customer
|
||||
addresses and phone numbers. For formatting, this takes less priority than the language and
|
||||
is therefore mostly relevant for languages used in different regions globally (like English)."
|
||||
}
|
||||
},
|
||||
…
|
||||
@@ -620,7 +622,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/"
|
||||
"region": "DE"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -632,7 +634,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/",
|
||||
"region": "DE",
|
||||
…
|
||||
}
|
||||
|
||||
|
||||
@@ -2038,7 +2038,7 @@ Manipulating individual positions
|
||||
|
||||
* ``order`` (mandatory, specified as a string mapping to a ``code``)
|
||||
|
||||
* ``addon_to`` (optional, specified as an integer mapping to the ``positionid`` of the parent position)
|
||||
* ``addon_to`` (optional, specified as an integer mapping to ``positionid`` - the number of the position within the order, see :ref:`_order-position-resource` - of the parent position)
|
||||
|
||||
* ``item`` (mandatory)
|
||||
|
||||
@@ -2348,7 +2348,7 @@ otherwise, such as splitting an order or changing fees.
|
||||
"subevent": 562,
|
||||
"seat": "seat-guid-2",
|
||||
"price": "99.99",
|
||||
"addon_to": 12374,
|
||||
"addon_to": 1,
|
||||
"attendee_name": "Peter",
|
||||
}
|
||||
],
|
||||
|
||||
@@ -65,6 +65,7 @@ valid_date_max date Maximum value f
|
||||
valid_datetime_min datetime Minimum value for date and time questions (optional)
|
||||
valid_datetime_max datetime Maximum value for date and time questions (optional)
|
||||
valid_file_portrait boolean Turn on file validation for portrait photos
|
||||
valid_string_length_min integer Minimum length for string questions (optional)
|
||||
valid_string_length_max integer Maximum length for string questions (optional)
|
||||
dependency_question integer Internal ID of a different question. The current
|
||||
question will only be shown if the question given in
|
||||
@@ -130,6 +131,7 @@ Endpoints
|
||||
"valid_date_max": null,
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_string_length_min": null,
|
||||
"valid_string_length_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"dependency_question": null,
|
||||
@@ -211,6 +213,7 @@ Endpoints
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"valid_string_length_min": null,
|
||||
"valid_string_length_max": null,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
@@ -315,6 +318,7 @@ Endpoints
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"valid_string_length_min": null,
|
||||
"valid_string_length_max": null,
|
||||
"options": [
|
||||
{
|
||||
@@ -399,6 +403,7 @@ Endpoints
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"valid_string_length_min": null,
|
||||
"valid_string_length_max": null,
|
||||
"options": [
|
||||
{
|
||||
|
||||
+58
-1
@@ -8,7 +8,64 @@ import vuePug from 'eslint-plugin-vue-pug'
|
||||
|
||||
const ignores = globalIgnores([
|
||||
'**/node_modules',
|
||||
'**/dist'
|
||||
'**/dist',
|
||||
// Vendored code
|
||||
'src/pretix/static/leaflet',
|
||||
'src/pretix/static/clipboard',
|
||||
'src/pretix/static/cropper',
|
||||
'src/pretix/static/lightbox',
|
||||
'src/pretix/static/are-you-sure',
|
||||
'src/pretix/static/vuejs',
|
||||
'src/pretix/static/fontawesome',
|
||||
'src/pretix/static/typeahead',
|
||||
'src/pretix/static/moment',
|
||||
'src/pretix/static/pdfjs',
|
||||
'src/pretix/static/sortable',
|
||||
'src/pretix/static/iframeresizer',
|
||||
'src/pretix/static/bootstrap',
|
||||
'src/pretix/static/d3',
|
||||
'src/pretix/static/jsi18n',
|
||||
'src/pretix/static/fabric',
|
||||
'src/pretix/static/datetimepicker',
|
||||
'src/pretix/static/charts',
|
||||
'src/pretix/static/fileupload',
|
||||
'src/pretix/static/seating',
|
||||
'src/pretix/static/rest_framework',
|
||||
'src/pretix/static/select2',
|
||||
'src/pretix/static/schema',
|
||||
'src/pretix/static/slider',
|
||||
'src/pretix/static/jquery',
|
||||
'src/pretix/static/colorpicker',
|
||||
'src/pretix/static/rrule',
|
||||
'src/pretix/static/pretixcontrol/js/jquery.qrcode.min.js',
|
||||
'src/pretix/static/pretixpresale/js/widget/docready.js',
|
||||
// Pre-vue JS code
|
||||
'src/pretix/static/pretixbase/js/addressform.js',
|
||||
'src/pretix/static/pretixbase/js/asynctask.js',
|
||||
'src/pretix/static/pretixbase/js/details.js',
|
||||
'src/pretix/static/pretixbase/js/gettextstub.js',
|
||||
'src/pretix/static/pretixbase/js/i18nstring.js',
|
||||
'src/pretix/static/pretixcontrol/js/menu.js',
|
||||
'src/pretix/static/pretixcontrol/js/ui/editor.js',
|
||||
'src/pretix/static/pretixcontrol/js/ui/geo.js',
|
||||
'src/pretix/static/pretixcontrol/js/ui/main.js',
|
||||
'src/pretix/static/pretixcontrol/js/ui/plugins.js',
|
||||
'src/pretix/static/pretixcontrol/js/ui/subevent.js',
|
||||
'src/pretix/static/pretixcontrol/js/ui/variations.js',
|
||||
'src/pretix/static/pretixcontrol/js/ui/webauthn.js',
|
||||
'src/pretix/static/pretixpresale/js/ui/cart.js',
|
||||
'src/pretix/static/pretixpresale/js/ui/main.js',
|
||||
'src/pretix/static/pretixpresale/js/ui/questions.js',
|
||||
'src/pretix/static/pretixpresale/js/widget/floatformat.js',
|
||||
'src/pretix/static/pretixpresale/js/widget/widget.js',
|
||||
'src/pretix/plugins/banktransfer/static',
|
||||
'src/pretix/plugins/paypal2/static',
|
||||
'src/pretix/plugins/statistics/static',
|
||||
'src/pretix/plugins/stripe/static',
|
||||
// Plugin checkouts
|
||||
'local',
|
||||
// docs
|
||||
'doc',
|
||||
])
|
||||
|
||||
export default defineConfig([
|
||||
|
||||
Generated
+14
-4
@@ -9,7 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.30"
|
||||
"vue": "^3.5.30",
|
||||
"vue-slicksort": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -4148,9 +4149,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
|
||||
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz",
|
||||
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -4635,6 +4636,15 @@
|
||||
"vue-eslint-parser": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-slicksort": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-slicksort/-/vue-slicksort-2.0.5.tgz",
|
||||
"integrity": "sha512-fXz1YrNjhUbJK7o0tMk27mIr4pMAZYLSYvtmLazCtfpvz+zafPCn34ILDL8B7hT7WLVZKreYs6JVe5VWymqmzA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.30"
|
||||
"vue": "^3.5.30",
|
||||
"vue-slicksort": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
+9
-9
@@ -33,12 +33,12 @@ dependencies = [
|
||||
"bleach==6.4.*",
|
||||
"celery==5.6.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=49.0.0",
|
||||
"cryptography>=50.0.0",
|
||||
"css-inline==0.21.*",
|
||||
"defusedcsv>=3.0.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==5.2.*",
|
||||
"django-bootstrap3==26.1",
|
||||
"Django[argon2]==5.2.*,>=5.2.17",
|
||||
"django-bootstrap3==26.2",
|
||||
"django-compressor==4.6.0",
|
||||
"django-countries==9.0.*",
|
||||
"django-filter==26.1",
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.11.*",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==5.0",
|
||||
"django-localflavor==5.1",
|
||||
"django-markup",
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.7.*",
|
||||
@@ -56,7 +56,7 @@ dependencies = [
|
||||
"django-querytagger==0.0.3",
|
||||
"django-redis==7.0.*",
|
||||
"django-scopes==2.1.*",
|
||||
"django-statici18n==2.7.*",
|
||||
"django-statici18n==2.8.*",
|
||||
"djangorestframework==3.17.*",
|
||||
"dnspython==2.8.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
@@ -67,7 +67,7 @@ dependencies = [
|
||||
"kombu==5.6.*",
|
||||
"libsass==0.23.*",
|
||||
"lxml",
|
||||
"markdown==3.10.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
"markdown==3.10.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||
"mt-940==4.30.*",
|
||||
"oauthlib==3.3.*",
|
||||
@@ -79,12 +79,12 @@ dependencies = [
|
||||
"phonenumberslite==9.0.*",
|
||||
"Pillow==12.3.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==7.35.*",
|
||||
"protobuf==7.36.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==3.0",
|
||||
"pycryptodome==3.23.*",
|
||||
"pypdf==6.5.*",
|
||||
"pypdf==6.11.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
@@ -94,7 +94,7 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==5.0.*",
|
||||
"requests==2.34.*",
|
||||
"sentry-sdk==2.66.*",
|
||||
"sentry-sdk==2.68.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2026.7.0"
|
||||
__version__ = "2026.8.0.dev0"
|
||||
|
||||
@@ -104,6 +104,7 @@ ALL_LANGUAGES = [
|
||||
('gl', _('Galician')),
|
||||
('el', _('Greek')),
|
||||
('he', _('Hebrew')),
|
||||
('hu', _('Hungarian')),
|
||||
('id', _('Indonesian')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.db import DatabaseError
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
@@ -30,6 +33,7 @@ from pretix.api.auth.devicesecurity import (
|
||||
FullAccessSecurityProfile, get_all_security_profiles,
|
||||
)
|
||||
from pretix.base.models import Device
|
||||
from pretix.base.models.devices import DeviceLastSeen
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,7 +46,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
||||
model = self.get_model()
|
||||
try:
|
||||
with scopes_disabled():
|
||||
device = model.objects.select_related('organizer').get(api_token=key)
|
||||
device = model.objects.select_related('organizer', 'last_seen').get(api_token=key)
|
||||
except model.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||
|
||||
@@ -53,6 +57,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
||||
logging.warning(f'Connection attempt of revoked device {device.pk}.')
|
||||
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
||||
|
||||
self._update_last_seen(device)
|
||||
return AnonymousUser(), device
|
||||
|
||||
def authenticate(self, request):
|
||||
@@ -63,3 +68,22 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
||||
if not profile.is_allowed(request):
|
||||
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
||||
return r
|
||||
|
||||
def _update_last_seen(self, device: Device):
|
||||
try:
|
||||
try:
|
||||
last_seen_obj = device.last_seen
|
||||
except DeviceLastSeen.DoesNotExist:
|
||||
# First request from device, create model, ignore result. Use get_or_create to be safe
|
||||
# against concurrent create requests
|
||||
DeviceLastSeen.objects.get_or_create(device=device, last_seen=now())
|
||||
else:
|
||||
if now() - last_seen_obj.last_seen < timedelta(seconds=10):
|
||||
# We don't need to know the last seen info of a device to more precision than this,
|
||||
# so we can avoid some database writes if the device is bursting a lot of requests.
|
||||
return
|
||||
last_seen_obj.last_seen = now()
|
||||
last_seen_obj.save(update_fields=["last_seen"])
|
||||
except DatabaseError:
|
||||
# Do not stop the request from happening
|
||||
logger.exception("Database error while updating last_seen")
|
||||
|
||||
@@ -115,7 +115,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('POST', 'api-v1:checkinrpc.annul'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
('GET', 'api-v1:reusablemedium-list'),
|
||||
('POST', 'api-v1:reusablemedium-lookup'),
|
||||
@@ -154,7 +154,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('POST', 'api-v1:checkinrpc.annul'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
@@ -191,7 +191,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('POST', 'api-v1:checkinrpc.annul'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
||||
answers = serializers.JSONField(required=False, allow_null=True)
|
||||
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
|
||||
exchange_medium_identifier = serializers.CharField(required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -702,8 +702,12 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
return super().save(**kwargs)
|
||||
|
||||
def validate_default(self, value):
|
||||
if not value and self.instance.default:
|
||||
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
|
||||
if not value:
|
||||
if self.instance:
|
||||
if self.instance.default:
|
||||
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
|
||||
elif not self.context["event"].tax_rules.exists():
|
||||
raise ValidationError("You can't remove the default property as there is only one tax rule.")
|
||||
return value
|
||||
|
||||
|
||||
@@ -975,7 +979,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'reusable_media_type_nfc_mf0aes',
|
||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||
'reusable_media_usage_enforced',
|
||||
'system_question_order',
|
||||
'system_question_order', # TODO(questionnaires) - remove or replace
|
||||
'tax_rule_payment',
|
||||
'tax_rule_cancellation',
|
||||
]
|
||||
|
||||
@@ -53,6 +53,7 @@ from pretix.base.models import (
|
||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
SalesChannel,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire, QuestionnaireChild
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
@@ -542,6 +543,7 @@ class LegacyDependencyValueField(serializers.CharField):
|
||||
class QuestionSerializer(I18nAwareModelSerializer):
|
||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
internal_name = serializers.CharField(allow_null=True, source='question', read_only=True)
|
||||
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -550,7 +552,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_file_portrait')
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait', 'internal_name',)
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
@@ -619,13 +621,167 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||
items = validated_data.pop('items', [])
|
||||
|
||||
question = Question.objects.create(**validated_data)
|
||||
question = Question.objects.create(**validated_data, container_type=Question.ContainerType.ORDERPOSITION)
|
||||
question.items.set(items)
|
||||
for opt_data in options_data:
|
||||
QuestionOption.objects.create(question=question, **opt_data)
|
||||
return question
|
||||
|
||||
|
||||
class QuestionRefField(serializers.PrimaryKeyRelatedField):
|
||||
def to_representation(self, qc):
|
||||
if not qc:
|
||||
return None
|
||||
elif qc.system_datafield:
|
||||
return qc.system_datafield
|
||||
elif qc.user_datafield_id:
|
||||
return qc.user_datafield_id
|
||||
else:
|
||||
return None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) == int:
|
||||
return {'user_datafield': super().to_internal_value(data), 'system_datafield': None}
|
||||
elif type(data) == str or data is None:
|
||||
return {'user_datafield': None, 'system_datafield': data}
|
||||
else:
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return self.source == '*'
|
||||
|
||||
|
||||
class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
||||
question = QuestionRefField(source='*', queryset=Question.objects.none())
|
||||
dependency_question = QuestionRefField(allow_null=True, required=False, queryset=Question.objects.none())
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ('question', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["question"].queryset = self.context["event"].questions.all()
|
||||
self.fields["dependency_question"].queryset = self.context["event"].questions.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
if dep.ask_during_checkin:
|
||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
||||
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
return data
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
|
||||
class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Questionnaire
|
||||
fields = ('id', 'type', 'internal_name', 'items', 'position', 'all_sales_channels', 'limit_sales_channels', 'children')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.fields['children'] = InlineQuestionnaireChildSerializer(many=True, required=True, context=kwargs['context'], partial=False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
#full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
#full_data.update(data)
|
||||
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
# raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
#if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
#Question.clean_items(event, full_data.get('items') or [])
|
||||
return data
|
||||
|
||||
def validate_children(self, value):
|
||||
prev_questions = {}
|
||||
for child in value:
|
||||
if child.get('dependency_question'):
|
||||
if (child['dependency_question']['user_datafield'] or child['dependency_question']['system_datafield']) not in prev_questions:
|
||||
raise ValidationError('A question can only depend on a previous question from the same questionnaire.')
|
||||
|
||||
if child['user_datafield']:
|
||||
prev_questions[child['user_datafield']] = child
|
||||
if child['system_datafield']:
|
||||
prev_questions[child['system_datafield']] = child
|
||||
return value
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
children_data = validated_data.pop('children') if 'children' in validated_data else []
|
||||
questionnaire = super().create(validated_data)
|
||||
self.set_children(questionnaire, children_data)
|
||||
return questionnaire
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
children_data = validated_data.pop('children', None)
|
||||
questionnaire = super().update(instance, validated_data)
|
||||
if children_data is not None:
|
||||
self.set_children(questionnaire, children_data)
|
||||
return questionnaire
|
||||
|
||||
def set_children(self, questionnaire, new_data):
|
||||
result = []
|
||||
child_serializer = self.fields['children'].child
|
||||
existing = questionnaire.children.all()
|
||||
for i, d in enumerate(new_data):
|
||||
d['questionnaire'] = questionnaire
|
||||
d['position'] = i + 1
|
||||
d.setdefault('required', False)
|
||||
d.setdefault('help_text', None)
|
||||
d.setdefault('dependency_question', None)
|
||||
d.setdefault('dependency_values', None)
|
||||
updatable = min(len(existing), len(new_data))
|
||||
for i in range(0, updatable):
|
||||
result.append(child_serializer.update(existing[i], new_data[i]))
|
||||
for i in range(updatable, len(new_data)):
|
||||
result.append(child_serializer.create(new_data[i]))
|
||||
for i in range(updatable, len(existing)):
|
||||
existing[i].delete()
|
||||
return result
|
||||
|
||||
|
||||
class QuotaSerializer(I18nAwareModelSerializer):
|
||||
available = serializers.BooleanField(read_only=True)
|
||||
available_number = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -426,6 +426,8 @@ class TeamSerializer(serializers.ModelSerializer):
|
||||
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
|
||||
if full_data.get(k) is True:
|
||||
data["limit_organizer_permissions"].update({kk: True for kk in v})
|
||||
for key in list(k for k in data if k.startswith("can_")):
|
||||
del data[key]
|
||||
|
||||
if full_data.get('limit_events') and full_data.get('all_events'):
|
||||
raise ValidationError('Do not set both limit_events and all_events.')
|
||||
|
||||
@@ -79,7 +79,8 @@ event_router.register(r'subevents', event.SubEventViewSet)
|
||||
event_router.register(r'clone', event.CloneEventViewSet)
|
||||
event_router.register(r'items', item.ItemViewSet)
|
||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||
event_router.register(r'questions', item.QuestionViewSet)
|
||||
event_router.register(r'datafields', item.QuestionViewSet)
|
||||
event_router.register(r'questionnaires', item.QuestionnaireViewSet)
|
||||
event_router.register(r'discounts', discount.DiscountViewSet)
|
||||
event_router.register(r'quotas', item.QuotaViewSet)
|
||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
|
||||
@@ -139,6 +139,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return qs
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -153,6 +154,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -837,6 +839,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
)
|
||||
|
||||
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
|
||||
if simulate:
|
||||
raise CheckInError(
|
||||
gettext('You cannot simulate a medium exchange.'),
|
||||
'error'
|
||||
)
|
||||
with transaction.atomic():
|
||||
# Do exchange and check-in atomically, i.e. both succeed or both fail
|
||||
medium = perform_media_exchange(
|
||||
@@ -1064,6 +1071,7 @@ class CheckinRPCRedeemView(views.APIView):
|
||||
legacy_url_support=False,
|
||||
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
|
||||
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
|
||||
simulate=s.validated_data.get('simulate'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
# 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.
|
||||
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
@@ -64,6 +65,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -78,6 +80,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -87,6 +90,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('You cannot delete this discount because it already has '
|
||||
|
||||
@@ -257,6 +257,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
copy_from = None
|
||||
if 'clone_from' in self.request.GET:
|
||||
@@ -320,6 +321,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('The event can not be deleted as it already contains orders. Please set \'live\''
|
||||
@@ -355,6 +357,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
|
||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
||||
@@ -513,6 +516,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
resp['X-Page-Generated'] = date
|
||||
return resp
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
super().perform_update(serializer)
|
||||
@@ -529,6 +533,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -538,6 +543,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
|
||||
@@ -566,6 +572,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
return self.request.event.tax_rules.all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.log_action(
|
||||
@@ -575,6 +582,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -584,6 +592,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This tax rule can not be deleted as it is currently in use.')
|
||||
@@ -757,6 +766,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
|
||||
}
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.event.log_action(
|
||||
@@ -766,6 +776,7 @@ class SeatViewSet(ConditionalListView, UpdateModelMixin, viewsets.ReadOnlyModelV
|
||||
data={"seats": [serializer.instance.pk]},
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def bulk_change_blocked(self, blocked):
|
||||
s = SeatBulkBlockInputSerializer(
|
||||
data=self.request.data,
|
||||
|
||||
@@ -23,6 +23,7 @@ from datetime import timedelta
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
@@ -45,7 +46,8 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.organizer import TeamQuerySet
|
||||
from pretix.base.services.export import (
|
||||
export, init_event_exporters, init_organizer_exporters, multiexport,
|
||||
ExportError, export, init_event_exporters, init_organizer_exporters,
|
||||
multiexport,
|
||||
)
|
||||
from pretix.helpers.http import ChunkBasedFileResponse
|
||||
|
||||
@@ -149,8 +151,11 @@ class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
))
|
||||
exporters = []
|
||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
try:
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
except ExportError:
|
||||
pass
|
||||
return exporters
|
||||
|
||||
def do_export(self, cf, instance, data):
|
||||
@@ -180,8 +185,11 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
))
|
||||
exporters = []
|
||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
try:
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
except ExportError:
|
||||
pass
|
||||
return exporters
|
||||
|
||||
def do_export(self, cf, instance, data):
|
||||
@@ -220,6 +228,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
qs = self.request.event.scheduled_exports
|
||||
return qs.select_related("owner")
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_authenticated:
|
||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||
@@ -250,6 +259,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
))
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
@@ -275,6 +285,7 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.request.event.log_action(
|
||||
'pretix.event.export.schedule.deleted',
|
||||
@@ -302,6 +313,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
qs = self.request.organizer.scheduled_exports
|
||||
return qs.select_related("owner")
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_authenticated:
|
||||
raise PermissionDenied('Creation of exports requires user-specific API access.')
|
||||
@@ -332,6 +344,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
))
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
||||
@@ -382,6 +395,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.export.schedule.deleted',
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
@@ -47,13 +48,15 @@ from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.item import (
|
||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
||||
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
||||
QuestionnaireSerializer, QuestionOptionSerializer, QuestionSerializer,
|
||||
QuotaSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
||||
ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
@@ -109,6 +112,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
|
||||
).all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -123,6 +127,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
@@ -139,6 +144,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This item cannot be deleted because it has already been ordered '
|
||||
@@ -183,6 +189,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = self.item
|
||||
if not item.has_variations:
|
||||
@@ -197,6 +204,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
{'value': serializer.instance.value})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.item.log_action(
|
||||
@@ -207,6 +215,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
{'value': serializer.instance.value})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('This variation cannot be deleted because it has already been ordered '
|
||||
@@ -249,6 +258,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
serializer.save(base_item=item)
|
||||
@@ -259,6 +269,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.base_item.log_action(
|
||||
@@ -268,6 +279,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.base_item.log_action(
|
||||
@@ -303,6 +315,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
serializer.save(item=item)
|
||||
@@ -313,6 +326,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.item.log_action(
|
||||
@@ -322,6 +336,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.item.log_action(
|
||||
@@ -354,6 +369,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
item = self.item
|
||||
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
|
||||
@@ -365,6 +381,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.base_item.log_action(
|
||||
@@ -374,6 +391,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.base_item.log_action(
|
||||
@@ -403,6 +421,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
return self.request.event.categories.all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -417,6 +436,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -426,6 +446,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
for item in instance.items.all():
|
||||
item.category = None
|
||||
@@ -456,8 +477,12 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.prefetch_related('options').all()
|
||||
return self.request.event.questions.filter(
|
||||
# the container_type parameter is undocumented, this API is going to change in a later release
|
||||
container_type=self.request.GET.get('container_type', Question.ContainerType.ORDERPOSITION),
|
||||
).prefetch_related('options').all()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -472,6 +497,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -481,6 +507,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.question.deleted',
|
||||
@@ -509,6 +536,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
ctx['question'] = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||
serializer.save(question=q)
|
||||
@@ -519,6 +547,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.question.log_action(
|
||||
@@ -528,6 +557,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.question.log_action(
|
||||
'pretix.event.question.option.deleted',
|
||||
@@ -538,6 +568,51 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class QuestionnaireViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = QuestionnaireSerializer
|
||||
queryset = Questionnaire.objects.none()
|
||||
#filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
#filterset_class = QuestionFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questionnaires.prefetch_related('children').all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.questionnaire.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
@@ -586,6 +661,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -608,6 +684,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ctx['request'] = self.request
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
@@ -663,6 +740,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
)
|
||||
serializer.instance.rebuild_cache()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.quota.deleted',
|
||||
|
||||
@@ -394,6 +394,7 @@ class TeamViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action('pretix.team.deleted', user=self.request.user, auth=self.request.auth)
|
||||
instance.delete()
|
||||
@@ -693,6 +694,7 @@ class MembershipTypeViewSet(viewsets.ModelViewSet):
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
@@ -833,6 +835,7 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
@@ -62,6 +63,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
@@ -70,6 +72,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
if serializer.instance.voucher:
|
||||
raise PermissionDenied('This entry can not be changed as it has already been assigned a voucher.')
|
||||
@@ -80,6 +83,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
if instance.voucher:
|
||||
raise PermissionDenied('This entry can not be deleted as it has already been assigned a voucher.')
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
|
||||
@@ -48,6 +49,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
@@ -57,6 +59,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
@@ -67,6 +70,7 @@ class WebHookViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
self.request.organizer.log_action(
|
||||
'pretix.webhook.changed',
|
||||
|
||||
+490
-428
@@ -349,16 +349,22 @@ class WrappedPhonePrefixSelect(Select):
|
||||
return super().render(name, value or self.initial, *args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
if value and self.choices[1][0] != value:
|
||||
matching_choices = len([1 for p, c in self.choices if p == value])
|
||||
# self.choices is lazy evaluated, needs to be realized to be modifiable
|
||||
choices = list(self.choices)
|
||||
if value and choices[1][0] != value:
|
||||
matching_choices = len([1 for p, c in choices if p == value])
|
||||
if matching_choices > 1:
|
||||
# Some countries share a phone prefix, for example +1 is used all over the Americas.
|
||||
# This causes a UX problem: If the default value or the existing data is +12125552368,
|
||||
# the widget will just show the first <option> entry with value="+1" as selected,
|
||||
# which alphabetically is America Samoa, although most numbers statistically are from
|
||||
# the US. As a workaround, we detect this case and add an aditional choice value with
|
||||
# the US. As a workaround, we detect this case and add an additional choice value with
|
||||
# just <option value="+1">+1</option> without an explicit country.
|
||||
self.choices.insert(1, (value, value))
|
||||
self.choices = [
|
||||
choices[0],
|
||||
(value, value),
|
||||
*choices[1:],
|
||||
]
|
||||
context = super().get_context(name, value, attrs)
|
||||
return context
|
||||
|
||||
@@ -638,447 +644,243 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
the attendee name for admission tickets, if the corresponding setting is enabled,
|
||||
as well as additional questions defined by the organizer.
|
||||
This is the base form class responsible for asking order- or ticket-related questions.
|
||||
"""
|
||||
address_validation = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Takes two additional keyword arguments:
|
||||
|
||||
:param cartpos: The cart position the form should be for
|
||||
:param event: The event this belongs to
|
||||
"""
|
||||
request = kwargs.pop('request', None)
|
||||
cartpos = self.cartpos = kwargs.pop('cartpos', None)
|
||||
orderpos = self.orderpos = kwargs.pop('orderpos', None)
|
||||
pos = cartpos or orderpos
|
||||
item = pos.item
|
||||
questions = pos.item.questions_to_ask
|
||||
event = kwargs.pop('event')
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||
if item.validity_dynamic_start_choice_day_limit:
|
||||
max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
||||
else:
|
||||
max_date = None
|
||||
min_date = time_machine_now()
|
||||
def build_user_question_field(self, request, event, answerlist, qc, datafield):
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
answers = [a for a in answerlist if a.question_id == datafield.id]
|
||||
if answers:
|
||||
initial = answers[0]
|
||||
else:
|
||||
initial = None
|
||||
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
|
||||
if pos.used_membership.date_start >= time_machine_now():
|
||||
initial = min_date = pos.used_membership.date_start
|
||||
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
|
||||
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
|
||||
attrs = {}
|
||||
if max_date:
|
||||
attrs['data-max'] = max_date.date().isoformat()
|
||||
if min_date:
|
||||
attrs['data-min'] = min_date.date().isoformat()
|
||||
self.fields['requested_valid_from'] = forms.DateField(
|
||||
label=_('Start date'),
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
validators=([MaxDateValidator(max_date.date())] if max_date else []) + [MinDateValidator(min_date.date())]
|
||||
)
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
required = qc.required and not self.all_optional
|
||||
if datafield.type == Question.TYPE_BOOLEAN:
|
||||
if required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
# itself.
|
||||
widget = forms.CheckboxInput(attrs={'required': 'required'})
|
||||
else:
|
||||
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
|
||||
label=_('Start date'),
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=min_date,
|
||||
max_date=max_date
|
||||
),
|
||||
validators=([MaxDateTimeValidator(max_date)] if max_date else []) + [MinDateTimeValidator(min_date)]
|
||||
)
|
||||
widget = forms.CheckboxInput()
|
||||
|
||||
add_fields = {}
|
||||
if initial:
|
||||
initialbool = (initial.answer == "True")
|
||||
else:
|
||||
initialbool = False
|
||||
|
||||
if item.ask_attendee_data and event.settings.attendee_names_asked:
|
||||
add_fields['attendee_name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=event.settings.attendee_names_required and not self.all_optional,
|
||||
scheme=event.settings.name_scheme,
|
||||
titles=event.settings.name_scheme_titles,
|
||||
label=_('Attendee name'),
|
||||
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
|
||||
field = forms.BooleanField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=initialbool, widget=widget,
|
||||
)
|
||||
if item.ask_attendee_data and event.settings.attendee_emails_asked:
|
||||
add_fields['attendee_email'] = forms.EmailField(
|
||||
required=event.settings.attendee_emails_required and not self.all_optional,
|
||||
label=_('Attendee email'),
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
'autocomplete': 'email'
|
||||
}
|
||||
)
|
||||
elif datafield.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_value=datafield.valid_number_min or Decimal('0.00'),
|
||||
max_value=datafield.valid_number_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
if item.ask_attendee_data and event.settings.attendee_company_asked:
|
||||
add_fields['company'] = forms.CharField(
|
||||
required=event.settings.attendee_company_required and not self.all_optional,
|
||||
label=_('Company'),
|
||||
max_length=255,
|
||||
initial=(cartpos.company if cartpos else orderpos.company),
|
||||
elif datafield.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_length=datafield.valid_string_length_min,
|
||||
max_length=datafield.valid_string_length_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
|
||||
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
|
||||
add_fields['street'] = forms.CharField(
|
||||
required=self.attendee_addresses_required,
|
||||
label=_('Address'),
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
'placeholder': _('Street and Number'),
|
||||
'autocomplete': 'street-address'
|
||||
}),
|
||||
initial=(cartpos.street if cartpos else orderpos.street),
|
||||
elif datafield.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_length=datafield.valid_string_length_min,
|
||||
max_length=datafield.valid_string_length_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
add_fields['zipcode'] = forms.CharField(
|
||||
required=False,
|
||||
max_length=30,
|
||||
label=_('ZIP code'),
|
||||
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'postal-code',
|
||||
}),
|
||||
)
|
||||
add_fields['city'] = forms.CharField(
|
||||
required=False,
|
||||
label=_('City'),
|
||||
max_length=255,
|
||||
initial=(cartpos.city if cartpos else orderpos.city),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
)
|
||||
country = (cartpos.country if cartpos else orderpos.country) or guess_country_from_request(request, event)
|
||||
add_fields['country'] = CountryField(
|
||||
countries=CachedCountries
|
||||
elif datafield.type == Question.TYPE_COUNTRYCODE:
|
||||
field = CountryField(
|
||||
countries=CachedCountries,
|
||||
blank=True, null=True, blank_label=' ',
|
||||
).formfield(
|
||||
required=self.attendee_addresses_required,
|
||||
label=_('Country'),
|
||||
initial=country,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'country',
|
||||
'data-trigger-address-info': 'on',
|
||||
}),
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
widget=forms.Select,
|
||||
empty_label=' ',
|
||||
initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None),
|
||||
)
|
||||
c = [('', '---')]
|
||||
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||
cc = None
|
||||
state = None
|
||||
if fprefix + 'country' in self.data:
|
||||
cc = str(self.data[fprefix + 'country'])
|
||||
elif country:
|
||||
cc = str(country)
|
||||
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||
state = (cartpos.state if cartpos else orderpos.state)
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
add_fields['state'] = forms.ChoiceField(
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
initial=state,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
elif datafield.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
queryset=datafield.options,
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
widget=forms.Select,
|
||||
to_field_name='identifier',
|
||||
empty_label='',
|
||||
initial=initial.options.first() if initial else None,
|
||||
)
|
||||
add_fields['state'].widget.is_required = True
|
||||
|
||||
field_positions = list(
|
||||
[
|
||||
(n, event.settings.system_question_order.get(n if n != 'state' else 'country', 0))
|
||||
for n in add_fields.keys()
|
||||
]
|
||||
)
|
||||
|
||||
for q in questions:
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
answers = [a for a in pos.answerlist if a.question_id == q.id]
|
||||
if answers:
|
||||
initial = answers[0]
|
||||
elif datafield.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
field = forms.ModelMultipleChoiceField(
|
||||
queryset=datafield.options,
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
to_field_name='identifier',
|
||||
widget=QuestionCheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_FILE:
|
||||
if datafield.valid_file_portrait:
|
||||
field = PortraitImageField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=initial.file if initial else None,
|
||||
widget=PortraitImageWidget(answer=initial, request=request, attrs={'data-portrait-photo': 'true'}),
|
||||
)
|
||||
else:
|
||||
initial = None
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
help_text = rich_text(q.help_text)
|
||||
label = escape(q.question) # django-bootstrap3 calls mark_safe
|
||||
required = q.required and not self.all_optional
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
if required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
# itself.
|
||||
widget = forms.CheckboxInput(attrs={'required': 'required'})
|
||||
else:
|
||||
widget = forms.CheckboxInput()
|
||||
|
||||
if initial:
|
||||
initialbool = (initial.answer == "True")
|
||||
else:
|
||||
initialbool = False
|
||||
|
||||
field = forms.BooleanField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initialbool, widget=widget,
|
||||
field = ExtFileField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(answer=initial, request=request),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
||||
)
|
||||
elif q.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=label, required=required,
|
||||
min_value=q.valid_number_min or Decimal('0.00'),
|
||||
max_value=q.valid_number_max,
|
||||
help_text=help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
label=label, required=required,
|
||||
max_length=q.valid_string_length_max,
|
||||
help_text=help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=label, required=required,
|
||||
max_length=q.valid_string_length_max,
|
||||
help_text=help_text,
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||
field = CountryField(
|
||||
countries=CachedCountries,
|
||||
blank=True, null=True, blank_label=' ',
|
||||
).formfield(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Select,
|
||||
empty_label=' ',
|
||||
initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None),
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
queryset=q.options,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Select,
|
||||
to_field_name='identifier',
|
||||
empty_label='',
|
||||
initial=initial.options.first() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
field = forms.ModelMultipleChoiceField(
|
||||
queryset=q.options,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
to_field_name='identifier',
|
||||
widget=QuestionCheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
if q.valid_file_portrait:
|
||||
field = PortraitImageField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.file if initial else None,
|
||||
widget=PortraitImageWidget(position=pos, event=event, answer=initial, attrs={'data-portrait-photo': 'true'}),
|
||||
elif datafield.type == Question.TYPE_DATE:
|
||||
attrs = {}
|
||||
if datafield.valid_date_min:
|
||||
attrs['data-min'] = datafield.valid_date_min.isoformat()
|
||||
if datafield.valid_date_max:
|
||||
attrs['data-max'] = datafield.valid_date_max.isoformat()
|
||||
help_text = qc.help_text
|
||||
if not help_text:
|
||||
if datafield.valid_date_min and datafield.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date between {min} and {max}.'),
|
||||
min=date_format(datafield.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(datafield.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
else:
|
||||
field = ExtFileField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
||||
elif datafield.valid_date_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no earlier than {min}.'),
|
||||
min=date_format(datafield.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
attrs = {}
|
||||
if q.valid_date_min:
|
||||
attrs['data-min'] = q.valid_date_min.isoformat()
|
||||
if q.valid_date_max:
|
||||
attrs['data-max'] = q.valid_date_max.isoformat()
|
||||
if not help_text:
|
||||
if q.valid_date_min and q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date between {min} and {max}.'),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif q.valid_date_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no earlier than {min}.'),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no later than {max}.'),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).date()
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
else:
|
||||
elif datafield.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no later than {max}.'),
|
||||
max=date_format(datafield.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).date()
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
field = forms.DateField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=_initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
)
|
||||
if q.valid_date_min:
|
||||
field.validators.append(MinDateValidator(q.valid_date_min))
|
||||
if q.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(q.valid_date_max))
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).time()
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
else:
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.DateField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(help_text),
|
||||
initial=_initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
)
|
||||
if datafield.valid_date_min:
|
||||
field.validators.append(MinDateValidator(datafield.valid_date_min))
|
||||
if datafield.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(datafield.valid_date_max))
|
||||
elif datafield.type == Question.TYPE_TIME:
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).time()
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
field = forms.TimeField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=_initial,
|
||||
widget=TimePickerWidget(without_seconds=True),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
if not help_text:
|
||||
if q.valid_datetime_min and q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time between {min} and {max}.'),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif q.valid_datetime_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no earlier than {min}.'),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no later than {max}.'),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.TimeField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=_initial,
|
||||
widget=TimePickerWidget(without_seconds=True),
|
||||
)
|
||||
elif datafield.type == Question.TYPE_DATETIME:
|
||||
help_text = qc.help_text
|
||||
if not help_text:
|
||||
if datafield.valid_datetime_min and datafield.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time between {min} and {max}.'),
|
||||
min=date_format(datafield.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(datafield.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_datetime_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no earlier than {min}.'),
|
||||
min=date_format(datafield.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no later than {max}.'),
|
||||
max=date_format(datafield.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).astimezone(tz)
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
else:
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).astimezone(tz)
|
||||
except dateutil.parser.ParserError:
|
||||
_initial = None
|
||||
else:
|
||||
_initial = None
|
||||
|
||||
field = SplitDateTimeField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=_initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=q.valid_datetime_min,
|
||||
max_date=q.valid_datetime_max
|
||||
),
|
||||
)
|
||||
if q.valid_datetime_min:
|
||||
field.validators.append(MinDateTimeValidator(q.valid_datetime_min))
|
||||
if q.valid_datetime_max:
|
||||
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
|
||||
elif q.type == Question.TYPE_PHONENUMBER:
|
||||
if initial:
|
||||
try:
|
||||
initial = PhoneNumber().from_string(initial.answer)
|
||||
except NumberParseException:
|
||||
initial = None
|
||||
field = SplitDateTimeField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(help_text),
|
||||
initial=_initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=datafield.valid_datetime_min,
|
||||
max_date=datafield.valid_datetime_max
|
||||
),
|
||||
)
|
||||
if datafield.valid_datetime_min:
|
||||
field.validators.append(MinDateTimeValidator(datafield.valid_datetime_min))
|
||||
if datafield.valid_datetime_max:
|
||||
field.validators.append(MaxDateTimeValidator(datafield.valid_datetime_max))
|
||||
elif datafield.type == Question.TYPE_PHONENUMBER:
|
||||
if initial:
|
||||
try:
|
||||
initial = PhoneNumber().from_string(initial.answer)
|
||||
except NumberParseException:
|
||||
initial = None
|
||||
|
||||
if not initial:
|
||||
phone_prefix = guess_phone_prefix_from_request(request, event)
|
||||
if phone_prefix:
|
||||
initial = "+{}.".format(phone_prefix)
|
||||
if not initial:
|
||||
phone_prefix = guess_phone_prefix_from_request(request, event)
|
||||
if phone_prefix:
|
||||
initial = "+{}.".format(phone_prefix)
|
||||
|
||||
field = PhoneNumberField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
|
||||
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
|
||||
# the future.
|
||||
initial=initial,
|
||||
widget=WrappedPhoneNumberPrefixWidget()
|
||||
)
|
||||
field.question = q
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
field = PhoneNumberField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
|
||||
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
|
||||
# the future.
|
||||
initial=initial,
|
||||
widget=WrappedPhoneNumberPrefixWidget()
|
||||
)
|
||||
field.datafield = datafield
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
|
||||
if q.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
|
||||
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values))
|
||||
if q.type != 'M':
|
||||
field.widget.attrs['required'] = q.required and not self.all_optional
|
||||
field._required = q.required and not self.all_optional
|
||||
field.required = False
|
||||
if qc.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = qc.dependency_question_id
|
||||
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(qc.dependency_values))
|
||||
if datafield.type != 'M':
|
||||
field.widget.attrs['required'] = qc.required and not self.all_optional
|
||||
field._required = qc.required and not self.all_optional
|
||||
field.required = False
|
||||
|
||||
add_fields['question_%s' % q.id] = field
|
||||
field_positions.append(('question_%s' % q.id, q.position))
|
||||
|
||||
field_positions.sort(key=lambda e: e[1])
|
||||
for fname, p in field_positions:
|
||||
self.fields[fname] = add_fields[fname]
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=pos)
|
||||
data = pos.meta_info_data
|
||||
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
for key, value in response.items():
|
||||
# We need to be this explicit, since OrderedDict.update does not retain ordering
|
||||
self.fields[key] = value
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if isinstance(v.widget, forms.MultiWidget):
|
||||
for w in v.widget.widgets:
|
||||
autocomplete = w.attrs.get('autocomplete', '')
|
||||
if autocomplete.strip() == "off":
|
||||
w.attrs['autocomplete'] = 'off'
|
||||
else:
|
||||
w.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + autocomplete
|
||||
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
|
||||
autocomplete = v.widget.attrs.get('autocomplete', '')
|
||||
if autocomplete.strip() == "off":
|
||||
v.widget.attrs['autocomplete'] = 'off'
|
||||
else:
|
||||
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + autocomplete
|
||||
|
||||
def clean(self):
|
||||
from pretix.base.addressvalidation import \
|
||||
validate_address # local import to prevent impact on startup time
|
||||
|
||||
d = super().clean()
|
||||
|
||||
if self.address_validation:
|
||||
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
|
||||
|
||||
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
if not d.get('state'):
|
||||
self.add_error('state', _('This field is required.'))
|
||||
return field
|
||||
|
||||
def check_user_questions(self, d):
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
@@ -1123,6 +925,265 @@ class BaseQuestionsForm(forms.Form):
|
||||
if 'question_%d' % q.pk in d and d['question_%d' % q.pk] is False:
|
||||
d['question_%d' % q.pk] = None
|
||||
|
||||
|
||||
class OrderLevelQuestionsForm(BaseQuestionsForm):
|
||||
def __init__(self, container, *args, **kwargs):
|
||||
"""
|
||||
Takes two additional keyword arguments:
|
||||
|
||||
:param checkoutsession: The checkout session the form should be for
|
||||
:param order: The order the form should be for
|
||||
:param event: The event this belongs to
|
||||
"""
|
||||
request = kwargs.pop('request', None)
|
||||
event = kwargs.pop('event')
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# TODO(questionnaires) - switch olq's to questionnaires !
|
||||
questions = Question.objects.filter(
|
||||
event=event, container_type=Question.ContainerType.ORDER,
|
||||
ask_during_checkin=False, hidden=False,
|
||||
).order_by('position')
|
||||
answerlist = container.answers.prefetch_related('options')
|
||||
|
||||
for q in questions:
|
||||
self.fields['question_%s' % q.id] = self.build_user_question_field(request, event, answerlist, q)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
self.check_user_questions(d)
|
||||
return d
|
||||
|
||||
|
||||
class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
"""
|
||||
This form class is responsible for asking ticket-related questions. This includes
|
||||
the attendee name for admission tickets, if the corresponding setting is enabled,
|
||||
as well as additional questions defined by the organizer.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Takes two additional keyword arguments:
|
||||
|
||||
:param cartpos: The cart position the form should be for
|
||||
:param event: The event this belongs to
|
||||
"""
|
||||
request = kwargs.pop('request', None)
|
||||
cartpos = self.cartpos = kwargs.pop('cartpos', None)
|
||||
orderpos = self.orderpos = kwargs.pop('orderpos', None)
|
||||
pos = cartpos or orderpos
|
||||
item = pos.item
|
||||
questionnaires = pos.item.relevant_questionnaires
|
||||
event = kwargs.pop('event')
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||
self.fields['requested_valid_from'] = self.build_requested_valid_from_field(event, pos, item)
|
||||
|
||||
for questionnaire in questionnaires:
|
||||
for child in getattr(questionnaire, 'childlist', questionnaire.children.all()):
|
||||
if child.user_datafield:
|
||||
df = child.user_datafield
|
||||
self.fields['question_%s' % df.id] = self.build_user_question_field(request, event, pos.answerlist, child, df)
|
||||
elif child.system_datafield:
|
||||
self.fields[child.system_datafield] = self.build_system_question_field(request, event, pos, child)
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=pos)
|
||||
data = pos.meta_info_data
|
||||
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
for key, value in response.items():
|
||||
# We need to be this explicit, since OrderedDict.update does not retain ordering
|
||||
self.fields[key] = value
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if isinstance(v.widget, forms.MultiWidget):
|
||||
for w in v.widget.widgets:
|
||||
autocomplete = w.attrs.get('autocomplete', '')
|
||||
if autocomplete.strip() == "off":
|
||||
w.attrs['autocomplete'] = 'off'
|
||||
else:
|
||||
w.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + autocomplete
|
||||
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
|
||||
autocomplete = v.widget.attrs.get('autocomplete', '')
|
||||
if autocomplete.strip() == "off":
|
||||
v.widget.attrs['autocomplete'] = 'off'
|
||||
else:
|
||||
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + autocomplete
|
||||
|
||||
def build_requested_valid_from_field(self, event, pos, item):
|
||||
if item.validity_dynamic_start_choice_day_limit:
|
||||
max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
||||
else:
|
||||
max_date = None
|
||||
min_date = time_machine_now()
|
||||
initial = None
|
||||
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
|
||||
if pos.used_membership.date_start >= time_machine_now():
|
||||
initial = min_date = pos.used_membership.date_start
|
||||
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
|
||||
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
|
||||
attrs = {}
|
||||
if max_date:
|
||||
attrs['data-max'] = max_date.date().isoformat()
|
||||
if min_date:
|
||||
attrs['data-min'] = min_date.date().isoformat()
|
||||
return forms.DateField(
|
||||
label=_('Start date'),
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
validators=([MaxDateValidator(max_date.date())] if max_date else []) + [MinDateValidator(min_date.date())]
|
||||
)
|
||||
else:
|
||||
return forms.SplitDateTimeField(
|
||||
label=_('Start date'),
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=min_date,
|
||||
max_date=max_date
|
||||
),
|
||||
validators=([MaxDateTimeValidator(max_date)] if max_date else []) + [MinDateTimeValidator(min_date)]
|
||||
)
|
||||
|
||||
def build_system_question_field(self, request, event, pos, qc):
|
||||
field_name = qc.system_datafield
|
||||
if field_name == 'attendee_name_parts':
|
||||
return NamePartsFormField(
|
||||
max_length=255,
|
||||
required=qc.required and not self.all_optional,
|
||||
scheme=event.settings.name_scheme,
|
||||
titles=event.settings.name_scheme_titles,
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.attendee_name_parts,
|
||||
)
|
||||
if field_name == 'attendee_email':
|
||||
return forms.EmailField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.attendee_email,
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
'autocomplete': 'email'
|
||||
}
|
||||
)
|
||||
)
|
||||
if field_name == 'company':
|
||||
return forms.CharField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
max_length=255,
|
||||
initial=pos.company,
|
||||
)
|
||||
|
||||
if field_name == 'street':
|
||||
return forms.CharField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
'placeholder': _('Street and Number'),
|
||||
'autocomplete': 'street-address'
|
||||
}),
|
||||
initial=pos.street,
|
||||
)
|
||||
if field_name == 'zipcode':
|
||||
return forms.CharField(
|
||||
required=False,
|
||||
max_length=30,
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.zipcode,
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'postal-code',
|
||||
}),
|
||||
)
|
||||
if field_name == 'city':
|
||||
return forms.CharField(
|
||||
required=False,
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
max_length=255,
|
||||
initial=pos.city,
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
)
|
||||
if field_name == 'country':
|
||||
country = pos.country or guess_country_from_request(request, event)
|
||||
return CountryField(
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=country,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'country',
|
||||
'data-trigger-address-info': 'on',
|
||||
}),
|
||||
)
|
||||
if field_name == 'state':
|
||||
country = pos.country or guess_country_from_request(request, event)
|
||||
c = [('', '---')]
|
||||
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||
cc = None
|
||||
state = None
|
||||
if fprefix + 'country' in self.data:
|
||||
cc = str(self.data[fprefix + 'country'])
|
||||
elif country:
|
||||
cc = str(country)
|
||||
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||
state = (pos.state)
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
field = forms.ChoiceField(
|
||||
label=escape(qc.label),
|
||||
help_text=rich_text(qc.help_text),
|
||||
required=False,
|
||||
choices=c,
|
||||
initial=state,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
)
|
||||
field.widget.is_required = True
|
||||
return field
|
||||
|
||||
def clean(self):
|
||||
from pretix.base.addressvalidation import \
|
||||
validate_address # local import to prevent impact on startup time
|
||||
|
||||
d = super().clean()
|
||||
|
||||
if self.address_validation:
|
||||
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
|
||||
|
||||
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
if not d.get('state'):
|
||||
self.add_error('state', _('This field is required.'))
|
||||
|
||||
self.check_user_questions(d)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
@@ -1201,15 +1262,15 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
if not self.ask_vat_id:
|
||||
del self.fields['vat_id']
|
||||
elif self.validate_vat_id:
|
||||
self.fields['vat_id'].help_text = '<br/>'.join([
|
||||
str(_('Optional, but depending on the country you reside in we might need to charge you '
|
||||
'additional taxes if you do not enter it.')),
|
||||
])
|
||||
self.fields['vat_id'].help_text = _(
|
||||
'Optional, but depending on the country you reside in we might need to charge you '
|
||||
'additional taxes if you do not enter it.'
|
||||
)
|
||||
else:
|
||||
self.fields['vat_id'].help_text = '<br/>'.join([
|
||||
str(_('Optional, but it might be required for you to claim tax benefits on your invoice '
|
||||
'depending on your and the seller’s country of residence.')),
|
||||
])
|
||||
self.fields['vat_id'].help_text = _(
|
||||
'Optional, but it might be required for you to claim tax benefits on your invoice '
|
||||
'depending on your and the seller’s country of residence.'
|
||||
)
|
||||
|
||||
transmission_type_choices = [
|
||||
(t.identifier, t.public_name) for t in get_transmission_types()
|
||||
@@ -1295,8 +1356,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
del self.fields['beneficiary']
|
||||
|
||||
if event.settings.invoice_address_custom_field:
|
||||
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
||||
self.fields['custom_field'].help_text = event.settings.invoice_address_custom_field_helptext
|
||||
self.fields['custom_field'].label = escape(event.settings.invoice_address_custom_field)
|
||||
self.fields['custom_field'].help_text = rich_text(event.settings.invoice_address_custom_field_helptext)
|
||||
else:
|
||||
del self.fields['custom_field']
|
||||
|
||||
@@ -1410,7 +1471,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
pass # Skip re-validation if it is validated
|
||||
elif self.validate_vat_id and vat_id_applicable:
|
||||
try:
|
||||
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
|
||||
requester_id = self.request.event.settings.invoice_address_from_vat_id
|
||||
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')), requester_id)
|
||||
self.instance.vat_id_validated = bool(normalized_id)
|
||||
self.instance.vat_id = data['vat_id'] = normalized_id
|
||||
except VATIDFinalError as e:
|
||||
|
||||
@@ -47,6 +47,7 @@ from pretix.helpers.i18n import (
|
||||
get_format_without_seconds, get_javascript_format,
|
||||
get_javascript_format_without_seconds,
|
||||
)
|
||||
from pretix.helpers.safedownload import get_token
|
||||
|
||||
|
||||
def replace_arabic_numbers(inp):
|
||||
@@ -157,36 +158,26 @@ class TimePickerWidget(forms.TimeInput):
|
||||
|
||||
class UploadedFileWidget(forms.ClearableFileInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.position = kwargs.pop('position')
|
||||
self.event = kwargs.pop('event')
|
||||
self.answer = kwargs.pop('answer')
|
||||
self.request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class FakeFile:
|
||||
def __init__(self, file, position, event, answer):
|
||||
def __init__(self, file, answer, request):
|
||||
self.file = file
|
||||
self.position = position
|
||||
self.event = event
|
||||
self.answer = answer
|
||||
self.request = request
|
||||
|
||||
def __str__(self):
|
||||
return os.path.basename(self.file.name).split('.', 1)[-1]
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
from pretix.base.models import OrderPosition
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
|
||||
if isinstance(self.position, OrderPosition):
|
||||
return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={
|
||||
'order': self.position.order.code,
|
||||
'secret': self.position.order.secret,
|
||||
'answer': self.answer.pk,
|
||||
})
|
||||
token = get_token(self.request, self.answer)
|
||||
if self.request.resolver_match.namespace == 'control':
|
||||
return self.answer.backend_file_url + '?token=' + token
|
||||
else:
|
||||
return eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={
|
||||
'answer': self.answer.pk,
|
||||
})
|
||||
return self.answer.frontend_file_url + '?token=' + token
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
# Browsers can't recognize that the server already has a file uploaded
|
||||
@@ -199,7 +190,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
|
||||
|
||||
def format_value(self, value):
|
||||
if self.is_initial(value):
|
||||
return self.FakeFile(value, self.position, self.event, self.answer)
|
||||
return self.FakeFile(value, self.answer, self.request)
|
||||
|
||||
|
||||
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
@@ -314,3 +305,18 @@ class BusinessBooleanRadio(forms.RadioSelect):
|
||||
'False': False,
|
||||
False: False,
|
||||
}.get(value)
|
||||
|
||||
|
||||
class OptionAttrsSelect(forms.Select):
|
||||
def __init__(self, *args, option_attrs=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.option_attrs = option_attrs or {}
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
option = super().create_option(
|
||||
name, value, label, selected, index, subindex=subindex, attrs=attrs
|
||||
)
|
||||
extra = self.option_attrs.get(str(value))
|
||||
if extra:
|
||||
option["attrs"].update(extra)
|
||||
return option
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.2.16 on 2026-08-05 08:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.helpers.database
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0306_alter_eventmetaproperty_unique_together"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DeviceLastSeen",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("last_seen", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"device",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.device",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="devicelastseen",
|
||||
index=pretix.helpers.database.BrinIndexIgnoredOnSQLite(
|
||||
models.F("last_seen"),
|
||||
autosummarize=True,
|
||||
name="pretixbase_device_last_seen",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 4.2.17 on 2025-01-01 20:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0307_devicelastseen"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CheckoutSession",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("cart_id", models.CharField(max_length=255, unique=True)),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("testmode", models.BooleanField(default=False)),
|
||||
("session_data", models.JSONField(default=dict)),
|
||||
(
|
||||
"customer",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="checkout_sessions",
|
||||
to="pretixbase.customer",
|
||||
),
|
||||
),
|
||||
(
|
||||
"event",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="checkout_sessions",
|
||||
to="pretixbase.event",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sales_channel",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.saleschannel",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="invoiceaddress",
|
||||
name="checkout_session",
|
||||
field=models.OneToOneField(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="invoice_address",
|
||||
to="pretixbase.checkoutsession",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.15 on 2026-08-07 20:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0308_checkoutsession_invoiceaddress_checkout_session'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='container_type',
|
||||
field=models.CharField(default='P', max_length=5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionanswer',
|
||||
name='checkoutsession',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.checkoutsession'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionanswer',
|
||||
name='order',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='pretixbase.order'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='questionanswer',
|
||||
unique_together={('cartposition', 'question'), ('checkoutsession', 'question'), ('order', 'question'), ('orderposition', 'question')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.12 on 2026-08-19 11:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0309_alter_questionanswer_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="question",
|
||||
name="valid_string_length_min",
|
||||
field=models.PositiveIntegerField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,196 @@
|
||||
# Generated by Django 4.2.29 on 2026-03-19 14:24
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from itertools import chain, groupby
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
FakeQuestion = namedtuple(
|
||||
'FakeQuestion', 'id question position required'
|
||||
)
|
||||
|
||||
|
||||
def get_fake_questions(settings):
|
||||
def b(s):
|
||||
return s == 'True'
|
||||
fq = []
|
||||
sqo = json.loads(settings.get('system_question_order', '{}'))
|
||||
_ = LazyI18nString.from_gettext
|
||||
|
||||
if b(settings.get('attendee_names_asked', 'True')):
|
||||
fq.append(FakeQuestion('attendee_name_parts', _('Attendee name'), sqo.get('attendee_name_parts', 0), b(settings.get('attendee_names_required'))))
|
||||
|
||||
if b(settings.get('attendee_emails_asked')):
|
||||
fq.append(FakeQuestion('attendee_email', _('Attendee email'), sqo.get('attendee_email', 0), b(settings.get('attendee_emails_required'))))
|
||||
|
||||
if b(settings.get('attendee_company_asked')):
|
||||
fq.append(FakeQuestion('company', _('Company'), sqo.get('company', 0), b(settings.get('attendee_company_required'))))
|
||||
|
||||
if b(settings.get('attendee_addresses_asked')):
|
||||
fq.append(FakeQuestion('street', _('Street'), sqo.get('street', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('zipcode', _('ZIP code'), sqo.get('zipcode', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('city', _('City'), sqo.get('city', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('country', _('Country'), sqo.get('country', 0), b(settings.get('attendee_addresses_required'))))
|
||||
return fq
|
||||
|
||||
|
||||
def migrate_questions_forward(apps, schema_editor):
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
Item = apps.get_model("pretixbase", "Item")
|
||||
Question = apps.get_model("pretixbase", "Question")
|
||||
Questionnaire = apps.get_model("pretixbase", "Questionnaire")
|
||||
QuestionnaireChild = apps.get_model("pretixbase", "QuestionnaireChild")
|
||||
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||
|
||||
def create_grouped_item_questionnaires(event, children, label_prefix, questionnaire_type):
|
||||
# group by item, creating a unique questionnaire per item
|
||||
item_questionnaires = (([t[3] for t in children], item_id) for item_id, children in
|
||||
groupby(children, key=lambda t: t[0]))
|
||||
|
||||
# group again, merging all questionnaires with identical children
|
||||
merged_questionnaires = groupby(sorted(item_questionnaires, key=lambda t: [q.id for q in t[0]]),
|
||||
key=lambda t: t[0])
|
||||
for children, iterator in merged_questionnaires:
|
||||
items = [item for _c, item in iterator]
|
||||
|
||||
# create questionnaires and children
|
||||
questionnaire = Questionnaire.objects.create(
|
||||
event=event, type=questionnaire_type, position=0, all_sales_channels=True,
|
||||
internal_name=label_prefix + ', '.join(str(iname or name) for (id, iname, name) in items)
|
||||
)
|
||||
questionnaire.items.set([id for (id, iname, name) in items])
|
||||
deps = {}
|
||||
for position, child in enumerate(children):
|
||||
if isinstance(child, FakeQuestion):
|
||||
QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
system_datafield=child.id,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
)
|
||||
else:
|
||||
deps[child.id] = QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
user_datafield=child,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
help_text=child.help_text,
|
||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
||||
dependency_values=child.dependency_values,
|
||||
)
|
||||
|
||||
for event in Event.objects.iterator():
|
||||
# get relevant settings
|
||||
settings = {
|
||||
setting.key: setting.value for setting in EventSettingsStore.objects.filter(object_id=event.id, key__in=(
|
||||
'system_question_order', 'attendee_names_asked', 'attendee_names_required', 'attendee_emails_asked', 'attendee_emails_required',
|
||||
'attendee_company_asked', 'attendee_company_required', 'attendee_addresses_asked', 'attendee_addresses_required',
|
||||
))
|
||||
}
|
||||
|
||||
# get all ticket-level questions (user-defined and system provided), along with the products for which they're asked
|
||||
questions = event.questions.filter(container_type='P')
|
||||
children = sorted(chain((
|
||||
(item, q.position, 0, q)
|
||||
for q in get_fake_questions(settings)
|
||||
for item in event.items.filter(personalized=True).values_list('id', 'internal_name', 'name')
|
||||
), (
|
||||
(item, q.position, q.id, q)
|
||||
for q in questions.filter(hidden=False)
|
||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
||||
)), key=lambda t: (t[0], t[1]))
|
||||
|
||||
create_grouped_item_questionnaires(event, children, '', 'PS')
|
||||
|
||||
children = sorted(chain((
|
||||
(item, q.position, q.id, q)
|
||||
for q in questions.filter(hidden=True)
|
||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
||||
)), key=lambda t: (t[0], t[1]))
|
||||
|
||||
create_grouped_item_questionnaires(event, children, 'Hidden questions for ', 'PH')
|
||||
|
||||
# get all order-level questions
|
||||
questions = list(event.questions.filter(container_type='O', hidden=False).order_by('position'))
|
||||
if questions:
|
||||
# create questionnaires and children
|
||||
questionnaire = Questionnaire.objects.create(
|
||||
event=event, type='OS', position=0, all_sales_channels=True,
|
||||
internal_name='Per-order questions',
|
||||
)
|
||||
deps = {}
|
||||
for position, child in enumerate(questions):
|
||||
deps[child.id] = QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
user_datafield=child,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
help_text=child.help_text,
|
||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
||||
dependency_values=child.dependency_values,
|
||||
)
|
||||
|
||||
|
||||
def migrate_questions_backward(apps, schema_editor):
|
||||
pass # as long as we don't delete the old columns, this is a no op. after that, it gets complicated...
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0309_alter_questionanswer_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Questionnaire',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('internal_name', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=5)),
|
||||
('position', models.PositiveIntegerField(default=0)),
|
||||
('all_sales_channels', models.BooleanField(default=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questionnaires', to='pretixbase.event')),
|
||||
('items', models.ManyToManyField(related_name='questionnaires', to='pretixbase.item')),
|
||||
('limit_sales_channels', models.ManyToManyField(to='pretixbase.saleschannel')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='QuestionnaireChild',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('position', models.PositiveIntegerField(default=0)),
|
||||
('system_datafield', models.CharField(max_length=25, null=True)),
|
||||
('required', models.BooleanField(default=False)),
|
||||
('label', i18nfield.fields.I18nTextField()),
|
||||
('help_text', i18nfield.fields.I18nTextField(null=True)),
|
||||
('dependency_values', pretix.base.models.fields.MultiStringField(default=[])),
|
||||
('dependency_question', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dependent_questions', to='pretixbase.questionnairechild')),
|
||||
('questionnaire', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pretixbase.questionnaire')),
|
||||
('user_datafield', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='references', to='pretixbase.question')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.RunPython(
|
||||
migrate_questions_forward,
|
||||
migrate_questions_backward,
|
||||
),
|
||||
# TODO(questionnaires) remove old columns from Question model
|
||||
]
|
||||
@@ -37,8 +37,8 @@ from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
|
||||
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
|
||||
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
|
||||
itempicture_upload_to,
|
||||
Questionnaire, QuestionnaireChild, QuestionOption, Quota, SubEventItem,
|
||||
SubEventItemVariation, itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .mail import OutgoingMail
|
||||
|
||||
@@ -33,6 +33,7 @@ from django.db.models.aggregates import Sum
|
||||
from django.db.models.expressions import OuterRef, Subquery
|
||||
from django.db.models.functions.comparison import Coalesce
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
@@ -409,6 +410,17 @@ class AttendeeProfile(models.Model):
|
||||
|
||||
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
|
||||
|
||||
@cached_property
|
||||
def answers_key_to_index(self):
|
||||
return {a.get('field_name'): i for i, a in enumerate(self.answers)}
|
||||
|
||||
def store_answer(self, answer_dict):
|
||||
k = answer_dict['field_name']
|
||||
if k in self.answers_key_to_index:
|
||||
self.answers[self.answers_key_to_index[k]] = answer_dict
|
||||
else:
|
||||
self.answers.append(answer_dict)
|
||||
|
||||
|
||||
def generate_client_id():
|
||||
return get_random_string(40)
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import string
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
@@ -32,6 +34,7 @@ from pretix.base.models import LoggedModel
|
||||
from pretix.base.permissions import (
|
||||
AnyPermissionOf, assert_valid_event_permission,
|
||||
)
|
||||
from pretix.helpers import BrinIndexIgnoredOnSQLite
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@@ -287,3 +290,27 @@ class Device(LoggedModel):
|
||||
return self.get_events_with_any_permission()
|
||||
else:
|
||||
return self.organizer.events.none()
|
||||
|
||||
|
||||
class DeviceLastSeen(models.Model):
|
||||
# This is a separate model since we expect it to get A LOT of writes and PostgreSQL always
|
||||
# writes full rows and then needs to update all indexes on the row, so this is going to save a
|
||||
# lot of write traffic on the databse
|
||||
device = models.OneToOneField("Device", on_delete=models.CASCADE, related_name="last_seen")
|
||||
last_seen = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
BrinIndexIgnoredOnSQLite(
|
||||
# BRIN indexes are highly efficient on lots of updates, especially of chronological data
|
||||
# and especially if we later want to query them by range, as we likely want to.
|
||||
"last_seen",
|
||||
name="pretixbase_device_last_seen",
|
||||
autosummarize=True
|
||||
)
|
||||
]
|
||||
|
||||
@property
|
||||
def is_recent(self):
|
||||
# pretixSCAN/pretixPOS sync every 5 minutes, so 7 minutes can be considered "offline"
|
||||
return now() - self.last_seen < timedelta(minutes=7)
|
||||
|
||||
@@ -736,7 +736,7 @@ class Event(EventMixin, LoggedModel):
|
||||
self.settings.mail_send_order_paid_attendee = True
|
||||
self.settings.mail_send_order_approved_attendee = True
|
||||
self.settings.mail_send_order_approved_free_attendee = True
|
||||
self.settings.mail_text_download_reminder_attendee = True
|
||||
self.settings.mail_send_download_reminder_attendee = True
|
||||
|
||||
@property
|
||||
def social_image(self):
|
||||
|
||||
+141
-17
@@ -50,7 +50,7 @@ from dateutil.tz import datetime_exists
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import (
|
||||
MaxLengthValidator, MinValueValidator, RegexValidator,
|
||||
MaxLengthValidator, MinLengthValidator, MinValueValidator, RegexValidator,
|
||||
)
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
@@ -1569,10 +1569,12 @@ class ItemBundle(models.Model):
|
||||
|
||||
class Question(LoggedModel):
|
||||
"""
|
||||
A question is an input field that can be used to extend a ticket by custom information,
|
||||
e.g. "Attendee age". The answers are found next to the position. The answers may be found
|
||||
in QuestionAnswers, attached to OrderPositions/CartPositions. A question can allow one of
|
||||
several input types, currently:
|
||||
A question is a data field that can be used to extend an order or a ticket by custom
|
||||
information, e.g. "Attendee age". To be actually useful, questions need to be added to
|
||||
one or multiple Questionnaires. The answers may be found in QuestionAnswers, attached
|
||||
to Orders, OrderPositions or CartPositions.
|
||||
|
||||
A question can allow one of several input types, currently:
|
||||
|
||||
* a number (``TYPE_NUMBER``)
|
||||
* a one-line string (``TYPE_STRING``)
|
||||
@@ -1592,7 +1594,7 @@ class Question(LoggedModel):
|
||||
:param required: Whether answering this question is required for submitting an order including
|
||||
items associated with this question.
|
||||
:type required: bool
|
||||
:param items: A set of ``Items`` objects that this question should be applied to
|
||||
:param items: TO BE REMOVED
|
||||
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
||||
:type ask_during_checkin: bool
|
||||
:param show_during_checkin: Whether to show the answer to this question during check-in.
|
||||
@@ -1606,6 +1608,9 @@ class Question(LoggedModel):
|
||||
:param dependency_values: The values that `dependency_question` needs to be set to for this question to be applicable.
|
||||
:type dependency_values: list[str]
|
||||
"""
|
||||
class ContainerType(models.TextChoices):
|
||||
ORDER = "O", _("Order")
|
||||
ORDERPOSITION = "P", _("Order position")
|
||||
TYPE_NUMBER = "N"
|
||||
TYPE_STRING = "S"
|
||||
TYPE_TEXT = "T"
|
||||
@@ -1641,7 +1646,14 @@ class Question(LoggedModel):
|
||||
related_name="questions",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
container_type = models.CharField(
|
||||
max_length=5,
|
||||
choices=ContainerType.choices,
|
||||
verbose_name=_("Asked on"),
|
||||
default=ContainerType.ORDERPOSITION,
|
||||
)
|
||||
question = I18nTextField(
|
||||
# TODO(questionnaires) : to be renamed to 'internal_name'
|
||||
verbose_name=_("Question")
|
||||
)
|
||||
identifier = models.CharField(
|
||||
@@ -1657,6 +1669,7 @@ class Question(LoggedModel):
|
||||
],
|
||||
)
|
||||
help_text = I18nTextField(
|
||||
# TODO(questionnaires) : to be removed
|
||||
verbose_name=_("Help text"),
|
||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
null=True, blank=True,
|
||||
@@ -1666,22 +1679,22 @@ class Question(LoggedModel):
|
||||
choices=TYPE_CHOICES,
|
||||
verbose_name=_("Question type")
|
||||
)
|
||||
required = models.BooleanField(
|
||||
required = models.BooleanField( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
default=False,
|
||||
verbose_name=_("Required question")
|
||||
)
|
||||
items = models.ManyToManyField(
|
||||
items = models.ManyToManyField( # TODO(questionnaires) : to be removed, -> Questionnaire
|
||||
Item,
|
||||
related_name='questions',
|
||||
verbose_name=_("Products"),
|
||||
blank=True,
|
||||
help_text=_('This question will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
position = models.PositiveIntegerField( # TODO(questionnaires) : to be removed, -> Questionnaire + QuestionnaireChild
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
ask_during_checkin = models.BooleanField(
|
||||
ask_during_checkin = models.BooleanField( # TODO(questionnaires) : to be removed
|
||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
@@ -1691,7 +1704,7 @@ class Question(LoggedModel):
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
hidden = models.BooleanField(
|
||||
hidden = models.BooleanField( # to be removed
|
||||
verbose_name=_('Hidden question'),
|
||||
help_text=_('This question will only show up in the backend.'),
|
||||
default=False
|
||||
@@ -1700,10 +1713,10 @@ class Question(LoggedModel):
|
||||
verbose_name=_('Print answer on invoices'),
|
||||
default=False
|
||||
)
|
||||
dependency_question = models.ForeignKey(
|
||||
dependency_question = models.ForeignKey( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_values = MultiStringField(default=[])
|
||||
dependency_values = MultiStringField(default=[]) # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
valid_number_min = models.DecimalField(decimal_places=6, max_digits=30, null=True, blank=True,
|
||||
verbose_name=_('Minimum value'),
|
||||
help_text=_('Currently not supported in our apps and during check-in'))
|
||||
@@ -1722,6 +1735,11 @@ class Question(LoggedModel):
|
||||
valid_datetime_max = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_('Maximum value'),
|
||||
help_text=_('Currently not supported in our apps and during check-in'))
|
||||
valid_string_length_min = models.PositiveIntegerField(null=True, blank=True,
|
||||
verbose_name=_('Minimum length'),
|
||||
help_text=_(
|
||||
'Currently not supported in our apps and during check-in'
|
||||
))
|
||||
valid_string_length_max = models.PositiveIntegerField(null=True, blank=True,
|
||||
verbose_name=_('Maximum length'),
|
||||
help_text=_(
|
||||
@@ -1737,9 +1755,9 @@ class Question(LoggedModel):
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Question")
|
||||
verbose_name_plural = _("Questions")
|
||||
ordering = ('position', 'id')
|
||||
verbose_name = _("Data field")
|
||||
verbose_name_plural = _("Data fields")
|
||||
ordering = ('question', 'id')
|
||||
unique_together = (('event', 'identifier'),)
|
||||
|
||||
def __str__(self):
|
||||
@@ -1876,6 +1894,11 @@ class Question(LoggedModel):
|
||||
else:
|
||||
raise ValidationError(_('Unknown country code.'))
|
||||
elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT):
|
||||
if self.valid_string_length_min is not None and len(answer) < self.valid_string_length_min:
|
||||
raise ValidationError(MinLengthValidator.message % {
|
||||
'limit_value': self.valid_string_length_min,
|
||||
'show_value': len(answer)
|
||||
})
|
||||
if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max:
|
||||
raise ValidationError(MaxLengthValidator.message % {
|
||||
'limit_value': self.valid_string_length_max,
|
||||
@@ -1885,7 +1908,7 @@ class Question(LoggedModel):
|
||||
return answer
|
||||
|
||||
@staticmethod
|
||||
def clean_items(event, items):
|
||||
def clean_items(event, items): # TODO(questionnaires) : remove method / move to qc
|
||||
for item in items:
|
||||
if event != item.event:
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
@@ -1897,6 +1920,8 @@ class Question(LoggedModel):
|
||||
raise ValidationError(_("The maximum date must not be before the minimum value."))
|
||||
if self.valid_number_max and self.valid_number_min and self.valid_number_min > self.valid_number_max:
|
||||
raise ValidationError(_("The maximum value must not be lower than the minimum value."))
|
||||
if self.valid_string_length_max and self.valid_string_length_min and self.valid_string_length_min > self.valid_string_length_max:
|
||||
raise ValidationError(_("The maximum length must not be shorter than the minimum length."))
|
||||
super().clean()
|
||||
|
||||
def clean_type_change(self, old_type, new_type):
|
||||
@@ -1964,6 +1989,105 @@ class QuestionOption(models.Model):
|
||||
ordering = ('position', 'id')
|
||||
|
||||
|
||||
class Questionnaire(LoggedModel):
|
||||
TYPE_ORDER_SALE = "OS"
|
||||
TYPE_ORDER_POSITION_SALE = "PS"
|
||||
TYPE_ORDER_POSITION_ATTENDEE_ONLY = "PA"
|
||||
TYPE_ORDER_POSITION_CHECKIN = "PC"
|
||||
TYPE_ORDER_POSITION_HIDDEN = "PH"
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_ORDER_SALE, _("Order-wide, before purchase")),
|
||||
(TYPE_ORDER_POSITION_SALE, _("Per product, before purchase")),
|
||||
(TYPE_ORDER_POSITION_ATTENDEE_ONLY, _("Per product, via attendee link")),
|
||||
(TYPE_ORDER_POSITION_CHECKIN, _("Per product, at check-in")),
|
||||
(TYPE_ORDER_POSITION_HIDDEN, _("Per product, hidden")),
|
||||
)
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
related_name="questionnaires",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
internal_name = models.CharField(
|
||||
verbose_name=_("Internal name"),
|
||||
max_length=255,
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=5,
|
||||
choices=TYPE_CHOICES,
|
||||
verbose_name=_("Questionnaire type")
|
||||
)
|
||||
items = models.ManyToManyField(
|
||||
Item,
|
||||
related_name='questionnaires',
|
||||
verbose_name=_("Products"),
|
||||
blank=True,
|
||||
help_text=_('This questionnaire will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels the product is sold on"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
||||
'selected here but not on product level, the variation will not be available.'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
|
||||
class QuestionnaireChild(LoggedModel):
|
||||
SYSTEM_QUESTION_CHOICES = (
|
||||
('attendee_name_parts', _('Attendee name')),
|
||||
('attendee_email', _('Attendee email')),
|
||||
('company', _('Company')),
|
||||
('street', _('Street')),
|
||||
('zipcode', _('ZIP code')),
|
||||
('city', _('City')),
|
||||
('country', _('Country')),
|
||||
)
|
||||
questionnaire = models.ForeignKey(
|
||||
Questionnaire,
|
||||
related_name="children",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
user_datafield = models.ForeignKey(
|
||||
Question,
|
||||
related_name="references",
|
||||
on_delete=models.CASCADE,
|
||||
null=True, blank=True,
|
||||
)
|
||||
system_datafield = models.CharField(
|
||||
max_length=25,
|
||||
choices=SYSTEM_QUESTION_CHOICES,
|
||||
null=True, blank=True,
|
||||
)
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Required question")
|
||||
)
|
||||
label = I18nTextField(
|
||||
verbose_name=_("Question")
|
||||
)
|
||||
help_text = I18nTextField(
|
||||
verbose_name=_("Help text"),
|
||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
dependency_question = models.ForeignKey(
|
||||
'QuestionnaireChild', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_values = MultiStringField(default=[])
|
||||
|
||||
|
||||
class Quota(LoggedModel):
|
||||
"""
|
||||
A quota is a "pool of tickets". It is there to limit the number of items
|
||||
|
||||
@@ -626,42 +626,14 @@ class Order(LockModel, LoggedModel):
|
||||
self.save(update_fields=['last_modified'])
|
||||
|
||||
def set_expires(self, now_dt=None, subevents=None):
|
||||
now_dt = now_dt or now()
|
||||
tz = ZoneInfo(self.event.settings.timezone)
|
||||
mode = self.event.settings.get('payment_term_mode')
|
||||
if mode == 'days':
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
|
||||
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
if self.event.settings.get('payment_term_weekdays'):
|
||||
if exp_by_date.weekday() == 5:
|
||||
exp_by_date += timedelta(days=2)
|
||||
elif exp_by_date.weekday() == 6:
|
||||
exp_by_date += timedelta(days=1)
|
||||
elif mode == 'minutes':
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get('payment_term_minutes', as_type=int))
|
||||
else:
|
||||
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
|
||||
from pretix.base.services.payment import compute_payment_deadline
|
||||
|
||||
self.expires = exp_by_date
|
||||
|
||||
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if term_last:
|
||||
if self.event.has_subevents and subevents:
|
||||
terms = [
|
||||
term_last.datetime(se).date()
|
||||
for se in subevents
|
||||
]
|
||||
if not terms:
|
||||
return
|
||||
term_last = min(terms)
|
||||
else:
|
||||
term_last = term_last.datetime(self.event).date()
|
||||
term_last = make_aware(datetime.combine(
|
||||
term_last,
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
if term_last < self.expires:
|
||||
self.expires = term_last
|
||||
self.expires = compute_payment_deadline(
|
||||
event=self.event,
|
||||
sales_channel=self.sales_channel,
|
||||
now_dt=now_dt,
|
||||
subevents=subevents,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def tax_total(self):
|
||||
@@ -1304,10 +1276,9 @@ class Order(LockModel, LoggedModel):
|
||||
|
||||
def answerfile_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
||||
event = (instance.cartposition if instance.cartposition else instance.orderposition.order).event
|
||||
return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format(
|
||||
org=event.organizer.slug,
|
||||
ev=event.slug,
|
||||
org=instance.event.organizer.slug,
|
||||
ev=instance.event.slug,
|
||||
secret=secret,
|
||||
filename=escape_uri_path(filename),
|
||||
)
|
||||
@@ -1336,6 +1307,14 @@ class QuestionAnswer(models.Model):
|
||||
'CartPosition', null=True, blank=True,
|
||||
related_name='answers', on_delete=models.CASCADE
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
'Order', null=True, blank=True,
|
||||
related_name='answers', on_delete=models.CASCADE
|
||||
)
|
||||
checkoutsession = models.ForeignKey(
|
||||
'CheckoutSession', null=True, blank=True,
|
||||
related_name='answers', on_delete=models.CASCADE
|
||||
)
|
||||
question = models.ForeignKey(
|
||||
Question, related_name='answers', on_delete=models.CASCADE
|
||||
)
|
||||
@@ -1351,16 +1330,21 @@ class QuestionAnswer(models.Model):
|
||||
objects = ScopedManager(organizer='question__event__organizer')
|
||||
|
||||
class Meta:
|
||||
unique_together = [['orderposition', 'question'], ['cartposition', 'question']]
|
||||
unique_together = [
|
||||
['orderposition', 'question'],
|
||||
['cartposition', 'question'],
|
||||
['order', 'question'],
|
||||
['checkoutsession', 'question'],
|
||||
]
|
||||
|
||||
@property
|
||||
def backend_file_url(self):
|
||||
if self.file:
|
||||
if self.orderposition:
|
||||
if self.associated_order:
|
||||
return reverse('control:event.order.download.answer', kwargs={
|
||||
'code': self.orderposition.order.code,
|
||||
'event': self.orderposition.order.event.slug,
|
||||
'organizer': self.orderposition.order.event.organizer.slug,
|
||||
'code': self.associated_order.code,
|
||||
'event': self.associated_order.event.slug,
|
||||
'organizer': self.associated_order.event.organizer.slug,
|
||||
'answer': self.pk,
|
||||
})
|
||||
return ""
|
||||
@@ -1370,14 +1354,14 @@ class QuestionAnswer(models.Model):
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
|
||||
if self.file:
|
||||
if self.orderposition:
|
||||
url = eventreverse(self.orderposition.order.event, 'presale:event.order.download.answer', kwargs={
|
||||
'order': self.orderposition.order.code,
|
||||
'secret': self.orderposition.order.secret,
|
||||
if self.associated_order:
|
||||
url = eventreverse(self.associated_order.event, 'presale:event.order.download.answer', kwargs={
|
||||
'order': self.associated_order.code,
|
||||
'secret': self.associated_order.secret,
|
||||
'answer': self.pk,
|
||||
})
|
||||
else:
|
||||
url = eventreverse(self.cartposition.event, 'presale:event.cart.download.answer', kwargs={
|
||||
url = eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={
|
||||
'answer': self.pk,
|
||||
})
|
||||
|
||||
@@ -1392,6 +1376,24 @@ class QuestionAnswer(models.Model):
|
||||
def file_name(self):
|
||||
return self.file.name.split('.', 1)[-1]
|
||||
|
||||
@property
|
||||
def associated_order(self):
|
||||
if self.orderposition:
|
||||
return self.orderposition.order
|
||||
elif self.order:
|
||||
return self.order
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
if self.orderposition:
|
||||
return self.orderposition.order.event
|
||||
elif self.cartposition:
|
||||
return self.cartposition.event
|
||||
elif self.order:
|
||||
return self.order.event
|
||||
elif self.checkoutsession:
|
||||
return self.checkoutsession.event
|
||||
|
||||
def __str__(self):
|
||||
return self.to_string(use_cached=True)
|
||||
|
||||
@@ -1447,6 +1449,12 @@ class QuestionAnswer(models.Model):
|
||||
else:
|
||||
return self.answer
|
||||
|
||||
def to_dependency_values(self):
|
||||
if self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
return [o.identifier for o in self.options.all()]
|
||||
elif self.question.type in (Question.TYPE_BOOLEAN, Question.TYPE_COUNTRYCODE):
|
||||
return self.answer
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.orderposition and self.cartposition:
|
||||
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
||||
@@ -1591,53 +1599,80 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
|
||||
def cache_answers(self, all=True):
|
||||
"""
|
||||
Creates two properties on the object.
|
||||
(1) answ: a dictionary of question.id → answer string
|
||||
(2) questions: a list of Question objects, extended by an 'answer' property
|
||||
Creates a new property on the object:
|
||||
questions: a list of Question objects, extended by an 'answer' property
|
||||
"""
|
||||
self.answ = {}
|
||||
for a in getattr(self, 'answerlist', self.answers.all()): # use prefetch_related cache from get_cart
|
||||
self.answ[a.question_id] = a
|
||||
|
||||
# We need to clone our question objects, otherwise we will override the cached
|
||||
# answers of other items in the same cart if the question objects have been
|
||||
# selected via prefetch_related
|
||||
if not all:
|
||||
if hasattr(self.item, 'questions_to_ask'):
|
||||
questions = list(copy.copy(q) for q in self.item.questions_to_ask)
|
||||
if hasattr(self.item, 'relevant_questionnaires'):
|
||||
children = list(copy.copy(qc) for qq in self.item.relevant_questionnaires for qc in qq.childlist)
|
||||
else:
|
||||
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False,
|
||||
hidden=False))
|
||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type='PS') for qc in qq.children.all())
|
||||
else:
|
||||
questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type__startswith='P') for qc in qq.children.all())
|
||||
|
||||
question_cache = {
|
||||
q.pk: q for q in questions
|
||||
qc_cache = {
|
||||
q.pk: q for q in children
|
||||
}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in question_cache:
|
||||
def qc_is_visible(parentid, qvals):
|
||||
if parentid not in qc_cache:
|
||||
return False
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||
parentqc = qc_cache[parentid]
|
||||
if parentqc.dependency_question_id and not qc_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
||||
return False
|
||||
if parentid not in self.answ:
|
||||
return False
|
||||
return (
|
||||
('True' in qvals and self.answ[parentid].answer == 'True')
|
||||
or ('False' in qvals and self.answ[parentid].answer == 'False')
|
||||
or (any(qval in [o.identifier for o in self.answ[parentid].options.all()] for qval in qvals))
|
||||
)
|
||||
answer_values = self.get_dependency_answer_values(parentqc)
|
||||
return any(qval in answer_values for qval in qvals)
|
||||
|
||||
self.questions = []
|
||||
for q in questions:
|
||||
if q.id in self.answ:
|
||||
q.answer = self.answ[q.id]
|
||||
q.answer.question = q # cache object
|
||||
for qc in children:
|
||||
if qc.user_datafield_id and qc.user_datafield_id in self.answer_cache:
|
||||
qc.answer = self.answer_cache[qc.user_datafield_id]
|
||||
#qc.answer.question = qc # cache object
|
||||
elif qc.system_datafield:
|
||||
qc.answer = self.get_system_answer(qc.system_datafield)
|
||||
#qc.answer.question = qc # cache object
|
||||
else:
|
||||
q.answer = ""
|
||||
if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values):
|
||||
self.questions.append(q)
|
||||
qc.answer = ""
|
||||
if not qc.dependency_question_id or qc_is_visible(qc.dependency_question_id, qc.dependency_values):
|
||||
self.questions.append(qc)
|
||||
|
||||
@cached_property
|
||||
def answer_cache(self):
|
||||
return {
|
||||
aw.question_id: aw for aw in getattr(self, 'answerlist', self.answers.all())
|
||||
}
|
||||
|
||||
def get_dependency_answer_values(self, qc):
|
||||
if qc.user_datafield_id:
|
||||
if qc.user_datafield_id not in self.answer_cache:
|
||||
return None
|
||||
answer = self.answer_cache[qc.user_datafield_id]
|
||||
return answer.to_dependency_values()
|
||||
elif qc.system_datafield:
|
||||
return [self.get_system_answer(qc.system_datafield)]
|
||||
else:
|
||||
raise ValueError('Questionnaire child without datafield has no answer')
|
||||
|
||||
def get_system_answer(self, system_datafield_name):
|
||||
if system_datafield_name == 'attendee_name_parts':
|
||||
return self.attendee_name_parts
|
||||
elif system_datafield_name == 'attendee_email':
|
||||
return self.attendee_email
|
||||
elif system_datafield_name == 'street':
|
||||
return self.street
|
||||
elif system_datafield_name == 'zipcode':
|
||||
return self.zipcode
|
||||
elif system_datafield_name == 'city':
|
||||
return self.city
|
||||
elif system_datafield_name == 'state':
|
||||
return self.state
|
||||
elif system_datafield_name == 'country':
|
||||
return self.country
|
||||
else:
|
||||
raise ValueError('Unknown system question name')
|
||||
|
||||
@property
|
||||
def net_price(self):
|
||||
@@ -3177,6 +3212,39 @@ class Transaction(models.Model):
|
||||
return self.tax_value_includes_rounding_correction * self.count
|
||||
|
||||
|
||||
class CheckoutSession(models.Model):
|
||||
"""
|
||||
A checkout session optionally bundles cart positions with additional information. This is historically
|
||||
not required in pretix and currently only used in the Storefront API.
|
||||
"""
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
verbose_name=_("Event"),
|
||||
related_name="checkout_sessions",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
cart_id = models.CharField(
|
||||
max_length=255, unique=True,
|
||||
verbose_name=_("Cart ID (e.g. session key)"),
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
verbose_name=_("Date"),
|
||||
auto_now_add=True,
|
||||
)
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
related_name='checkout_sessions',
|
||||
null=True, blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
sales_channel = models.ForeignKey(
|
||||
"SalesChannel",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
testmode = models.BooleanField(default=False)
|
||||
session_data = models.JSONField(default=dict)
|
||||
|
||||
|
||||
class CartPosition(AbstractPosition):
|
||||
"""
|
||||
A cart position is similar to an order line, except that it is not
|
||||
@@ -3381,6 +3449,13 @@ class CartPosition(AbstractPosition):
|
||||
|
||||
class InvoiceAddress(models.Model):
|
||||
last_modified = models.DateTimeField(auto_now=True)
|
||||
checkout_session = models.OneToOneField(
|
||||
CheckoutSession,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='invoice_address',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
|
||||
@@ -72,7 +72,7 @@ from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.money import DecimalTextInput
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
from pretix.presale.views import get_cart
|
||||
from pretix.presale.views import get_cart_positions
|
||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -330,9 +330,24 @@ class BasePaymentProvider:
|
||||
payment method. This returns ``False`` by default which is no guarantee that
|
||||
aborting a pending payment can never happen, it just hides the frontend button
|
||||
to avoid users accidentally committing double payments.
|
||||
If the decision doesn't depend on the specific payment, then only implementing
|
||||
``abort_pending_allowed`` is enough, ``payment_abort_pending_allowed(payment: OrderPayment)``
|
||||
is expected to take this into account.
|
||||
As a consumer only evaluate ``payment_abort_pending_allowed(payment: OrderPayment)``
|
||||
to check if aborting this pending payment is possible.
|
||||
"""
|
||||
return False
|
||||
|
||||
def _payment_abort_pending_allowed(self, payment: OrderPayment) -> bool:
|
||||
"""
|
||||
Experimental: This might change during upcomming releases.
|
||||
Whether or not a user can abort a payment in pending state to switch to another
|
||||
payment method. This returns ``self.abort_pending_allowed`` by default which is
|
||||
no guarantee that aborting a pending payment can never happen, it just hides the
|
||||
frontend button to avoid users accidentally committing double payments.
|
||||
"""
|
||||
return self.abort_pending_allowed
|
||||
|
||||
@property
|
||||
def requires_invoice_immediately(self):
|
||||
"""
|
||||
@@ -1019,10 +1034,12 @@ class BasePaymentProvider:
|
||||
On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method).
|
||||
On failure, you should raise a PaymentException.
|
||||
"""
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_PENDING and not self.abort_pending_allowed:
|
||||
raise PaymentException(_(
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
))
|
||||
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_PENDING:
|
||||
if not self._payment_abort_pending_allowed(payment):
|
||||
raise PaymentException(_(
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
))
|
||||
|
||||
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||
payment.save(update_fields=['state'])
|
||||
@@ -1148,7 +1165,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||
from .services.cart import get_fees
|
||||
|
||||
cart = get_cart(request)
|
||||
cart = get_cart_positions(request)
|
||||
|
||||
try:
|
||||
fees = get_fees(event=request.event, request=request,
|
||||
@@ -1416,7 +1433,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
for p in cs.get('payments', [])
|
||||
if p.get('info_data', {}).get('gift_card')
|
||||
]
|
||||
positions = get_cart(request)
|
||||
positions = get_cart_positions(request)
|
||||
testmode = self.event.testmode
|
||||
else:
|
||||
used_cards = []
|
||||
|
||||
+16
-25
@@ -1232,9 +1232,7 @@ class Renderer:
|
||||
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = self.bg_pdf.pages[i]
|
||||
_correct_page_media_box(bg_page)
|
||||
page.merge_page(bg_page, over=False)
|
||||
output.add_page(page)
|
||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
@@ -1299,39 +1297,32 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
|
||||
bg_pdf.write(bg_filename)
|
||||
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
||||
else:
|
||||
output = PdfWriter()
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = bg_pdf.pages[i]
|
||||
_correct_page_media_box(bg_page)
|
||||
page.merge_page(bg_page, over=False)
|
||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
fg_pdf.pdf_header = bg_pdf.pdf_header
|
||||
|
||||
fg_pdf.write(out_file)
|
||||
output.pdf_header = (
|
||||
bg_pdf.pdf_header
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:])
|
||||
else fg_pdf.pdf_header
|
||||
)
|
||||
output.write(out_file)
|
||||
|
||||
|
||||
def _correct_page_media_box(page: pypdf.PageObject):
|
||||
if page.rotation != 0:
|
||||
page.transfer_rotation_to_content()
|
||||
media_box = page.mediabox
|
||||
def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject):
|
||||
if bg_page.rotation != 0:
|
||||
bg_page.transfer_rotation_to_content()
|
||||
media_box = bg_page.mediabox
|
||||
trsf = pypdf.Transformation()
|
||||
if media_box.bottom != 0:
|
||||
trsf = trsf.translate(0, -media_box.bottom)
|
||||
if media_box.left != 0:
|
||||
trsf = trsf.translate(-media_box.left, 0)
|
||||
page.add_transformation(trsf, False)
|
||||
for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]:
|
||||
if b in page:
|
||||
rr = pypdf.generic.RectangleObject(page[b])
|
||||
pt1 = trsf.apply_on(rr.lower_left)
|
||||
pt2 = trsf.apply_on(rr.upper_right)
|
||||
page[pypdf.generic.NameObject(b)] = pypdf.generic.RectangleObject((
|
||||
min(pt1[0], pt2[0]),
|
||||
min(pt1[1], pt2[1]),
|
||||
max(pt1[0], pt2[0]),
|
||||
max(pt1[1], pt2[1]),
|
||||
))
|
||||
|
||||
fg_page = output.add_page(fg_page)
|
||||
fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False)
|
||||
|
||||
|
||||
@deconstructible
|
||||
|
||||
+373
-155
@@ -19,30 +19,245 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import copy
|
||||
import datetime
|
||||
import os
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from typing import Union
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING, Iterable, List, Literal, Optional, Tuple, Union, cast,
|
||||
)
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from dateutil import parser
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.forms.widgets import Widget
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.functional import Promise, lazy
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
BASE_CHOICES = (
|
||||
('date_from', _('Event start')),
|
||||
('date_to', _('Event end')),
|
||||
('date_admission', _('Event admission')),
|
||||
('presale_start', _('Presale start')),
|
||||
('presale_end', _('Presale end')),
|
||||
)
|
||||
from pretix.base.forms.widgets import OptionAttrsSelect
|
||||
|
||||
RelativeDate = namedtuple('RelativeDate', ['days', 'minutes', 'time', 'is_after', 'base_date_name'], defaults=(0, None, None, False, 'date_from'))
|
||||
if TYPE_CHECKING:
|
||||
from .models import Event, Order, SubEvent
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BaseChoice:
|
||||
base: Literal["event", "order"]
|
||||
attribute: str
|
||||
text: Promise
|
||||
supports_before: bool
|
||||
supports_after: bool
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
key = f"{self.base}__{self.attribute}"
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def find(objects: Iterable["BaseChoice"], key: str) -> "BaseChoice":
|
||||
if "__" in key:
|
||||
choice = next((obj for obj in objects if obj.key == key), None)
|
||||
else:
|
||||
# fallback for RelativeDateFields stored, before support for bases other than event was added
|
||||
choice = next((obj for obj in objects if obj.attribute == key and obj.base == "event"), None)
|
||||
|
||||
if choice is None:
|
||||
raise TypeError(f"key {key} must be a valid key in BASE_CHOICES")
|
||||
|
||||
return choice
|
||||
|
||||
|
||||
BASE_CHOICES: List[BaseChoice] = [
|
||||
BaseChoice('event', 'date_from', _('Event start'), True, True),
|
||||
BaseChoice('event', 'date_to', _('Event end'), True, True),
|
||||
BaseChoice('event', 'date_admission', _('Event admission'), True, True),
|
||||
BaseChoice('event', 'presale_start', _('Presale start'), True, True),
|
||||
BaseChoice('event', 'presale_end', _('Presale end'), True, True),
|
||||
BaseChoice('order', 'datetime', _('Order creation'), False, True),
|
||||
BaseChoice('order', 'expires', _('Order expiry'), True, True),
|
||||
]
|
||||
|
||||
EVENT_BASE_CHOICES = [
|
||||
x for x in BASE_CHOICES if x.base == 'event'
|
||||
]
|
||||
|
||||
ORDER_BASE_CHOICES = [
|
||||
x for x in BASE_CHOICES if x.base == 'order'
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RelativeDate:
|
||||
"""
|
||||
This contains information on a date that is defined in relation to a fixed base point.
|
||||
This means that the underlying data is a fixed date as the base point and a number of days or a time interval
|
||||
to calculate the date.
|
||||
|
||||
The list of valid base date choices is defined in BASE_CHOICES.
|
||||
If the base_date_name is not set, the date_from attribute of Event is used.
|
||||
"""
|
||||
|
||||
days: int = 0
|
||||
minutes: Optional[int] = None
|
||||
time: Optional[datetime.time] = None
|
||||
is_after: bool = False
|
||||
base_date_name: str = 'event__date_from__'
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.is_after and not self._choice.supports_after:
|
||||
raise ValueError(
|
||||
"The selected base date and attribute combination does not support relative dates placed after the base date"
|
||||
)
|
||||
if not self.is_after and not self._choice.supports_before:
|
||||
raise ValueError(
|
||||
"The selected base date and attribute combination does not support relative dates placed before the base date")
|
||||
|
||||
@property
|
||||
def _choice(self):
|
||||
return BaseChoice.find(BASE_CHOICES, self.base_date_name)
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self._choice.key
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
if not isinstance(o, RelativeDate):
|
||||
return False
|
||||
return self.to_string() == o.to_string()
|
||||
|
||||
def _resolve_date(self, base: "Event | Order | SubEvent") -> Tuple[datetime.datetime, ZoneInfo]:
|
||||
"""
|
||||
Resolves the datetime and timezone information of this RelativeDate object in relation to the provided reference.
|
||||
|
||||
:param base: the reference which should be used to resolve the relative date
|
||||
:return:
|
||||
"""
|
||||
from .models import Event, Order, SubEvent
|
||||
|
||||
choice = self._choice
|
||||
|
||||
if choice.base == "order" and isinstance(base, Order):
|
||||
event = base.event
|
||||
base_date = getattr(base, choice.attribute)
|
||||
elif choice.base == "event" and isinstance(base, SubEvent):
|
||||
event = base.event
|
||||
base_date = (getattr(base, choice.attribute) or
|
||||
getattr(base.event, choice.attribute) or
|
||||
base.date_from)
|
||||
elif choice.base == "event" and isinstance(base, Event):
|
||||
event = base
|
||||
base_date = getattr(base, choice.attribute) or event.date_from
|
||||
else:
|
||||
raise TypeError("The base defined by data does not match the passed in base")
|
||||
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
return base_date, tz
|
||||
|
||||
def date(self, base: "Event | Order | SubEvent") -> datetime.date:
|
||||
"""
|
||||
Resolves the effective date of this RelativeDate object in relation to the provided reference.
|
||||
|
||||
:param base: the reference which should be used to resolve the date
|
||||
:return: datetime.date
|
||||
"""
|
||||
if self.minutes is not None:
|
||||
raise ValueError('A minute-based relative datetime can not be used as a date')
|
||||
|
||||
base_date, tz = self._resolve_date(base)
|
||||
|
||||
if self.is_after:
|
||||
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.days)
|
||||
else:
|
||||
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.days)
|
||||
return new_date.date()
|
||||
|
||||
def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
|
||||
"""
|
||||
Resolves the effective datetime of this RelativeDate object in relation to the provided reference.
|
||||
|
||||
:param base: the reference which should be used to resolve the datetime
|
||||
:return: datetime.datetime
|
||||
"""
|
||||
base_date, tz = self._resolve_date(base)
|
||||
|
||||
if self.minutes is not None:
|
||||
if self.is_after:
|
||||
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.minutes)
|
||||
else:
|
||||
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.minutes)
|
||||
else:
|
||||
if self.is_after:
|
||||
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.days)).astimezone(tz)
|
||||
else:
|
||||
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.days)).astimezone(tz)
|
||||
if self.time:
|
||||
new_date = new_date.replace(
|
||||
hour=self.time.hour,
|
||||
minute=self.time.minute,
|
||||
second=self.time.second
|
||||
)
|
||||
new_date = new_date.astimezone(tz)
|
||||
return new_date
|
||||
|
||||
def to_string(self) -> str:
|
||||
if self.minutes is not None:
|
||||
return 'RELDATE/minutes/{}/{}/{}'.format( #
|
||||
self.minutes,
|
||||
self._choice.key,
|
||||
'after' if self.is_after else '',
|
||||
)
|
||||
return 'RELDATE/{}/{}/{}/{}'.format( #
|
||||
self.days,
|
||||
self.time.strftime('%H:%M:%S') if self.time else '-',
|
||||
self._choice.key,
|
||||
'after' if self.is_after else '',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, input: str):
|
||||
if not input.startswith('RELDATE/'):
|
||||
raise TypeError("Invalid input for RelativeDate.from_string()")
|
||||
|
||||
parts = input.split('/')
|
||||
if parts[1] == 'minutes':
|
||||
data = RelativeDate(
|
||||
days=0,
|
||||
minutes=int(parts[2]),
|
||||
base_date_name=parts[3],
|
||||
time=None,
|
||||
is_after=len(parts) > 4 and parts[4] == "after",
|
||||
)
|
||||
else:
|
||||
if parts[2] == '-':
|
||||
time = None
|
||||
else:
|
||||
timeparts = parts[2].split(':')
|
||||
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
|
||||
try:
|
||||
data = RelativeDate(
|
||||
days=int(parts[1] or 0),
|
||||
base_date_name=parts[3],
|
||||
time=time,
|
||||
minutes=None,
|
||||
is_after=len(parts) > 4 and parts[4] == "after",
|
||||
)
|
||||
except ValueError:
|
||||
data = RelativeDate(
|
||||
days=0,
|
||||
base_date_name=parts[3],
|
||||
time=time,
|
||||
minutes=None,
|
||||
is_after=len(parts) > 4 and parts[4] == "after",
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RelativeDateWrapper:
|
||||
@@ -50,130 +265,53 @@ class RelativeDateWrapper:
|
||||
This contains information on a date that might be relative to an event. This means
|
||||
that the underlying data is either a fixed date or a number of days and a wall clock
|
||||
time to calculate the date based on a base point.
|
||||
|
||||
The base point can be the date_from, date_to, date_admission, presale_start or presale_end
|
||||
attribute of an event or subevent. If the respective attribute is not set, ``date_from``
|
||||
will be used.
|
||||
"""
|
||||
|
||||
def __init__(self, data: Union[datetime.datetime, RelativeDate]):
|
||||
self.data = data
|
||||
|
||||
def date(self, event) -> datetime.date:
|
||||
from .models import SubEvent
|
||||
def date(self, base: "Event | Order | SubEvent") -> datetime.date:
|
||||
"""
|
||||
If the RelativeDateWrapper wraps a RelativeDate object:
|
||||
Resolves the effective date of this object in relation to the provided reference.
|
||||
If the RelativeDateWrapper wraps an absolute date or datetime:
|
||||
Returns the wrapped absolute date.
|
||||
|
||||
:param base: the reference which should be used to resolve the date in case it is a relative date.
|
||||
:return: datetime.date
|
||||
"""
|
||||
if isinstance(self.data, datetime.datetime):
|
||||
return self.data.date()
|
||||
elif isinstance(self.data, datetime.date):
|
||||
return self.data
|
||||
else:
|
||||
if self.data.minutes is not None:
|
||||
raise ValueError('A minute-based relative datetime can not be used as a date')
|
||||
return self.data.date(base)
|
||||
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
if isinstance(event, SubEvent):
|
||||
base_date = (
|
||||
getattr(event, self.data.base_date_name)
|
||||
or getattr(event.event, self.data.base_date_name)
|
||||
or event.date_from
|
||||
)
|
||||
else:
|
||||
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||
|
||||
if self.data.is_after:
|
||||
new_date = base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)
|
||||
else:
|
||||
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)
|
||||
return new_date.date()
|
||||
|
||||
def datetime(self, event) -> datetime.datetime:
|
||||
from .models import SubEvent
|
||||
def datetime(self, base: "Event | Order | SubEvent") -> datetime.datetime:
|
||||
"""
|
||||
If the RelativeDateWrapper wraps a RelativeDate object:
|
||||
Resolves the effective datetime of this object in relation to the provided reference.
|
||||
If the RelativeDateWrapper wraps an absolute date or datetime:
|
||||
Returns the wrapped absolute datetime.
|
||||
|
||||
:param base: the reference which should be used to resolve the datetime in case it is a relative datetime.
|
||||
:return: datetime.datetime
|
||||
"""
|
||||
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
||||
return self.data
|
||||
else:
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
if isinstance(event, SubEvent):
|
||||
base_date = (
|
||||
getattr(event, self.data.base_date_name)
|
||||
or getattr(event.event, self.data.base_date_name)
|
||||
or event.date_from
|
||||
)
|
||||
else:
|
||||
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||
|
||||
if self.data.minutes is not None:
|
||||
if self.data.is_after:
|
||||
return base_date.astimezone(tz) + datetime.timedelta(minutes=self.data.minutes)
|
||||
else:
|
||||
return base_date.astimezone(tz) - datetime.timedelta(minutes=self.data.minutes)
|
||||
else:
|
||||
if self.data.is_after:
|
||||
new_date = (base_date.astimezone(tz) + datetime.timedelta(days=self.data.days)).astimezone(tz)
|
||||
else:
|
||||
new_date = (base_date.astimezone(tz) - datetime.timedelta(days=self.data.days)).astimezone(tz)
|
||||
if self.data.time:
|
||||
new_date = new_date.replace(
|
||||
hour=self.data.time.hour,
|
||||
minute=self.data.time.minute,
|
||||
second=self.data.time.second
|
||||
)
|
||||
new_date = new_date.astimezone(tz)
|
||||
return new_date
|
||||
return self.data.datetime(base)
|
||||
|
||||
def to_string(self) -> str:
|
||||
if isinstance(self.data, (datetime.datetime, datetime.date)):
|
||||
return self.data.isoformat()
|
||||
else:
|
||||
if self.data.minutes is not None:
|
||||
return 'RELDATE/minutes/{}/{}/{}'.format( #
|
||||
self.data.minutes,
|
||||
self.data.base_date_name,
|
||||
'after' if self.data.is_after else '',
|
||||
)
|
||||
return 'RELDATE/{}/{}/{}/{}'.format( #
|
||||
self.data.days,
|
||||
self.data.time.strftime('%H:%M:%S') if self.data.time else '-',
|
||||
self.data.base_date_name,
|
||||
'after' if self.data.is_after else '',
|
||||
)
|
||||
return self.data.to_string()
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, input: str):
|
||||
if input.startswith('RELDATE/'):
|
||||
parts = input.split('/')
|
||||
if parts[1] == 'minutes':
|
||||
data = RelativeDate(
|
||||
days=0,
|
||||
minutes=int(parts[2]),
|
||||
base_date_name=parts[3],
|
||||
time=None,
|
||||
is_after=len(parts) > 4 and parts[4] == "after",
|
||||
)
|
||||
else:
|
||||
if parts[2] == '-':
|
||||
time = None
|
||||
else:
|
||||
timeparts = parts[2].split(':')
|
||||
time = datetime.time(hour=int(timeparts[0]), minute=int(timeparts[1]), second=int(timeparts[2]))
|
||||
try:
|
||||
data = RelativeDate(
|
||||
days=int(parts[1] or 0),
|
||||
base_date_name=parts[3],
|
||||
time=time,
|
||||
minutes=None,
|
||||
is_after=len(parts) > 4 and parts[4] == "after",
|
||||
)
|
||||
except ValueError:
|
||||
data = RelativeDate(
|
||||
days=0,
|
||||
base_date_name=parts[3],
|
||||
time=time,
|
||||
minutes=None,
|
||||
is_after=len(parts) > 4 and parts[4] == "after",
|
||||
)
|
||||
if data.base_date_name not in [k[0] for k in BASE_CHOICES]:
|
||||
raise ValueError('{} is not a valid base date'.format(data.base_date_name))
|
||||
data = RelativeDate.from_string(input)
|
||||
else:
|
||||
data = parser.parse(input)
|
||||
return RelativeDateWrapper(data)
|
||||
@@ -187,7 +325,6 @@ BEFORE_AFTER_CHOICE = (
|
||||
('after', _('after')),
|
||||
)
|
||||
|
||||
|
||||
reldatetimeparts = namedtuple('reldatetimeparts', (
|
||||
"status", # 0
|
||||
"absolute", # 1
|
||||
@@ -202,6 +339,14 @@ reldatetimeparts = namedtuple('reldatetimeparts', (
|
||||
reldatetimeparts.indizes = reldatetimeparts(*range(9))
|
||||
|
||||
|
||||
def _get_choices(base_choices: List[BaseChoice]) -> List[Tuple[str, Promise]]:
|
||||
return [(c.key, c.text) for c in base_choices]
|
||||
|
||||
|
||||
def _get_choice_validation_obj(choices: List[BaseChoice]):
|
||||
return {c.key: {"data-supports-before": c.supports_before, "data-supports-after": c.supports_after} for c in choices}
|
||||
|
||||
|
||||
class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
template_name = 'pretixbase/forms/widgets/reldatetime.html'
|
||||
parts = reldatetimeparts
|
||||
@@ -209,6 +354,7 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.status_choices = kwargs.pop('status_choices')
|
||||
base_choices = kwargs.pop('base_choices')
|
||||
choices = _get_choices(base_choices)
|
||||
|
||||
def placeholder_datetime_format():
|
||||
df = get_format('DATETIME_INPUT_FORMATS')[0]
|
||||
@@ -220,21 +366,29 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
tf = get_format('TIME_INPUT_FORMATS')[0]
|
||||
return datetime.time(8, 30, 0).strftime(tf)
|
||||
|
||||
widgets = reldatetimeparts(
|
||||
widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldatetimeparts(
|
||||
status=forms.RadioSelect(choices=self.status_choices),
|
||||
absolute=forms.DateTimeInput(
|
||||
attrs={'placeholder': lazy(placeholder_datetime_format, str), 'class': 'datetimepicker'}
|
||||
),
|
||||
rel_days_number=forms.NumberInput(),
|
||||
rel_mins_relationto=forms.Select(choices=base_choices),
|
||||
rel_mins_relationto=OptionAttrsSelect(
|
||||
attrs={'data-relative-choice': True},
|
||||
choices=choices,
|
||||
option_attrs=_get_choice_validation_obj(base_choices)
|
||||
),
|
||||
rel_days_timeofday=forms.TimeInput(
|
||||
attrs={'placeholder': lazy(placeholder_time_format, str), 'class': 'timepickerfield'}
|
||||
),
|
||||
rel_mins_number=forms.NumberInput(),
|
||||
rel_days_relationto=forms.Select(choices=base_choices),
|
||||
rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||
)
|
||||
rel_days_relationto=OptionAttrsSelect(
|
||||
attrs={'data-relative-choice': True},
|
||||
choices=choices,
|
||||
option_attrs=_get_choice_validation_obj(base_choices)
|
||||
),
|
||||
rel_mins_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
|
||||
rel_days_relation=forms.Select(attrs={'data-relation-choice': True}, choices=BEFORE_AFTER_CHOICE),
|
||||
)))
|
||||
super().__init__(widgets=widgets, *args, **kwargs)
|
||||
|
||||
def decompress(self, value):
|
||||
@@ -247,10 +401,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
status="unset",
|
||||
absolute=None,
|
||||
rel_days_number=1,
|
||||
rel_mins_relationto="date_from",
|
||||
rel_mins_relationto="event__date_from",
|
||||
rel_days_timeofday=None,
|
||||
rel_mins_number=0,
|
||||
rel_days_relationto="date_from",
|
||||
rel_days_relationto="event__date_from",
|
||||
rel_mins_relation="before",
|
||||
rel_days_relation="before"
|
||||
)
|
||||
@@ -259,10 +413,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
status="absolute",
|
||||
absolute=value.data,
|
||||
rel_days_number=1,
|
||||
rel_mins_relationto="date_from",
|
||||
rel_mins_relationto="event__date_from",
|
||||
rel_days_timeofday=None,
|
||||
rel_mins_number=0,
|
||||
rel_days_relationto="date_from",
|
||||
rel_days_relationto="event__date_from",
|
||||
rel_mins_relation="before",
|
||||
rel_days_relation="before"
|
||||
)
|
||||
@@ -271,10 +425,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
status="relative_minutes",
|
||||
absolute=None,
|
||||
rel_days_number=None,
|
||||
rel_mins_relationto=value.data.base_date_name,
|
||||
rel_mins_relationto=value.data.key,
|
||||
rel_days_timeofday=None,
|
||||
rel_mins_number=value.data.minutes,
|
||||
rel_days_relationto=value.data.base_date_name,
|
||||
rel_days_relationto=value.data.key,
|
||||
rel_mins_relation="after" if value.data.is_after else "before",
|
||||
rel_days_relation="after" if value.data.is_after else "before"
|
||||
)
|
||||
@@ -282,10 +436,10 @@ class RelativeDateTimeWidget(forms.MultiWidget):
|
||||
status="relative",
|
||||
absolute=None,
|
||||
rel_days_number=value.data.days,
|
||||
rel_mins_relationto=value.data.base_date_name,
|
||||
rel_mins_relationto=value.data.key,
|
||||
rel_days_timeofday=value.data.time,
|
||||
rel_mins_number=0,
|
||||
rel_days_relationto=value.data.base_date_name,
|
||||
rel_days_relationto=value.data.key,
|
||||
rel_mins_relation="after" if value.data.is_after else "before",
|
||||
rel_days_relation="after" if value.data.is_after else "before"
|
||||
)
|
||||
@@ -309,17 +463,41 @@ class RelativeDateTimeField(forms.MultiValueField):
|
||||
('relative', _('Relative date:')),
|
||||
('relative_minutes', _('Relative time:')),
|
||||
]
|
||||
self.relative_to_order = kwargs.pop('relative_to_order', False)
|
||||
|
||||
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
|
||||
if self.relative_to_order:
|
||||
possible_choices.extend(ORDER_BASE_CHOICES)
|
||||
|
||||
if kwargs.get('limit_choices'):
|
||||
limit = kwargs.pop('limit_choices')
|
||||
choices = [(k, v) for k, v in BASE_CHOICES if k in limit]
|
||||
else:
|
||||
choices = BASE_CHOICES
|
||||
if any(["__" not in l for l in limit]):
|
||||
_warn_skips = (str(os.path.dirname(__file__)),)
|
||||
|
||||
warnings.warn(
|
||||
message="Please prefix limit_choices with the base the attributes refer to, for example event__date_from",
|
||||
category=DeprecationWarning,
|
||||
skip_file_prefixes=_warn_skips
|
||||
)
|
||||
|
||||
possible_choices = [
|
||||
c for c in possible_choices if
|
||||
# new base case as we want limit_choices to be expressed as base__attribute
|
||||
(c.key in limit) or
|
||||
# fallback for old event based entries
|
||||
# if the base is an event, then using only attribute is fine
|
||||
(c.base == "event" and c.attribute in limit)
|
||||
]
|
||||
|
||||
if not kwargs.get('required', True):
|
||||
status_choices.insert(0, ('unset', _('Not set')))
|
||||
|
||||
choices = _get_choices(possible_choices)
|
||||
|
||||
fields = reldatetimeparts(
|
||||
status=forms.ChoiceField(
|
||||
choices=status_choices,
|
||||
required=True
|
||||
required=True,
|
||||
),
|
||||
absolute=forms.DateTimeField(
|
||||
required=False
|
||||
@@ -329,7 +507,7 @@ class RelativeDateTimeField(forms.MultiValueField):
|
||||
),
|
||||
rel_mins_relationto=forms.ChoiceField(
|
||||
choices=choices,
|
||||
required=False
|
||||
required=False,
|
||||
),
|
||||
rel_days_timeofday=forms.TimeField(
|
||||
required=False,
|
||||
@@ -339,7 +517,7 @@ class RelativeDateTimeField(forms.MultiValueField):
|
||||
),
|
||||
rel_days_relationto=forms.ChoiceField(
|
||||
choices=choices,
|
||||
required=False
|
||||
required=False,
|
||||
),
|
||||
rel_mins_relation=forms.ChoiceField(
|
||||
choices=BEFORE_AFTER_CHOICE,
|
||||
@@ -350,8 +528,9 @@ class RelativeDateTimeField(forms.MultiValueField):
|
||||
required=False
|
||||
),
|
||||
)
|
||||
|
||||
if 'widget' not in kwargs:
|
||||
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=choices)
|
||||
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=possible_choices)
|
||||
kwargs.pop('max_length', 0)
|
||||
kwargs.pop('empty_value', 0)
|
||||
super().__init__(
|
||||
@@ -359,21 +538,24 @@ class RelativeDateTimeField(forms.MultiValueField):
|
||||
)
|
||||
|
||||
def set_event(self, event):
|
||||
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = [
|
||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
||||
]
|
||||
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = [
|
||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
||||
]
|
||||
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
|
||||
if self.relative_to_order:
|
||||
possible_choices.extend(ORDER_BASE_CHOICES)
|
||||
|
||||
possible_choices = possible_choices
|
||||
choices = _get_choices(possible_choices)
|
||||
|
||||
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = choices
|
||||
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = choices
|
||||
|
||||
def compress(self, data_list):
|
||||
if not data_list:
|
||||
return None
|
||||
data = reldatetimeparts(*data_list)
|
||||
if data.status == 'absolute':
|
||||
return RelativeDateWrapper(data.absolute)
|
||||
elif data.status == 'unset':
|
||||
if data.status == 'unset':
|
||||
return None
|
||||
elif data.status == 'absolute':
|
||||
return RelativeDateWrapper(data.absolute)
|
||||
elif data.status == 'relative_minutes':
|
||||
return RelativeDateWrapper(RelativeDate(
|
||||
days=0,
|
||||
@@ -404,6 +586,18 @@ class RelativeDateTimeField(forms.MultiValueField):
|
||||
raise ValidationError(self.error_messages['incomplete'])
|
||||
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
|
||||
raise ValidationError(self.error_messages['incomplete'])
|
||||
elif data.status == 'relative':
|
||||
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
|
||||
if data.rel_days_relation == "before" and not choice.supports_before:
|
||||
raise ValidationError(_('A relative date cannot be expressed as "before" for "{}"'.format(choice.text)))
|
||||
elif data.status == 'relative' and data.rel_days_relation == "after" and not choice.supports_after:
|
||||
raise ValidationError(_('A relative date cannot be expressed as "after" for "{}"'.format(choice.text)))
|
||||
elif data.status == 'relative_minutes':
|
||||
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
|
||||
if data.rel_days_relation == "before" and not choice.supports_before:
|
||||
raise ValidationError(_('A relative time cannot be expressed as "before" for "{}"'.format(choice.text)))
|
||||
elif data.rel_days_relation == "after" and not choice.supports_after:
|
||||
raise ValidationError(_('A relative time cannot be expressed as "after" for "{}"'.format(choice.text)))
|
||||
|
||||
return super().clean(value)
|
||||
|
||||
@@ -424,15 +618,20 @@ class RelativeDateWidget(RelativeDateTimeWidget):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.status_choices = kwargs.pop('status_choices')
|
||||
widgets = reldateparts(
|
||||
self.base_choices = kwargs.pop('base_choices')
|
||||
widgets = cast(dict[str, Widget | type[Widget]], cast(object, reldateparts(
|
||||
status=forms.RadioSelect(choices=self.status_choices),
|
||||
absolute=forms.DateInput(
|
||||
attrs={'class': 'datepickerfield'}
|
||||
),
|
||||
rel_days_number=forms.NumberInput(),
|
||||
rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')),
|
||||
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||
)
|
||||
rel_days_relationto=OptionAttrsSelect(
|
||||
choices=self.base_choices,
|
||||
option_attrs=_get_choice_validation_obj(self.base_choices),
|
||||
attrs={'data-relative-choice': True},
|
||||
),
|
||||
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE, attrs={'data-relation-choice': True},),
|
||||
)))
|
||||
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
|
||||
|
||||
def decompress(self, value):
|
||||
@@ -460,7 +659,7 @@ class RelativeDateWidget(RelativeDateTimeWidget):
|
||||
status="relative",
|
||||
absolute=None,
|
||||
rel_days_number=value.data.days,
|
||||
rel_days_relationto=value.data.base_date_name,
|
||||
rel_days_relationto=value.data.key,
|
||||
rel_days_relation="after" if value.data.is_after else "before"
|
||||
)
|
||||
|
||||
@@ -474,6 +673,15 @@ class RelativeDateField(RelativeDateTimeField):
|
||||
]
|
||||
if not kwargs.get('required', True):
|
||||
status_choices.insert(0, ('unset', _('Not set')))
|
||||
|
||||
self.relative_to_order = kwargs.pop('relative_to_order', False)
|
||||
|
||||
possible_choices = copy.deepcopy(EVENT_BASE_CHOICES)
|
||||
if self.relative_to_order:
|
||||
possible_choices.extend(ORDER_BASE_CHOICES)
|
||||
|
||||
choices = _get_choices(possible_choices)
|
||||
|
||||
fields = reldateparts(
|
||||
status=forms.ChoiceField(
|
||||
choices=status_choices,
|
||||
@@ -486,33 +694,37 @@ class RelativeDateField(RelativeDateTimeField):
|
||||
required=False
|
||||
),
|
||||
rel_days_relationto=forms.ChoiceField(
|
||||
choices=BASE_CHOICES,
|
||||
required=False
|
||||
choices=choices,
|
||||
required=False,
|
||||
),
|
||||
rel_days_relation=forms.ChoiceField(
|
||||
choices=BEFORE_AFTER_CHOICE,
|
||||
required=False
|
||||
),
|
||||
)
|
||||
|
||||
if 'widget' not in kwargs:
|
||||
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
|
||||
kwargs['widget'] = RelativeDateWidget(status_choices=status_choices, base_choices=possible_choices)
|
||||
forms.MultiValueField.__init__(
|
||||
self, fields=fields, require_all_fields=False, *args, **kwargs
|
||||
)
|
||||
|
||||
def set_event(self, event):
|
||||
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = [
|
||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
||||
choices = [
|
||||
(c.key, c.text) for c in EVENT_BASE_CHOICES if getattr(event, c.attribute, None)
|
||||
]
|
||||
if self.relative_to_order:
|
||||
choices += [(c.key, c.text) for c in ORDER_BASE_CHOICES]
|
||||
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = choices
|
||||
|
||||
def compress(self, data_list):
|
||||
if not data_list:
|
||||
return None
|
||||
data = reldateparts(*data_list)
|
||||
if data.status == 'absolute':
|
||||
return RelativeDateWrapper(data.absolute)
|
||||
elif data.status == 'unset':
|
||||
if data.status == 'unset':
|
||||
return None
|
||||
elif data.status == 'absolute':
|
||||
return RelativeDateWrapper(data.absolute)
|
||||
else:
|
||||
return RelativeDateWrapper(RelativeDate(
|
||||
days=data.rel_days_number,
|
||||
@@ -525,8 +737,14 @@ class RelativeDateField(RelativeDateTimeField):
|
||||
data = reldateparts(*value)
|
||||
if data.status == 'absolute' and not data.absolute:
|
||||
raise ValidationError(self.error_messages['incomplete'])
|
||||
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto):
|
||||
raise ValidationError(self.error_messages['incomplete'])
|
||||
if data.status == 'relative':
|
||||
choice = BaseChoice.find(BASE_CHOICES, data.rel_days_relationto)
|
||||
if data.rel_days_number is None or not data.rel_days_relationto:
|
||||
raise ValidationError(self.error_messages['incomplete'])
|
||||
elif data.rel_days_relation == "before" and not choice.supports_before:
|
||||
raise ValidationError(_('A relative date cannot be expressed as "before" for "{}"'.format(choice.text)))
|
||||
elif data.rel_days_relation == "after" and not choice.supports_after:
|
||||
raise ValidationError(_('A relative date cannot be expressed as "after" for "{}"'.format(choice.text)))
|
||||
|
||||
return forms.MultiValueField.clean(self, value)
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ from django.utils.translation import (
|
||||
)
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
@@ -60,7 +61,7 @@ from pretix.base.models import (
|
||||
Seat, SeatCategoryMapping, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.models.orders import CheckoutSession, OrderFee
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.checkin import _save_answers
|
||||
@@ -471,6 +472,16 @@ class CartManager:
|
||||
if term_last < time_machine_now(self.real_now_dt):
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
|
||||
def _ensure_checkout_session(self):
|
||||
CheckoutSession.objects.get_or_create(
|
||||
event=self.event,
|
||||
cart_id=self.cart_id,
|
||||
defaults={
|
||||
"sales_channel": self._sales_channel,
|
||||
"testmode": self.event.testmode,
|
||||
},
|
||||
)
|
||||
|
||||
def _extend_expiry_of_valid_existing_positions(self):
|
||||
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
|
||||
# delta to reduce risk of extending already expired CartPositions.
|
||||
@@ -916,6 +927,8 @@ class CartManager:
|
||||
if custom_price > 99_999_999_999:
|
||||
raise CartError(error_messages['price_too_high'])
|
||||
|
||||
custom_price = round_decimal(custom_price, currency=self.event.currency)
|
||||
|
||||
op = self.AddOperation(
|
||||
count=i['count'],
|
||||
item=item,
|
||||
@@ -1038,6 +1051,8 @@ class CartManager:
|
||||
if custom_price > 99_999_999_999:
|
||||
raise CartError(error_messages['price_too_high'])
|
||||
|
||||
custom_price = round_decimal(custom_price, currency=self.event.currency)
|
||||
|
||||
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
|
||||
for ca in current_addons[cp][a['item'], a['variation']]:
|
||||
if ca.listed_price != listed_price:
|
||||
@@ -1554,6 +1569,7 @@ class CartManager:
|
||||
|
||||
def commit(self):
|
||||
self._check_presale_dates()
|
||||
self._ensure_checkout_session()
|
||||
self._check_max_cart_size()
|
||||
|
||||
err = self._delete_out_of_timeframe()
|
||||
@@ -1589,6 +1605,7 @@ def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None,
|
||||
'max_value': str(max_value) if max_value is not None else None,
|
||||
'info_data': info_data or {},
|
||||
})
|
||||
cart_session['payments_postpone'] = False
|
||||
|
||||
|
||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||
|
||||
@@ -40,7 +40,7 @@ import dateutil
|
||||
import dateutil.parser
|
||||
from dateutil.tz import datetime_exists
|
||||
from django.core.files import File
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import (
|
||||
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
||||
OuterRef, Q, Subquery, TextField, Value, When,
|
||||
@@ -59,6 +59,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.signals import checkin_created, periodic_task
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.database import conditional_atomic
|
||||
from pretix.helpers.jsonlogic import Logic
|
||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||
from pretix.helpers.jsonlogic_query import (
|
||||
@@ -891,6 +892,7 @@ def _save_answers(op, answers, given_answers):
|
||||
qa.answer = answer
|
||||
qa.save(update_fields=['answer'])
|
||||
qa.options.clear()
|
||||
return qa
|
||||
|
||||
written = False
|
||||
for q, a in given_answers.items():
|
||||
@@ -1043,10 +1045,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
if not simulate:
|
||||
_save_answers(op, answers, given_answers)
|
||||
|
||||
with transaction.atomic():
|
||||
with conditional_atomic(not simulate):
|
||||
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||
opqs = OrderPosition.all.select_related("order", "item")
|
||||
if type != Checkin.TYPE_EXIT:
|
||||
if type != Checkin.TYPE_EXIT and not simulate:
|
||||
opqs = opqs.select_for_update(of=OF_SELF)
|
||||
op = opqs.get(pk=op.pk)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ from pretix.base.models.customers import CustomerSSOGrant
|
||||
|
||||
from ..models import CachedFile, CartPosition, InvoiceAddress
|
||||
from ..models.auth import UserKnownLoginSource
|
||||
from ..models.orders import CheckoutSession
|
||||
from ..signals import periodic_task
|
||||
|
||||
|
||||
@@ -43,6 +44,10 @@ def clean_cart_positions(sender, **kwargs):
|
||||
cp.delete()
|
||||
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
|
||||
cp.delete()
|
||||
for cs in CheckoutSession.objects.filter(created__lt=now() - timedelta(days=14)).exclude(
|
||||
Exists(CartPosition.objects.filter(cart_id=OuterRef("cart_id")))
|
||||
):
|
||||
cs.delete()
|
||||
for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)):
|
||||
ia.delete()
|
||||
|
||||
|
||||
@@ -48,12 +48,12 @@ from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db import models, transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
|
||||
Value,
|
||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery,
|
||||
Sum, Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.models.functions import Cast, Greatest
|
||||
from django.db.transaction import get_connection
|
||||
from django.dispatch import receiver
|
||||
from django.utils.functional import cached_property
|
||||
@@ -71,10 +71,10 @@ from pretix.base.models import (
|
||||
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
||||
SeatCategoryMapping, User, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.event import Event_SettingsStore, SubEvent
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
||||
generate_secret,
|
||||
BlockedTicketSecret, CheckoutSession, InvoiceAddress, OrderFee,
|
||||
OrderRefund, generate_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
@@ -961,7 +961,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
|
||||
|
||||
def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
|
||||
meta_info: dict, event: Event, require_approval=False):
|
||||
meta_info: dict, event: Event, sales_channel: SalesChannel, require_approval=False):
|
||||
fees = []
|
||||
# Pre-rounding, pre-fee total is used for fee calculation
|
||||
total = sum([c.gross_price_before_rounding for c in positions])
|
||||
@@ -1021,7 +1021,14 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
|
||||
payments_assigned += to_pay
|
||||
p['payment_amount'] = to_pay
|
||||
|
||||
if total != payments_assigned and not require_approval:
|
||||
allow_postponed_payment = (
|
||||
require_approval or
|
||||
(
|
||||
sales_channel.identifier in event.settings.payment_choice_postpone_allowed_channels and not payment_requests
|
||||
)
|
||||
)
|
||||
|
||||
if total != payments_assigned and not allow_postponed_payment:
|
||||
raise OrderError(_("The selected payment methods do not cover the total balance."))
|
||||
|
||||
return fees
|
||||
@@ -1030,7 +1037,8 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
|
||||
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
|
||||
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
|
||||
customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None):
|
||||
customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None,
|
||||
cart_id: str=None):
|
||||
payments = []
|
||||
|
||||
try:
|
||||
@@ -1042,7 +1050,15 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
|
||||
|
||||
# Final calculation of fees, also performs final rounding
|
||||
try:
|
||||
fees = _apply_rounding_and_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
|
||||
fees = _apply_rounding_and_fees(
|
||||
positions,
|
||||
payment_requests,
|
||||
address,
|
||||
meta_info,
|
||||
event,
|
||||
sales_channel=sales_channel,
|
||||
require_approval=require_approval
|
||||
)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(error_messages['country_blocked'])
|
||||
|
||||
@@ -1113,6 +1129,14 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
|
||||
if meta_info:
|
||||
for msg in meta_info.get('confirm_messages', []):
|
||||
order.log_action('pretix.event.order.consent', data={'msg': msg})
|
||||
if cart_id:
|
||||
try:
|
||||
session = CheckoutSession.objects.get(event=event, cart_id=cart_id)
|
||||
except CheckoutSession.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
session.answers.update(order=order, checkoutsession=None)
|
||||
session.delete()
|
||||
|
||||
order_placed.send(event, order=order, bulk=False)
|
||||
return order, payments
|
||||
@@ -1160,7 +1184,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
|
||||
|
||||
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
|
||||
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
|
||||
shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None):
|
||||
shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None, cart_id: str=None):
|
||||
for p in payment_requests:
|
||||
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
|
||||
if not p['pprov']:
|
||||
@@ -1267,6 +1291,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
valid_if_pending=valid_if_pending,
|
||||
api_meta=api_meta,
|
||||
tax_rounding_mode=tax_rounding_mode,
|
||||
cart_id=cart_id,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -1494,83 +1519,104 @@ def send_expiry_warnings(sender, **kwargs):
|
||||
@scopes_disabled()
|
||||
def send_download_reminders(sender, **kwargs):
|
||||
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
qs = Order.objects.annotate(
|
||||
first_date=Coalesce(
|
||||
Min('all_positions__subevent__date_from'),
|
||||
F('event__date_from')
|
||||
|
||||
events = Event.objects.filter(
|
||||
Q(has_subevents=False, date_from__gte=now()) |
|
||||
(Q(has_subevents=True) & Q(Exists(
|
||||
SubEvent.objects.filter(event_id=OuterRef('id'), date_from__gte=now())
|
||||
)))
|
||||
).annotate(
|
||||
reminder_days=Subquery(
|
||||
Event_SettingsStore.objects.filter(
|
||||
object=OuterRef('id'),
|
||||
key='mail_days_download_reminder'
|
||||
).exclude(
|
||||
value="None"
|
||||
).annotate(
|
||||
val=Cast(F("value"), output_field=models.IntegerField()),
|
||||
).values("val")
|
||||
)
|
||||
).filter(
|
||||
download_reminder_sent=False,
|
||||
datetime__lte=now() - timedelta(hours=2),
|
||||
first_date__gte=today,
|
||||
).only(
|
||||
'pk', 'event_id', 'sales_channel', 'datetime',
|
||||
).order_by('event_id')
|
||||
event_id = None
|
||||
days = None
|
||||
event = None
|
||||
reminder_days__isnull=False,
|
||||
).order_by()
|
||||
|
||||
for o in qs:
|
||||
if o.event_id != event_id:
|
||||
days = o.event.settings.get('mail_days_download_reminder', as_type=int)
|
||||
event = o.event
|
||||
event_id = o.event_id
|
||||
for event in events.iterator(chunk_size=10_000):
|
||||
qs = event.orders.filter(
|
||||
download_reminder_sent=False,
|
||||
datetime__lte=now() - timedelta(hours=2),
|
||||
)
|
||||
|
||||
if days is None:
|
||||
continue
|
||||
|
||||
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
||||
continue
|
||||
|
||||
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if now() < reminder_date or o.datetime > reminder_date:
|
||||
continue
|
||||
|
||||
with transaction.atomic():
|
||||
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
|
||||
if o.download_reminder_sent:
|
||||
# Race condition
|
||||
continue
|
||||
positions = list(o.positions_with_tickets)
|
||||
if not positions:
|
||||
if event.has_subevents:
|
||||
qs = qs.annotate(
|
||||
first_date=Min('all_positions__subevent__date_from')
|
||||
).filter(
|
||||
Q(first_date__gte=today)
|
||||
)
|
||||
else:
|
||||
event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if now() < event_reminder_date:
|
||||
continue
|
||||
|
||||
if not o.ticket_download_available:
|
||||
qs = qs.only(
|
||||
'pk', 'event_id', 'sales_channel', 'datetime',
|
||||
).order_by()
|
||||
|
||||
for o in qs:
|
||||
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
||||
continue
|
||||
|
||||
if o.status != Order.STATUS_PAID:
|
||||
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
||||
if event.has_subevents:
|
||||
reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
else:
|
||||
reminder_date = event_reminder_date
|
||||
if now() < reminder_date or o.datetime > reminder_date:
|
||||
continue
|
||||
|
||||
with transaction.atomic():
|
||||
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
|
||||
if o.download_reminder_sent:
|
||||
# Race condition
|
||||
continue
|
||||
positions = list(o.positions_with_tickets)
|
||||
if not positions:
|
||||
continue
|
||||
|
||||
with language(o.locale, o.event.settings.region):
|
||||
o.download_reminder_sent = True
|
||||
o.save(update_fields=['download_reminder_sent'])
|
||||
email_template = event.settings.mail_text_download_reminder
|
||||
email_subject = event.settings.mail_subject_download_reminder
|
||||
email_context = get_email_context(event=event, order=o)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True
|
||||
)
|
||||
if not o.ticket_download_available:
|
||||
continue
|
||||
|
||||
if event.settings.mail_send_download_reminder_attendee:
|
||||
for p in positions:
|
||||
if p.subevent_id:
|
||||
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
if now() < reminder_date:
|
||||
continue
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
|
||||
email_template = event.settings.mail_text_download_reminder_attendee
|
||||
email_subject = event.settings.mail_subject_download_reminder_attendee
|
||||
email_context = get_email_context(event=event, order=o, position=p)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True, position=p
|
||||
)
|
||||
if o.status != Order.STATUS_PAID:
|
||||
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
||||
continue
|
||||
|
||||
with language(o.locale, o.event.settings.region):
|
||||
o.download_reminder_sent = True
|
||||
o.save(update_fields=['download_reminder_sent'])
|
||||
email_template = event.settings.mail_text_download_reminder
|
||||
email_subject = event.settings.mail_subject_download_reminder
|
||||
email_context = get_email_context(event=event, order=o)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True
|
||||
)
|
||||
|
||||
if event.settings.mail_send_download_reminder_attendee:
|
||||
for p in positions:
|
||||
if p.subevent_id:
|
||||
reminder_date = (p.subevent.date_from - timedelta(days=event.reminder_days)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
if now() < reminder_date:
|
||||
continue
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
|
||||
email_template = event.settings.mail_text_download_reminder_attendee
|
||||
email_subject = event.settings.mail_subject_download_reminder_attendee
|
||||
email_context = get_email_context(event=event, order=o, position=p)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True, position=p
|
||||
)
|
||||
|
||||
|
||||
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
|
||||
@@ -3169,12 +3215,12 @@ class OrderChangeManager:
|
||||
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None,
|
||||
api_meta: dict=None):
|
||||
api_meta: dict=None, cart_id: str=None):
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
try:
|
||||
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||
sales_channel, shown_total, customer, api_meta)
|
||||
sales_channel, shown_total, customer, api_meta, cart_id=cart_id)
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
@@ -3438,7 +3484,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
}
|
||||
)
|
||||
|
||||
new_invoice_created = False
|
||||
new_invoice = None
|
||||
if recreate_invoices:
|
||||
# Lock to prevent duplicate invoice creation
|
||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
||||
@@ -3449,13 +3495,16 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
if has_active_invoice and order.total != oldtotal:
|
||||
try:
|
||||
generate_cancellation(i)
|
||||
generate_invoice(order)
|
||||
new_invoice = generate_invoice(order)
|
||||
except Exception as e:
|
||||
logger.exception("Could not generate invoice.")
|
||||
order.log_action("pretix.event.order.invoice.failed", data={
|
||||
"exception": str(e)
|
||||
})
|
||||
new_invoice_created = True
|
||||
else:
|
||||
order.log_action('pretix.event.order.invoice.generated', data={
|
||||
'invoice': new_invoice.pk
|
||||
})
|
||||
|
||||
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
|
||||
if order.event.settings.get('invoice_generate') == 'True' or (
|
||||
@@ -3465,10 +3514,9 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
try:
|
||||
if has_active_invoice:
|
||||
generate_cancellation(i)
|
||||
i = generate_invoice(order)
|
||||
new_invoice_created = True
|
||||
new_invoice = generate_invoice(order)
|
||||
order.log_action('pretix.event.order.invoice.generated', data={
|
||||
'invoice': i.pk
|
||||
'invoice': new_invoice.pk
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("Could not generate invoice.")
|
||||
@@ -3476,8 +3524,11 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
"exception": str(e)
|
||||
})
|
||||
|
||||
if new_invoice and invoice_transmission_separately(new_invoice):
|
||||
transmit_invoice.apply_async(args=(order.event_id, new_invoice.pk, False))
|
||||
|
||||
order.create_transactions()
|
||||
return old_fee, new_fee, fee, new_payment, new_invoice_created
|
||||
return old_fee, new_fee, fee, new_payment, bool(new_invoice)
|
||||
|
||||
|
||||
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#
|
||||
# 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 datetime import datetime, time, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.utils.timezone import make_aware, now
|
||||
|
||||
from pretix.base.models import Event, SalesChannel
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
|
||||
|
||||
def compute_payment_deadline(event: Event, sales_channel: SalesChannel, now_dt=None, subevents=None) -> datetime:
|
||||
now_dt = now_dt or now()
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
|
||||
sales_channel_suffix = "_" + sales_channel.identifier.replace(".", "_")
|
||||
if not (mode := event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
|
||||
mode = event.settings.get('payment_term_mode')
|
||||
sales_channel_suffix = ""
|
||||
|
||||
if mode == 'days':
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(
|
||||
days=event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
|
||||
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
if event.settings.get('payment_term_weekdays'):
|
||||
if exp_by_date.weekday() == 5:
|
||||
exp_by_date += timedelta(days=2)
|
||||
elif exp_by_date.weekday() == 6:
|
||||
exp_by_date += timedelta(days=1)
|
||||
elif mode == 'minutes':
|
||||
exp_by_date = now_dt.astimezone(tz) + timedelta(
|
||||
minutes=event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
|
||||
else:
|
||||
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
|
||||
|
||||
expires = exp_by_date
|
||||
|
||||
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if term_last:
|
||||
if event.has_subevents and subevents:
|
||||
terms = [
|
||||
term_last.datetime(se).date()
|
||||
for se in subevents
|
||||
]
|
||||
if not terms:
|
||||
return expires
|
||||
term_last = min(terms)
|
||||
else:
|
||||
term_last = term_last.datetime(event).date()
|
||||
term_last = make_aware(datetime.combine(
|
||||
term_last,
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
if term_last < expires:
|
||||
return term_last
|
||||
|
||||
return expires
|
||||
@@ -561,6 +561,22 @@ def base_placeholders(sender, **kwargs):
|
||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||
lambda event: str(event.location or ''),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'event_begin_time', ['event_or_subevent'],
|
||||
lambda event_or_subevent:
|
||||
date_format(event_or_subevent.date_from.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
||||
if event_or_subevent.date_from
|
||||
else '',
|
||||
lambda event: date_format(event.date_from.astimezone(event.timezone), 'TIME_FORMAT') if event.date_from else '',
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'event_end_time', ['event_or_subevent'],
|
||||
lambda event_or_subevent:
|
||||
date_format(event_or_subevent.date_to.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
||||
if event_or_subevent.date_to
|
||||
else '',
|
||||
lambda event: date_format(event.date_to.astimezone(event.timezone), 'TIME_FORMAT') if event.date_to else '',
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'event_admission_time', ['event_or_subevent'],
|
||||
lambda event_or_subevent:
|
||||
@@ -801,11 +817,10 @@ def get_available_placeholders(event, base_parameters, rich=False):
|
||||
return params
|
||||
|
||||
|
||||
def get_sample_context(event, context_parameters, rich=True):
|
||||
def prepare_sample_context_for_preview(placeholder_to_sample):
|
||||
context_dict = {}
|
||||
lbl = _('This value will be replaced based on dynamic parameters.')
|
||||
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
|
||||
sample = v.render_sample(event)
|
||||
for k, sample in placeholder_to_sample.items():
|
||||
if isinstance(sample, PlainHtmlAlternativeString):
|
||||
context_dict[k] = PlainHtmlAlternativeString(
|
||||
'<{el} class="placeholder" title="{title}">{plain}</{el}>'.format(
|
||||
@@ -830,3 +845,12 @@ def get_sample_context(event, context_parameters, rich=True):
|
||||
escape(sample)
|
||||
))
|
||||
return context_dict
|
||||
|
||||
|
||||
def get_sample_context(event, context_parameters, rich=True):
|
||||
return prepare_sample_context_for_preview(
|
||||
{
|
||||
k: v.render_sample(event)
|
||||
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -193,7 +193,7 @@ class QuotaAvailability:
|
||||
lock_name = '_'.join([str(p) for p in sorted([q.pk for q in quotas])])
|
||||
if rc.exists(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}'):
|
||||
return
|
||||
rc.setex(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', 10)
|
||||
rc.set(f'quotas:availabilitycachewrite:{lock_name}{self._cache_key_suffix}', '1', ex=10)
|
||||
|
||||
update = defaultdict(list)
|
||||
for q in quotas:
|
||||
|
||||
@@ -343,6 +343,66 @@ def _validate_vat_id_EU(vat_id, country_code):
|
||||
return vat_id
|
||||
|
||||
|
||||
def _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id):
|
||||
# We can skip most static validation checks because _validate_vat_id_EU always runs before
|
||||
vat_id = normalize_vat_id(vat_id, country_code)
|
||||
|
||||
# The VIES service of the European commission is overused and down due to rate limits A LOT. There is another
|
||||
# API by German BZSt, but it only works if the requester is German and the requested is not.
|
||||
# https://www.bzst.de/DE/Unternehmen/Identifikationsnummern/Umsatzsteuer-Identifikationsnummer/AuslaendischeUSt-IdNr/auslaendische_ust_idnr_node.html
|
||||
try:
|
||||
r = requests.post(
|
||||
"https://api.evatr.vies.bzst.de/app/v1/abfrage",
|
||||
json={
|
||||
"anfragendeUstid": requester_id,
|
||||
"angefragteUstid": vat_id,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
d = r.json()
|
||||
if r.status_code == 200:
|
||||
if d['status'] in ('evatr-0000', 'evatr-2008'):
|
||||
# evatr-0000: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
|
||||
# evatr-2008: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
|
||||
# Für die qualifizierte Bestätigungsanfrage liegt einer Besonderheit vor.
|
||||
# Für Rückfragen wenden Sie sich an das BZSt.
|
||||
return vat_id
|
||||
# evatr-2002: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht gültig.
|
||||
# Sie ist erst gültig ab dem Datum im Feld gueltigAb.
|
||||
# evatr-2006: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
|
||||
# Sie war gültig im Zeitraum, der durch die Werte in den Feldern gueltigAb und gueltigBis beschrieben ist.
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
elif r.status_code == 400:
|
||||
if d['status'] in ('evatr-0002', 'evatr-0004', 'evatr-0008'):
|
||||
# evatr-0002: Mindestens eins der Pflichtfelder ist nicht besetzt.
|
||||
# evatr-0004: Die anfragende DE Ust-IdNr. ist syntaktisch falsch. Sie passt nicht in das deutsche Erzeugungsschema.
|
||||
# evatr-0008: Die maximale Anzahl von qualifizierten Bestätigungsabfragen für diese Session wurde erreicht.
|
||||
# Bitte starten Sie erneut mit einer einfachen Bestätigungsabfrage.
|
||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
||||
# evatr-0005: Die angegebene angefragte Ust-IdNr. ist syntaktisch falsch.
|
||||
# evatr-0012: Die angefrage USt-IdNr. ist syntaktisch falsch. Sie passt nicht in das Erzeugungsschema.
|
||||
# evatr-2003: Das angegebene Länderkennzeichen der angefragten USt-IdNr. ist nicht gültig.
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
elif r.status_code == 403:
|
||||
# evatr-0006: Die anfragende DE USt-IdNr. ist nicht berechtigt eine DE Ust-IdNr. anzufragen.
|
||||
# evatr-0007: Fehlerhafter Aufruf.
|
||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
||||
elif r.status_code == 404:
|
||||
if d['status'] in ('evatr-2005'):
|
||||
# evatr-2005: Die angegebene eigene DE Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
|
||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
||||
# evatr-2001: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht vergeben.
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
else: # 500, 503
|
||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
||||
except requests.RequestException:
|
||||
logger.exception('VAT ID checking failed for country {}'.format(country_code))
|
||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
||||
except ValueError: # JSON parsing failed
|
||||
logger.exception('VAT ID checking failed for country {}'.format(country_code))
|
||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
||||
|
||||
|
||||
def _validate_vat_id_CH(vat_id, country_code):
|
||||
if vat_id[:3] != 'CHE':
|
||||
raise VATIDFinalError(error_messages['country_mismatch'])
|
||||
@@ -394,12 +454,18 @@ def _validate_vat_id_CH(vat_id, country_code):
|
||||
return vat_id
|
||||
|
||||
|
||||
def validate_vat_id(vat_id, country_code):
|
||||
def validate_vat_id(vat_id, country_code, requester_id=None):
|
||||
if not vat_id:
|
||||
return vat_id
|
||||
country_code = str(country_code)
|
||||
if is_eu_country(country_code):
|
||||
return _validate_vat_id_EU(vat_id, country_code)
|
||||
try:
|
||||
return _validate_vat_id_EU(vat_id, country_code)
|
||||
except VATIDTemporaryError:
|
||||
if requester_id and requester_id.startswith("DE") and not vat_id.startswith("DE"):
|
||||
return _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id)
|
||||
else:
|
||||
raise
|
||||
elif country_code == 'CH':
|
||||
return _validate_vat_id_CH(vat_id, country_code)
|
||||
elif country_code == 'NO':
|
||||
|
||||
+49
-19
File diff suppressed because one or more lines are too long
+138
-86
@@ -27,23 +27,26 @@ from decimal import Decimal
|
||||
from django import forms
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch, QuerySet
|
||||
from django.db.models import Prefetch, Q, QuerySet
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from pretix.base.forms.questions import (
|
||||
BaseInvoiceAddressForm, BaseInvoiceNameForm, BaseQuestionsForm,
|
||||
BaseInvoiceAddressForm, BaseInvoiceNameForm, OrderLevelQuestionsForm,
|
||||
TicketLevelQuestionsForm,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer,
|
||||
QuestionOption,
|
||||
QuestionnaireChild, QuestionOption,
|
||||
)
|
||||
from pretix.base.models.customers import AttendeeProfile
|
||||
from pretix.base.models.orders import CheckoutSession, Order
|
||||
from pretix.presale.signals import contact_form_fields_overrides
|
||||
|
||||
|
||||
class BaseQuestionsViewMixin:
|
||||
form_class = BaseQuestionsForm
|
||||
order_form_class = OrderLevelQuestionsForm
|
||||
orderposition_form_class = TicketLevelQuestionsForm
|
||||
all_optional = False
|
||||
|
||||
@cached_property
|
||||
@@ -56,6 +59,28 @@ class BaseQuestionsViewMixin:
|
||||
def question_form_kwargs(self, cr):
|
||||
return {}
|
||||
|
||||
@property
|
||||
def order_question_container(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@cached_property
|
||||
def order_questions_form(self):
|
||||
container = self.order_question_container
|
||||
if container is None:
|
||||
return None
|
||||
kwargs = {} # self.question_form_kwargs(cr)
|
||||
form = self.order_form_class(
|
||||
event=self.request.event,
|
||||
prefix='order',
|
||||
request=self.request,
|
||||
container=container,
|
||||
all_optional=self.all_optional,
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
files=(self.request.FILES if self.request.method == 'POST' else None),
|
||||
**kwargs
|
||||
)
|
||||
return form
|
||||
|
||||
@cached_property
|
||||
def forms(self):
|
||||
"""
|
||||
@@ -69,15 +94,17 @@ class BaseQuestionsViewMixin:
|
||||
orderpos = cr if isinstance(cr, OrderPosition) else None
|
||||
|
||||
kwargs = self.question_form_kwargs(cr)
|
||||
form = self.form_class(event=self.request.event,
|
||||
prefix=cr.id,
|
||||
request=self.request,
|
||||
cartpos=cartpos,
|
||||
orderpos=orderpos,
|
||||
all_optional=self.all_optional,
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
files=(self.request.FILES if self.request.method == 'POST' else None),
|
||||
**kwargs)
|
||||
form = self.orderposition_form_class(
|
||||
event=self.request.event,
|
||||
prefix=cr.id,
|
||||
request=self.request,
|
||||
cartpos=cartpos,
|
||||
orderpos=orderpos,
|
||||
all_optional=self.all_optional,
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
files=(self.request.FILES if self.request.method == 'POST' else None),
|
||||
**kwargs
|
||||
)
|
||||
form.pos = cartpos or orderpos
|
||||
form.show_copy_answers_to_addon_button = form.pos.addon_to and (
|
||||
set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or
|
||||
@@ -108,6 +135,8 @@ class BaseQuestionsViewMixin:
|
||||
question_field.initial = getattr(question_field, 'initial', None) or src['initial']
|
||||
if 'validators' in src:
|
||||
question_field.validators += src['validators']
|
||||
if 'label' in src:
|
||||
question_field.label = src['label']
|
||||
|
||||
if len(form.fields) > 0:
|
||||
formlist.append(form)
|
||||
@@ -130,8 +159,38 @@ class BaseQuestionsViewMixin:
|
||||
|
||||
def save(self):
|
||||
failed = False
|
||||
if self.order_questions_form:
|
||||
if not self.order_questions_form.is_valid():
|
||||
failed = True
|
||||
else:
|
||||
checkoutsession = self.order_question_container if isinstance(self.order_question_container, CheckoutSession) else None
|
||||
order = self.order_question_container if isinstance(self.order_question_container, Order) else None
|
||||
for k, v in self.order_questions_form.cleaned_data.items():
|
||||
if k.startswith('question_'):
|
||||
field = self.order_questions_form.fields[k]
|
||||
if hasattr(field, 'answer'):
|
||||
# We already have a cached answer object, so we don't
|
||||
# have to create a new one
|
||||
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \
|
||||
or (isinstance(v, QuerySet) and not v.exists()):
|
||||
if field.answer.file:
|
||||
field.answer.file.delete()
|
||||
field.answer.delete()
|
||||
else:
|
||||
self._save_to_answer(field, field.answer, v)
|
||||
field.answer.save()
|
||||
elif v != '' and v is not None:
|
||||
self._upsert_answer(
|
||||
field, v,
|
||||
checkoutsession=checkoutsession,
|
||||
order=order,
|
||||
question=field.question,
|
||||
)
|
||||
|
||||
for form in self.forms:
|
||||
meta_info = form.pos.meta_info_data
|
||||
cartposition = form.pos if isinstance(form.pos, CartPosition) else None
|
||||
orderposition = form.pos if isinstance(form.pos, OrderPosition) else None
|
||||
# Every form represents a CartPosition or OrderPosition with questions attached
|
||||
if not form.is_valid():
|
||||
failed = True
|
||||
@@ -140,10 +199,8 @@ class BaseQuestionsViewMixin:
|
||||
prof = AttendeeProfile.objects.filter(
|
||||
customer=self.cart_customer, pk=form.cleaned_data.get('saved_id')
|
||||
).first() or AttendeeProfile(customer=getattr(self, 'cart_customer', None))
|
||||
answers_key_to_index = {a.get('field_name'): i for i, a in enumerate(prof.answers)}
|
||||
else:
|
||||
prof = AttendeeProfile(customer=getattr(self, 'cart_customer', None))
|
||||
answers_key_to_index = {}
|
||||
|
||||
# This form was correctly filled, so we store the data as
|
||||
# answers to the questions / in the CartPosition object
|
||||
@@ -181,64 +238,19 @@ class BaseQuestionsViewMixin:
|
||||
else:
|
||||
self._save_to_answer(field, field.answer, v)
|
||||
field.answer.save()
|
||||
if isinstance(field, forms.ModelMultipleChoiceField) or isinstance(field, forms.ModelChoiceField):
|
||||
answer_value = {o.identifier: str(o) for o in field.answer.options.all()}
|
||||
elif isinstance(field, forms.BooleanField):
|
||||
answer_value = bool(field.answer.answer)
|
||||
else:
|
||||
answer_value = str(field.answer.answer)
|
||||
answer_dict = {
|
||||
'field_name': k,
|
||||
'field_label': str(field.label),
|
||||
'value': answer_value,
|
||||
'question_type': field.question.type,
|
||||
'question_identifier': field.question.identifier,
|
||||
}
|
||||
if k in answers_key_to_index:
|
||||
prof.answers[answers_key_to_index[k]] = answer_dict
|
||||
else:
|
||||
prof.answers.append(answer_dict)
|
||||
|
||||
answer_dict = self._build_answer_dict(field, field.answer, k)
|
||||
prof.store_answer(answer_dict)
|
||||
elif v != '' and v is not None:
|
||||
answer = QuestionAnswer(
|
||||
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None),
|
||||
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None),
|
||||
answer = self._upsert_answer(
|
||||
field, v,
|
||||
cartposition=cartposition,
|
||||
orderposition=orderposition,
|
||||
question=field.question,
|
||||
)
|
||||
try:
|
||||
self._save_to_answer(field, answer, v)
|
||||
answer.save()
|
||||
except IntegrityError:
|
||||
# Since we prefill ``field.answer`` at form creation time, there's a possible race condition
|
||||
# here if the users submits their save request a second time while the first one is still running,
|
||||
# thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper"
|
||||
# fix would be a transaction with select_for_update(), or at least fetching using get_or_create here
|
||||
# again. However, both of these approaches have a significant performance overhead for *all* requests,
|
||||
# while the issue happens very very rarely. So we opt for just catching the error and retrying properly.
|
||||
answer = QuestionAnswer.objects.get(
|
||||
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None),
|
||||
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None),
|
||||
question=field.question,
|
||||
)
|
||||
self._save_to_answer(field, answer, v)
|
||||
answer.save()
|
||||
|
||||
if isinstance(field, forms.ModelMultipleChoiceField) or isinstance(field, forms.ModelChoiceField):
|
||||
answer_value = {o.identifier: str(o) for o in answer.options.all()}
|
||||
elif isinstance(field, forms.BooleanField):
|
||||
answer_value = bool(answer.answer)
|
||||
else:
|
||||
answer_value = str(answer.answer)
|
||||
answer_dict = {
|
||||
'field_name': k,
|
||||
'field_label': str(field.label),
|
||||
'value': answer_value,
|
||||
'question_type': field.question.type,
|
||||
'question_identifier': field.question.identifier,
|
||||
}
|
||||
if k in answers_key_to_index:
|
||||
prof.answers[answers_key_to_index[k]] = answer_dict
|
||||
else:
|
||||
prof.answers.append(answer_dict)
|
||||
answer_dict = self._build_answer_dict(field, answer, k)
|
||||
prof.store_answer(answer_dict)
|
||||
|
||||
else:
|
||||
field = form.fields[k]
|
||||
@@ -257,10 +269,7 @@ class BaseQuestionsViewMixin:
|
||||
'question_type': None,
|
||||
'question_identifier': None,
|
||||
}
|
||||
if k in answers_key_to_index:
|
||||
prof.answers[answers_key_to_index[k]] = answer_dict
|
||||
else:
|
||||
prof.answers.append(answer_dict)
|
||||
prof.store_answer(answer_dict)
|
||||
|
||||
form.pos.meta_info = json.dumps(meta_info)
|
||||
form.pos.save()
|
||||
@@ -271,6 +280,23 @@ class BaseQuestionsViewMixin:
|
||||
|
||||
return not failed
|
||||
|
||||
def _upsert_answer(self, field, v, **answer_kwargs):
|
||||
answer = QuestionAnswer(**answer_kwargs)
|
||||
try:
|
||||
self._save_to_answer(field, answer, v)
|
||||
answer.save()
|
||||
except IntegrityError:
|
||||
# Since we prefill ``field.answer`` at form creation time, there's a possible race condition
|
||||
# here if the users submits their save request a second time while the first one is still running,
|
||||
# thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper"
|
||||
# fix would be a transaction with select_for_update(), or at least fetching using get_or_create here
|
||||
# again. However, both of these approaches have a significant performance overhead for *all* requests,
|
||||
# while the issue happens very very rarely. So we opt for just catching the error and retrying properly.
|
||||
answer = QuestionAnswer.objects.get(**answer_kwargs)
|
||||
self._save_to_answer(field, answer, v)
|
||||
answer.save()
|
||||
return answer
|
||||
|
||||
def _save_to_answer(self, field, answer, value):
|
||||
if isinstance(field, forms.ModelMultipleChoiceField):
|
||||
answstr = ", ".join([str(o) for o in value])
|
||||
@@ -294,6 +320,21 @@ class BaseQuestionsViewMixin:
|
||||
else:
|
||||
answer.answer = value
|
||||
|
||||
def _build_answer_dict(self, field, answer, k):
|
||||
if isinstance(field, forms.ModelMultipleChoiceField) or isinstance(field, forms.ModelChoiceField):
|
||||
answer_value = {o.identifier: str(o) for o in answer.options.all()}
|
||||
elif isinstance(field, forms.BooleanField):
|
||||
answer_value = bool(answer.answer)
|
||||
else:
|
||||
answer_value = str(answer.answer)
|
||||
return {
|
||||
'field_name': k,
|
||||
'field_label': str(field.label),
|
||||
'value': answer_value,
|
||||
'question_type': field.question.type,
|
||||
'question_identifier': field.question.identifier,
|
||||
}
|
||||
|
||||
|
||||
class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
invoice_form_class = BaseInvoiceAddressForm
|
||||
@@ -307,27 +348,37 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
qqs = self.request.event.questions.all()
|
||||
qqs = self.request.event.questionnaires.all()
|
||||
if self.only_user_visible:
|
||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False)
|
||||
qqs = qqs.filter(type='PS')
|
||||
else:
|
||||
qqs = qqs.filter(type__startswith='P')
|
||||
qqs = qqs.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.order.sales_channel.identifier)
|
||||
)
|
||||
return list(self.order.positions.select_related(
|
||||
'item', 'variation'
|
||||
).prefetch_related(
|
||||
Prefetch('answers',
|
||||
QuestionAnswer.objects.prefetch_related('options'),
|
||||
to_attr='answerlist'),
|
||||
Prefetch('item__questions',
|
||||
Prefetch('item__questionnaires',
|
||||
qqs.prefetch_related(
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
Prefetch('children', QuestionnaireChild.objects.prefetch_related(
|
||||
Prefetch('user_datafield', Question.objects.prefetch_related(
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
))
|
||||
),
|
||||
to_attr='childlist')
|
||||
),
|
||||
to_attr='relevant_questionnaires')
|
||||
))
|
||||
|
||||
@cached_property
|
||||
@@ -397,6 +448,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['order'] = self.order
|
||||
ctx['order_questions_form'] = self.order_questions_form
|
||||
ctx['formgroups'] = self.formdict.items()
|
||||
ctx['invoice_form'] = self.invoice_form
|
||||
ctx['invoice_address_asked'] = self.address_asked
|
||||
|
||||
@@ -26,6 +26,7 @@ from django.core import signing
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,6 +61,7 @@ def redir_view(request):
|
||||
u = urllib.parse.urlparse(url)
|
||||
return render(request, 'pretixbase/redirect.html', {
|
||||
'hostname': u.hostname,
|
||||
'bold_hostname': format_html("<strong>{}</strong>", u.hostname),
|
||||
'url': url,
|
||||
})
|
||||
|
||||
|
||||
@@ -172,7 +172,9 @@ class CachedFileInput(forms.ClearableFileInput):
|
||||
from ...base.models import CachedFile
|
||||
v = super().value_from_datadict(data, files, name)
|
||||
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
|
||||
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
if not v.allowed_for_session(self.request):
|
||||
v = None
|
||||
return v
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
@@ -244,6 +246,11 @@ class ExtFileField(ExtValidationMixin, SizeFileField):
|
||||
class CachedFileField(ExtFileField):
|
||||
widget = CachedFileInput
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop("request", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.widget.request = self.request
|
||||
|
||||
def to_python(self, data):
|
||||
from ...base.models import CachedFile
|
||||
|
||||
@@ -271,6 +278,8 @@ class CachedFileField(ExtFileField):
|
||||
filename=data.name,
|
||||
type=data.content_type,
|
||||
)
|
||||
if self.request:
|
||||
cf.bind_to_session(self.request) # no salt because we want direct web access
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
data._uploaded_to = cf
|
||||
@@ -294,6 +303,8 @@ class CachedFileField(ExtFileField):
|
||||
filename=data.name,
|
||||
type=data.content_type,
|
||||
)
|
||||
if self.request:
|
||||
cf.bind_to_session(self.request) # no salt because we want direct web access
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
data._uploaded_to = cf
|
||||
|
||||
@@ -600,6 +600,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
||||
'imprint_url',
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'event_list_empty_text',
|
||||
'voucher_explanation_text',
|
||||
'checkout_success_text',
|
||||
'show_dates_on_frontpage',
|
||||
@@ -734,6 +735,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
||||
del self.fields['event_list_available_only']
|
||||
del self.fields['event_list_filters']
|
||||
del self.fields['event_calendar_future_only']
|
||||
del self.fields['event_list_empty_text']
|
||||
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
|
||||
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
|
||||
], key=lambda a: a[0])
|
||||
@@ -853,9 +855,60 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'payment_term_accept_late',
|
||||
'payment_pending_hidden',
|
||||
'payment_explanation',
|
||||
'payment_choice_postpone_allowed_channels',
|
||||
'tax_rule_payment',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
channels = list(self.obj.organizer.sales_channels.all())
|
||||
self.fields['payment_choice_postpone_allowed_channels'].choices = [
|
||||
(c.identifier, c.label) for c in channels
|
||||
if c.type_instance.payment_restrictions_supported
|
||||
]
|
||||
|
||||
self.term_channel_fields = {}
|
||||
for c in channels:
|
||||
if c.type_instance.payment_restrictions_supported and c.identifier != "web":
|
||||
# At the moment, it seems sufficient to allow this for the same channel types as other payment settings
|
||||
# We can always introduce more flags later if needed
|
||||
suffix = '_' + c.identifier.replace(".", "_")
|
||||
self.term_channel_fields[c] = [
|
||||
'payment_term_mode' + suffix,
|
||||
'payment_term_days' + suffix,
|
||||
'payment_term_minutes' + suffix,
|
||||
]
|
||||
self.fields['payment_term_mode' + suffix] = DEFAULTS['payment_term_mode']['form_class'](
|
||||
label=_("Payment term"),
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
choices=(
|
||||
('', _("same as above")),
|
||||
('days', _("different payment term in days")),
|
||||
('minutes', _("different payment term in minutes"))
|
||||
),
|
||||
)
|
||||
self.fields['payment_term_days' + suffix] = DEFAULTS['payment_term_days']['form_class'](
|
||||
required=False,
|
||||
**DEFAULTS['payment_term_days']['form_kwargs'](suffix, 1),
|
||||
)
|
||||
self.fields['payment_term_minutes' + suffix] = DEFAULTS['payment_term_minutes']['form_class'](
|
||||
required=False,
|
||||
**DEFAULTS['payment_term_minutes']['form_kwargs'](suffix, 2),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
for c in self.term_channel_fields.keys():
|
||||
suffix = '_' + c.identifier.replace(".", "_")
|
||||
mode = self.cleaned_data.get(f'payment_term_mode{suffix}')
|
||||
if mode == 'days' and self.cleaned_data.get(f'payment_term_days{suffix}') is None:
|
||||
raise ValidationError({f'payment_term_days{suffix}': _("This field is required.")})
|
||||
if mode == 'minutes' and self.cleaned_data.get(f'payment_term_minutes{suffix}') is None:
|
||||
raise ValidationError({f'payment_term_minutes{suffix}': _("This field is required.")})
|
||||
return data
|
||||
|
||||
def clean_payment_term_days(self):
|
||||
value = self.cleaned_data.get('payment_term_days')
|
||||
if self.cleaned_data.get('payment_term_mode') == 'days' and value is None:
|
||||
|
||||
@@ -105,12 +105,12 @@ class GlobalSettingsForm(SettingsForm):
|
||||
domain=settings.SITE_URL
|
||||
)
|
||||
)),
|
||||
('widget_vite_origins', forms.CharField(
|
||||
('widget_vue2_origins', forms.CharField(
|
||||
widget=forms.Textarea(attrs={'rows': '3'}),
|
||||
required=False,
|
||||
# Not translated on purpose, this is a temporary feature and contains too many special case words
|
||||
label="Vite widget origins",
|
||||
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the new vite-based widget.",
|
||||
label="Vue2 widget origins",
|
||||
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the old vue2-based widget.",
|
||||
))
|
||||
])
|
||||
responses = register_global_settings.send(self)
|
||||
|
||||
@@ -153,11 +153,19 @@ class QuestionForm(I18nModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['items'].queryset = self.instance.event.items.all()
|
||||
self.fields['items'].required = True
|
||||
if self.instance.container_type == Question.ContainerType.ORDERPOSITION:
|
||||
self.fields['items'].queryset = self.instance.event.items.all()
|
||||
self.fields['items'].required = True
|
||||
else:
|
||||
del self.fields['items']
|
||||
del self.fields['ask_during_checkin']
|
||||
del self.fields['show_during_checkin']
|
||||
del self.fields['print_on_invoice']
|
||||
self.fields['dependency_question'].widget.attrs['data-container-type'] = self.instance.container_type
|
||||
self.fields['dependency_question'].queryset = self.instance.event.questions.filter(
|
||||
type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE),
|
||||
ask_during_checkin=False
|
||||
ask_during_checkin=False,
|
||||
container_type=self.instance.container_type,
|
||||
)
|
||||
if self.instance.pk:
|
||||
self.fields['dependency_question'].queryset = self.fields['dependency_question'].queryset.exclude(
|
||||
@@ -243,6 +251,7 @@ class QuestionForm(I18nModelForm):
|
||||
'valid_date_min',
|
||||
'valid_date_max',
|
||||
'valid_file_portrait',
|
||||
'valid_string_length_min',
|
||||
'valid_string_length_max',
|
||||
]
|
||||
widgets = {
|
||||
|
||||
@@ -87,6 +87,7 @@ class RRuleForm(forms.Form):
|
||||
('1', pgettext_lazy('rrule', 'first')),
|
||||
('2', pgettext_lazy('rrule', 'second')),
|
||||
('3', pgettext_lazy('rrule', 'third')),
|
||||
('4', pgettext_lazy('rrule', 'fourth')),
|
||||
('-1', pgettext_lazy('rrule', 'last')),
|
||||
],
|
||||
required=False
|
||||
@@ -134,6 +135,7 @@ class RRuleForm(forms.Form):
|
||||
('1', pgettext_lazy('rrule', 'first')),
|
||||
('2', pgettext_lazy('rrule', 'second')),
|
||||
('3', pgettext_lazy('rrule', 'third')),
|
||||
('4', pgettext_lazy('rrule', 'fourth')),
|
||||
('-1', pgettext_lazy('rrule', 'last')),
|
||||
],
|
||||
required=False
|
||||
|
||||
@@ -94,14 +94,14 @@ class SubEventBulkForm(SubEventForm):
|
||||
label=_('Start of presale'),
|
||||
help_text=_('Optional. No products will be sold before this date.'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
limit_choices=('event__date_from', 'event__date_to'),
|
||||
)
|
||||
rel_presale_end = RelativeDateTimeField(
|
||||
label=_('End of presale'),
|
||||
help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale '
|
||||
'will end after the end date of your event.'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
limit_choices=('event__date_from', 'event__date_to'),
|
||||
)
|
||||
skip_if_overlap = forms.BooleanField(
|
||||
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
|
||||
@@ -333,12 +333,12 @@ class BulkSubEventItemForm(SubEventItemForm):
|
||||
rel_available_from = RelativeDateTimeField(
|
||||
label=_('Available from'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
limit_choices=('event__date_from', 'event__date_to'),
|
||||
)
|
||||
rel_available_until = RelativeDateTimeField(
|
||||
label=_('Available until'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
limit_choices=('event__date_from', 'event__date_to'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -355,12 +355,12 @@ class BulkSubEventItemVariationForm(SubEventItemVariationForm):
|
||||
rel_available_from = RelativeDateTimeField(
|
||||
label=_('Available from'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
limit_choices=('event__date_from', 'event__date_to'),
|
||||
)
|
||||
rel_available_until = RelativeDateTimeField(
|
||||
label=_('Available_until'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
limit_choices=('event__date_from', 'event__date_to'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -861,6 +861,9 @@ class OrganizerPluginStateLogEntryType(LogEntryType):
|
||||
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
|
||||
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
|
||||
'pretix.event.question.option.changed': _('An answer option has been changed.'),
|
||||
'pretix.event.questionnaire.added': _('A questionnaire has been created.'),
|
||||
'pretix.event.questionnaire.deleted': _('A questionnaire has been deleted.'),
|
||||
'pretix.event.questionnaire.changed': _('A questionnaire has been changed.'),
|
||||
'pretix.event.permissions.added': _('A user has been added to the event team.'),
|
||||
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
|
||||
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
|
||||
|
||||
@@ -182,12 +182,12 @@ def get_event_navigation(request: HttpRequest):
|
||||
'active': 'event.items.categories' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Questions'),
|
||||
'url': reverse('control:event.items.questions', kwargs={
|
||||
'label': _('Questionnaires'),
|
||||
'url': reverse('control:event.items.questionnaires', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.questions' in url.url_name,
|
||||
'active': 'event.items.questionnaires' in url.url_name or 'event.items.questions' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Discounts'),
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<script type="text/javascript" src="{% static "leaflet/leaflet.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/geo.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/reldate.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "sortable/Sortable.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load getitem %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Payment settings" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal form-plugins">
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
@@ -71,20 +72,44 @@
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
{% bootstrap_field form.payment_term_mode layout="control" %}
|
||||
{% bootstrap_field form.payment_term_days layout="control" %}
|
||||
{% bootstrap_field form.payment_term_weekdays layout="control" %}
|
||||
{% bootstrap_field form.payment_term_minutes layout="control" %}
|
||||
{% bootstrap_field form.payment_term_weekdays layout="control" %}
|
||||
{% bootstrap_field form.payment_term_last layout="control" %}
|
||||
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
|
||||
{% trans "days" context "unit" as days %}
|
||||
{% bootstrap_field form.payment_term_expire_delay_days layout="control" addon_after=days %}
|
||||
{% bootstrap_field form.payment_term_accept_late layout="control" %}
|
||||
{% bootstrap_field form.payment_pending_hidden layout="control" %}
|
||||
|
||||
{% for c, fields in form.term_channel_fields.items %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ c.type_instance.verbose_name }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ c.type_instance.verbose_name }}"></span>
|
||||
{% endif %}
|
||||
{{ c.label }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for f in fields %}
|
||||
{% bootstrap_field form|getitem:f layout="control" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced" %}</legend>
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
{% bootstrap_field form.tax_rule_payment layout="control" %}
|
||||
{% bootstrap_field form.payment_explanation layout="control" %}
|
||||
{% bootstrap_field form.payment_choice_postpone_allowed_channels layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if "event.settings.payment:write" in request.eventpermset %}
|
||||
|
||||
@@ -275,6 +275,9 @@
|
||||
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
||||
{% if sform.event_list_empty_text %}
|
||||
{% bootstrap_field sform.event_list_empty_text layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h4>{% trans "Order details" %}</h4>
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
{% blocktrans with url=edit_url|add:"#tab-0-1-open" %}If you want to keep the answers, <a href="{{url}}">edit the question</a> and set it to hidden.{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% for item in dependent %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item }}</a></li>
|
||||
{% endfor %}
|
||||
<ul>
|
||||
{% for item in dependent %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.pk %}">{{ item }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
|
||||
@@ -12,142 +12,120 @@
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
{% if question %}
|
||||
<h1>{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}</h1>
|
||||
<h1>{% blocktrans with name=question.question %}Data field: {{ name }}{% endblocktrans %}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Question" %}</h1>
|
||||
<h1>{% trans "Data field" %}</h1>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
{% bootstrap_field form.items layout="control" %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
<div class="alert alert-info alert-required-boolean">
|
||||
{% blocktrans trimmed %}
|
||||
If you mark a Yes/No question as required, it means that the user has to select Yes and No is not
|
||||
accepted. If you want to allow both options, do not make this field required.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div id="valid-number">
|
||||
{% bootstrap_field form.valid_number_min layout="control" %}
|
||||
{% bootstrap_field form.valid_number_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-date">
|
||||
{% bootstrap_field form.valid_date_min layout="control" %}
|
||||
{% bootstrap_field form.valid_date_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-datetime">
|
||||
{% bootstrap_field form.valid_datetime_min layout="control" %}
|
||||
{% bootstrap_field form.valid_datetime_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-string">
|
||||
{% bootstrap_field form.valid_string_length_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-file">
|
||||
{% bootstrap_field form.valid_file_portrait layout="control" %}
|
||||
</div>
|
||||
<div id="answer-options">
|
||||
<h3>{% trans "Answer options" %}</h3>
|
||||
<noscript>
|
||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||
</noscript>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% blocktrans trimmed with id=form.instance.identifier %}
|
||||
Answer option {{ id }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% trans "New answer option" %}
|
||||
</span>
|
||||
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced" %}</legend>
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_dependency_question">
|
||||
{% trans "Question dependency" %}
|
||||
<br><span class="optional">{% trans "Optional" context "form" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.dependency_question layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<script type="text/plain" id="dependency_value_val">{{ form.instance.dependency_values|escapejson_dumps }}</script>
|
||||
{% bootstrap_field form.dependency_values layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
<div id="valid-number">
|
||||
{% bootstrap_field form.valid_number_min layout="control" %}
|
||||
{% bootstrap_field form.valid_number_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-date">
|
||||
{% bootstrap_field form.valid_date_min layout="control" %}
|
||||
{% bootstrap_field form.valid_date_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-datetime">
|
||||
{% bootstrap_field form.valid_datetime_min layout="control" %}
|
||||
{% bootstrap_field form.valid_datetime_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-string">
|
||||
{% bootstrap_field form.valid_string_length_min layout="control" %}
|
||||
{% bootstrap_field form.valid_string_length_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-file">
|
||||
{% bootstrap_field form.valid_file_portrait layout="control" %}
|
||||
</div>
|
||||
<div id="answer-options">
|
||||
<h3>{% trans "Answer options" %}</h3>
|
||||
<noscript>
|
||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||
</noscript>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% blocktrans trimmed with id=form.instance.identifier %}
|
||||
Answer option {{ id }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% trans "New answer option" %}
|
||||
</span>
|
||||
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% if form.ask_during_checkin %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% if form.show_during_checkin %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% if form.print_on_invoice %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load icon %}
|
||||
{% load compress %}
|
||||
{% load vite %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Questionnaires" %}
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% trans "Questionnaires" %}
|
||||
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default pull-right">
|
||||
{% icon "wrench" %} {% trans "Manage data fields" %}
|
||||
</a>
|
||||
</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Questionaires allow your attendees to fill in additional data about their ticket. If you provide food, one
|
||||
example might be to ask your users about dietary requirements.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
TODO(questionnaires) : add more specific explanation of the questionnaire concept.
|
||||
</p>
|
||||
|
||||
{{ request.event.settings.locales|json_script:"event_locales" }}
|
||||
{{ questionnaire_type_choices|json_script:"questionnaire_type_choices" }}
|
||||
|
||||
{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=0 as datafield_edit_url %}
|
||||
{{ datafield_edit_url|json_script:"datafield_edit_url" }}
|
||||
|
||||
<div id="questionnaires-editor">
|
||||
<!-- Vue app mount point -->
|
||||
</div>
|
||||
|
||||
{% vite_hmr %}
|
||||
{% vite_asset "src/pretix/static/pretixcontrol/js/ui/questionnaires/index.ts" %}
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Questions" %}{% endblock %}
|
||||
{% block title %}{% trans "Data fields" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Questions" %}</h1>
|
||||
<h1>{% trans "Data fields" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Questions allow your attendees to fill in additional data about their ticket. If you provide food, one
|
||||
@@ -10,49 +10,52 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% csrf_token %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{% if request.event.settings.feature_flag_order_level_questions %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new per-ticket data field" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new order-level data field" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% trans "Per-ticket data fields" %}</h2>
|
||||
<p>{% trans "These data field can be used on individual tickets." %}</p>
|
||||
{% else %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new data field" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Question" %}</th>
|
||||
<th>{% trans "Internal name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<th class="action-col-2"></th>
|
||||
{% endif %}
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% for q in questions %}
|
||||
<tr data-dnd-id="{{ q.id }}">
|
||||
<tbody>
|
||||
{% for q in questions %}{% if q.container_type == "P" %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
{% if q.pk %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||
{% endif %}
|
||||
{{ q.question }}
|
||||
{% if q.pk %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||
{{ q.question }}
|
||||
</a>
|
||||
</strong><br>
|
||||
<small class="text-muted">{{ q.identifier }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk %}
|
||||
{{ q.get_type_display }}
|
||||
{% else %}
|
||||
{% trans "System question" %}
|
||||
{% endif %}
|
||||
{{ q.get_type_display }}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.required %}
|
||||
@@ -63,47 +66,93 @@
|
||||
{% if q.pk and q.ask_during_checkin %}
|
||||
<span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk and q.hidden %}
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk %}
|
||||
<ul>
|
||||
{% for item in q.items.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<small>{% trans "All personalized products" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<td class="dnd-container">
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right flip">
|
||||
{% if q.pk %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if 'event.settings.general:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if request.event.settings.feature_flag_order_level_questions %}
|
||||
<h2>
|
||||
{% trans "Per-order data fields" %}
|
||||
<small><span class="label label-info" title="
|
||||
{% trans "This functionality is in active development and expected to change significantly over the coming months." %}
|
||||
{% trans "In pretixPOS, per-order data fields are currently not supported and will not be displayed." %}
|
||||
" data-toggle="tooltip">
|
||||
<span class="fa fa-flask" aria-hidden="true"></span>
|
||||
{% trans "Experimental feature" %}
|
||||
</span></small>
|
||||
</h2>
|
||||
<p>{% trans "These data fields are asked once per order." %}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Internal name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in questions %}{% if q.container_type == "O" %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
{{ q.question }}
|
||||
</strong><br>
|
||||
<small class="text-muted">{{ q.identifier }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ q.get_type_display }}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.required %}
|
||||
<span class="fa fa-exclamation-circle text-muted" data-toggle="tooltip" title="{% trans "Required question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk and q.ask_during_checkin %}
|
||||
<span class="fa fa-check-square text-muted" data-toggle="tooltip" title="{% trans "Ask during check-in" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk and q.hidden %}
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if q.pk %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if 'event.settings.general:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -35,6 +35,18 @@
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if order_questions_form.fields %}
|
||||
<details class="panel panel-default" open>
|
||||
<summary class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<strong>{% trans "Additional order information" %}</strong>
|
||||
</h4>
|
||||
</summary>
|
||||
<div class="panel-body">
|
||||
{% bootstrap_form order_questions_form layout="horizontal" %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% for pos, forms in formgroups %}
|
||||
<details class="panel panel-default" open>
|
||||
<summary class="panel-heading">
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{% load safelink %}
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% if answer %}
|
||||
{% if answer.file %}
|
||||
<span class="fa fa-file"></span>
|
||||
<a href="{{ answer.backend_file_url }}?token={% answer_token request answer %}">
|
||||
{{ answer.file_name }}
|
||||
</a>
|
||||
<span class="label label-danger" data-toggle="tooltip"
|
||||
title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">
|
||||
{% trans "UNSAFE" %}
|
||||
</span>
|
||||
{% if answer.is_image %}
|
||||
<br>
|
||||
<a href="{{ answer.backend_file_url }}?token={% answer_token request answer %}" data-lightbox="order"
|
||||
class="answer-thumb">
|
||||
<img src="{{ answer.backend_file_url }}?token={% answer_token request answer %}">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif question.type == "M" %}
|
||||
{{ answer.to_string_i18n|rich_text_snippet }}
|
||||
{% elif question.type %}
|
||||
{{ answer.to_string_i18n|rich_text_snippet }}
|
||||
{% else %}
|
||||
{{ answer|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
@@ -427,6 +427,14 @@
|
||||
</form>
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if order.answers.all %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% for ans in order.answers.all %}
|
||||
<dt>{{ ans.question.internal_name|default:ans.question.question }}</dt>
|
||||
<dd>{% include "pretixcontrol/order/fragment_question_answer.html" with request=request question=ans.question answer=ans %}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
@@ -602,60 +610,9 @@
|
||||
{% endif %}
|
||||
{% if line.has_questions %}
|
||||
<dl>
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
|
||||
<dt>{% trans "Attendee name" %}</dt>
|
||||
<dd>{% if line.attendee_name %}{{ line.attendee_name_all_components }}{% else %}
|
||||
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
|
||||
<dt>{% trans "Attendee email" %}</dt>
|
||||
<dd>
|
||||
{% if line.attendee_email %}
|
||||
{{ line.attendee_email }}
|
||||
{% if not line.addon_to %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
|
||||
class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee company" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee address" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.street or line.zipcode or line.city or line.country %}
|
||||
{{ line.street|default_if_none:""|linebreaksbr }}<br>
|
||||
{{ line.zipcode|default_if_none:"" }} {{ line.city|default_if_none:"" }}<br>
|
||||
{% if line.state %}{{ line.state_for_address }}<br>{% endif %}
|
||||
{{ line.country.name|default_if_none:"" }}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% for q in line.questions %}
|
||||
<dt>
|
||||
{{ q.question }}
|
||||
{{ q.label }}
|
||||
{% if q.ask_during_checkin %}
|
||||
<span class="fa fa-qrcode text-muted"
|
||||
data-toggle="tooltip"
|
||||
@@ -664,6 +621,7 @@
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd>
|
||||
{#
|
||||
{% if q.answer %}
|
||||
{% if q.answer.file %}
|
||||
<span class="fa fa-file"></span>
|
||||
@@ -683,12 +641,29 @@
|
||||
{% endif %}
|
||||
{% elif q.type == "M" %}
|
||||
{{ q.answer.to_string_i18n|rich_text_snippet }}
|
||||
{% else %}
|
||||
{% elif q.type %}
|
||||
{{ q.answer.to_string_i18n|linebreaksbr }}
|
||||
{% else %}<!-- TODO(questionnaires): proper separation of QuestionAnswer objects and system answers...... -->
|
||||
{{ q.answer|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
#}
|
||||
{% include "pretixcontrol/order/fragment_question_answer.html" with request=request question=q answer=q.answer %}
|
||||
{% if q.system_datafield == "attendee_email" and not line.addon_to %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
|
||||
class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% for q in line.additional_fields %}
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<span class="fa fa-fw {% if d.last_seen and d.last_seen.is_recent %}fa-check-circle text-success{% else %}fa-circle text-danger{% endif %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% if d.last_seen %}{% blocktrans with time=d.last_seen.last_seen|date:"SHORT_DATETIME_FORMAT" %}Last seen: {{ time }}{% endblocktrans %}{% else %}{% trans "No recent contact" %}{% endif %}"
|
||||
></span>
|
||||
{{ d.device_id }}
|
||||
</td>
|
||||
<td>
|
||||
@@ -125,6 +129,7 @@
|
||||
<del>{% endif %}
|
||||
{{ d.name }}
|
||||
{% if d.revoked %}</del>{% endif %}
|
||||
|
||||
{% if d.gate %}
|
||||
<br>
|
||||
<small class="text-muted">{{ d.gate.name }}</small>
|
||||
|
||||
@@ -350,6 +350,7 @@ urlpatterns = [
|
||||
re_path(r'^questions/(?P<question>\d+)/change$', item.QuestionUpdate.as_view(),
|
||||
name='event.items.questions.edit'),
|
||||
re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
||||
re_path(r'^questionnaires/$', item.QuestionnairesEditor.as_view(), name='event.items.questionnaires'),
|
||||
re_path(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
|
||||
re_path(r'^quotas/bulk_action$', item.QuotaBulkAction.as_view(), name='event.items.quotas.bulkaction'),
|
||||
re_path(r'^quotas/bulk_edit$', item.QuotaBulkUpdateView.as_view(), name='event.items.quotas.bulkedit'),
|
||||
|
||||
@@ -64,6 +64,7 @@ from pretix.base.forms.auth import (
|
||||
)
|
||||
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.http import get_client_ip, redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||
from pretix.helpers.security import handle_login_source, session_login
|
||||
@@ -395,15 +396,17 @@ class Recover(TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.form.is_valid():
|
||||
try:
|
||||
user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native')
|
||||
except User.DoesNotExist:
|
||||
return self.invalid('unknownuser')
|
||||
if not default_token_generator.check_token(user, self.request.GET.get('token')):
|
||||
return self.invalid('invalid')
|
||||
user.set_password(self.form.cleaned_data['password'])
|
||||
user.needs_password_change = False
|
||||
user.save()
|
||||
with transaction.atomic():
|
||||
# Check token in transaction to prevent race condition
|
||||
try:
|
||||
user = User.objects.select_for_update(of=OF_SELF).get(id=self.request.GET.get('id'), auth_backend='native')
|
||||
except User.DoesNotExist:
|
||||
return self.invalid('unknownuser')
|
||||
if not default_token_generator.check_token(user, self.request.GET.get('token')):
|
||||
return self.invalid('invalid')
|
||||
user.set_password(self.form.cleaned_data['password'])
|
||||
user.needs_password_change = False
|
||||
user.save()
|
||||
messages.success(request, _('You can now login using your new password.'))
|
||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||
|
||||
|
||||
@@ -163,6 +163,9 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
|
||||
i = modelcopy(self.copy_from)
|
||||
i.pk = None
|
||||
kwargs['instance'] = i
|
||||
kwargs["initial"]["limit_sales_channels"] = self.copy_from.limit_sales_channels.all()
|
||||
kwargs["initial"]["condition_limit_products"] = self.copy_from.condition_limit_products.all()
|
||||
kwargs["initial"]["benefit_limit_products"] = self.copy_from.benefit_limit_products.all()
|
||||
else:
|
||||
kwargs['instance'] = Discount(event=self.request.event)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import FormView, ListView, View
|
||||
from django.views.generic import FormView, ListView, TemplateView, View
|
||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
from django_countries.fields import Country
|
||||
|
||||
@@ -426,7 +426,7 @@ def reorder_categories(request, organizer, event):
|
||||
|
||||
|
||||
FakeQuestion = namedtuple(
|
||||
'FakeQuestion', 'id question position required'
|
||||
'FakeQuestion', 'id question position required container_type'
|
||||
)
|
||||
|
||||
|
||||
@@ -436,140 +436,7 @@ class QuestionList(ListView):
|
||||
template_name = 'pretixcontrol/items/questions.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.prefetch_related('items')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
questions = []
|
||||
|
||||
if self.request.event.settings.attendee_names_asked:
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='attendee_name_parts',
|
||||
question=_('Attendee name'),
|
||||
position=self.request.event.settings.system_question_order.get(
|
||||
'attendee_name_parts', 0
|
||||
),
|
||||
required=self.request.event.settings.attendee_names_required,
|
||||
)
|
||||
)
|
||||
|
||||
if self.request.event.settings.attendee_emails_asked:
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='attendee_email',
|
||||
question=_('Attendee email'),
|
||||
position=self.request.event.settings.system_question_order.get(
|
||||
'attendee_email', 0
|
||||
),
|
||||
required=self.request.event.settings.attendee_emails_required,
|
||||
)
|
||||
)
|
||||
|
||||
if self.request.event.settings.attendee_company_asked:
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='company',
|
||||
question=_('Company'),
|
||||
position=self.request.event.settings.system_question_order.get(
|
||||
'company', 0
|
||||
),
|
||||
required=self.request.event.settings.attendee_company_required,
|
||||
)
|
||||
)
|
||||
|
||||
if self.request.event.settings.attendee_addresses_asked:
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='street',
|
||||
question=_('Street'),
|
||||
position=self.request.event.settings.system_question_order.get(
|
||||
'street', 0
|
||||
),
|
||||
required=self.request.event.settings.attendee_addresses_required,
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='zipcode',
|
||||
question=_('ZIP code'),
|
||||
position=self.request.event.settings.system_question_order.get(
|
||||
'zipcode', 0
|
||||
),
|
||||
required=self.request.event.settings.attendee_addresses_required,
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='city',
|
||||
question=_('City'),
|
||||
position=self.request.event.settings.system_question_order.get(
|
||||
'city', 0
|
||||
),
|
||||
required=self.request.event.settings.attendee_addresses_required,
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
FakeQuestion(
|
||||
id='country',
|
||||
question=_('Country'),
|
||||
position=self.request.event.settings.system_question_order.get(
|
||||
'country', 0
|
||||
),
|
||||
required=self.request.event.settings.attendee_addresses_required,
|
||||
)
|
||||
)
|
||||
|
||||
questions += list(ctx['questions'])
|
||||
questions.sort(key=lambda q: q.position)
|
||||
ctx['questions'] = questions
|
||||
return ctx
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@event_permission_required("event.items:write")
|
||||
@require_http_methods(["POST"])
|
||||
def reorder_questions(request, organizer, event):
|
||||
try:
|
||||
ids = json.loads(request.body.decode('utf-8'))['ids']
|
||||
except (JSONDecodeError, KeyError, ValueError):
|
||||
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
||||
|
||||
# filter system_questions - normal questions are int/digit, system_questions strings
|
||||
custom_question_ids = [i for i in ids if i.isdigit()]
|
||||
input_questions = list(request.event.questions.filter(id__in=custom_question_ids))
|
||||
|
||||
if len(input_questions) != len(custom_question_ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
|
||||
if len(input_questions) != request.event.questions.count():
|
||||
raise Http404(_("Not all objects have been selected."))
|
||||
|
||||
for q in input_questions:
|
||||
pos = ids.index(str(q.pk))
|
||||
if pos != q.position: # Save unneccessary UPDATE queries
|
||||
q.position = pos
|
||||
q.save(update_fields=['position'])
|
||||
q.log_action(
|
||||
'pretix.event.question.reordered', user=request.user, data={
|
||||
'position': pos,
|
||||
}
|
||||
)
|
||||
|
||||
system_question_order = {}
|
||||
for s in ('attendee_name_parts', 'attendee_email', 'company', 'street', 'zipcode', 'city', 'country'):
|
||||
if s in ids:
|
||||
system_question_order[s] = ids.index(s)
|
||||
else:
|
||||
system_question_order[s] = -1
|
||||
request.event.settings.system_question_order = system_question_order
|
||||
request.event.log_action(
|
||||
'pretix.event.settings', user=request.user, data={
|
||||
'system_question_order': system_question_order,
|
||||
}
|
||||
)
|
||||
|
||||
return HttpResponse()
|
||||
return self.request.event.questions
|
||||
|
||||
|
||||
class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
@@ -805,6 +672,9 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['instance'] = Question(event=self.request.event)
|
||||
kwargs['instance'].container_type = self.request.GET.get('container_type', Question.ContainerType.ORDERPOSITION)
|
||||
if kwargs['instance'].container_type not in Question.ContainerType.values:
|
||||
raise PermissionDenied
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
@@ -836,6 +706,11 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
||||
return ret
|
||||
|
||||
|
||||
class QuestionnairesEditor(EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/items/questionnaires.html'
|
||||
|
||||
|
||||
class QuotaQueryMixin:
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -269,8 +269,8 @@ class MailSettingsSetupView(TemplateView):
|
||||
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
|
||||
dmarc_record = get_dmarc_record(hostname)
|
||||
if not dmarc_record:
|
||||
spf_warning = _(
|
||||
'We did not find DMARC record for your domain. This means that there is a very high chance '
|
||||
dmarc_warning = _(
|
||||
'We did not find a DMARC record for your domain. This means that there is a very high chance '
|
||||
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
|
||||
'of your domain.'
|
||||
)
|
||||
|
||||
@@ -382,7 +382,7 @@ class OrderOverpaidRefundBulkActionView(BaseOrderBulkActionView):
|
||||
'provider': refund.provider,
|
||||
}, user=self.request.user)
|
||||
payment.payment_provider.execute_refund(refund)
|
||||
return True
|
||||
return bool(proposals)
|
||||
except (ValueError, PaymentException):
|
||||
return False
|
||||
|
||||
@@ -1646,7 +1646,8 @@ class OrderCheckVATID(OrderView):
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
|
||||
requester_id = self.request.event.settings.invoice_address_from_vat_id
|
||||
normalized_id = validate_vat_id(ia.vat_id, str(ia.country), requester_id)
|
||||
with transaction.atomic():
|
||||
ia.vat_id_validated = True
|
||||
ia.vat_id = normalized_id
|
||||
@@ -2008,7 +2009,10 @@ class OrderChange(OrderView):
|
||||
'issued_gift_cards',
|
||||
'addons',
|
||||
).annotate(
|
||||
_seat_allowed=Exists(SeatCategoryMapping.objects.filter(subevent=OuterRef("subevent"), product=OuterRef("item")))
|
||||
_seat_allowed=Exists(SeatCategoryMapping.objects.filter(
|
||||
subevent=OuterRef("subevent") if self.request.event.has_subevents else None,
|
||||
product=OuterRef("item"))
|
||||
)
|
||||
))
|
||||
for p in positions:
|
||||
p.form = OrderPositionChangeForm(
|
||||
@@ -2226,6 +2230,10 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
|
||||
only_user_visible = False
|
||||
all_optional = True
|
||||
|
||||
@property
|
||||
def order_question_container(self):
|
||||
return self.order
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['other_form'] = self.other_form
|
||||
@@ -2587,7 +2595,11 @@ class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
|
||||
answid = kwargs.get('answer')
|
||||
token = request.GET.get('token', '')
|
||||
|
||||
answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid)
|
||||
answer = get_object_or_404(
|
||||
QuestionAnswer,
|
||||
Q(orderposition__order=self.order) | Q(order=self.order),
|
||||
id=answid
|
||||
)
|
||||
if not answer.file:
|
||||
raise Http404()
|
||||
if not check_token(request, answer, token):
|
||||
@@ -2597,7 +2609,7 @@ class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code,
|
||||
answer.orderposition.positionid,
|
||||
answer.orderposition.positionid if answer.orderposition else '',
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
)
|
||||
return resp
|
||||
|
||||
@@ -108,6 +108,9 @@ from pretix.base.services.export import (
|
||||
init_organizer_exporters, multiexport, scheduled_organizer_export,
|
||||
)
|
||||
from pretix.base.services.mail import mail, prefix_subject
|
||||
from pretix.base.services.placeholders import (
|
||||
prepare_sample_context_for_preview,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
||||
@@ -345,16 +348,11 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
||||
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(
|
||||
MailSettingsForm.base_context[item]).items():
|
||||
if s.strip().startswith('*'):
|
||||
ctx[p] = s
|
||||
else:
|
||||
ctx[p] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
s
|
||||
)
|
||||
ctx = prepare_sample_context_for_preview(
|
||||
MailSettingsForm(obj=self.request.organizer)._get_sample_context(
|
||||
MailSettingsForm.base_context[item]
|
||||
)
|
||||
)
|
||||
return self.SafeDict(ctx)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -1210,7 +1208,7 @@ class DeviceQueryMixin:
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.devices.prefetch_related(
|
||||
'limit_events', 'gate',
|
||||
).order_by('revoked', '-device_id')
|
||||
).select_related('last_seen').order_by('revoked', '-device_id')
|
||||
|
||||
if 'device' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
|
||||
@@ -27,6 +27,7 @@ from decimal import Decimal
|
||||
from io import BytesIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
@@ -193,6 +194,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
c.bind_to_session(request, "ticketoutput-pdf-background")
|
||||
c.type = 'application/pdf'
|
||||
c.save()
|
||||
c.file.save('empty.pdf', ContentFile(buffer.read()))
|
||||
@@ -218,6 +220,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
c.bind_to_session(request, "ticketoutput-pdf-background")
|
||||
c.type = 'application/pdf'
|
||||
c.file = fileobj
|
||||
c.save()
|
||||
@@ -303,5 +306,7 @@ class FontsCSSView(TemplateView):
|
||||
class PdfView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
|
||||
if not cf.allowed_for_session(request, "ticketoutput-pdf-background"):
|
||||
raise PermissionDenied()
|
||||
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
|
||||
return resp
|
||||
|
||||
@@ -431,7 +431,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
days = (self.copy_from.date_from.astimezone(tz).date() - value.astimezone(tz).date()).days
|
||||
return RelativeDateWrapper(RelativeDate(
|
||||
days=abs(days),
|
||||
base_date_name='date_from',
|
||||
base_date_name='event__date_from',
|
||||
time=value.astimezone(tz).time(),
|
||||
minutes=None,
|
||||
is_after=days < 0,
|
||||
|
||||
@@ -144,7 +144,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
|
||||
headers = [
|
||||
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
|
||||
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
|
||||
_('Comment')
|
||||
_('Comment'), _('Budget'), _('Budget used')
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
@@ -170,7 +170,9 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
|
||||
str(v.redeemed),
|
||||
str(v.max_usages),
|
||||
str(v.seat) if v.seat else "",
|
||||
str(v.comment) if v.comment else ""
|
||||
str(v.comment) if v.comment else "",
|
||||
str(v.budget) if v.budget is not None else "",
|
||||
str(v.budget_used) if v.budget is not None else "",
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import contextlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.indexes import BrinIndex
|
||||
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import (
|
||||
@@ -285,3 +286,30 @@ def get_deterministic_ordering(model, ordering):
|
||||
# on the primary key to provide total ordering.
|
||||
ordering.append("-pk")
|
||||
return ordering
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def conditional_atomic(do_atomic, **kwargs):
|
||||
if do_atomic:
|
||||
with transaction.atomic(**kwargs):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
class IgnoreOnSQLiteMixin:
|
||||
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created
|
||||
# on SQLite. SQLite is supported for testing only anyways!
|
||||
def create_sql(self, model, schema_editor, *args, **kwargs):
|
||||
if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
|
||||
return ""
|
||||
return super().create_sql(model, schema_editor, *args, **kwargs)
|
||||
|
||||
def remove_sql(self, model, schema_editor, **kwargs):
|
||||
if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
|
||||
return ""
|
||||
return super().remove_sql(model, schema_editor, **kwargs)
|
||||
|
||||
|
||||
class BrinIndexIgnoredOnSQLite(IgnoreOnSQLiteMixin, BrinIndex):
|
||||
pass
|
||||
|
||||
@@ -24,17 +24,21 @@ import hashlib
|
||||
from django.core.signing import BadSignature, TimestampSigner
|
||||
|
||||
|
||||
class SafeDownloadSigner(TimestampSigner):
|
||||
pass
|
||||
|
||||
|
||||
def get_token(request, answer):
|
||||
if not request.session.session_key:
|
||||
request.session.create()
|
||||
payload = '{}:{}'.format(request.session.session_key, answer.pk)
|
||||
signer = TimestampSigner()
|
||||
signer = SafeDownloadSigner()
|
||||
return signer.sign(hashlib.sha1(payload.encode()).hexdigest())
|
||||
|
||||
|
||||
def check_token(request, answer, token):
|
||||
payload = hashlib.sha1('{}:{}'.format(request.session.session_key, answer.pk).encode()).hexdigest()
|
||||
signer = TimestampSigner()
|
||||
signer = SafeDownloadSigner()
|
||||
try:
|
||||
return payload == signer.unsign(token, max_age=3600 * 24)
|
||||
except BadSignature:
|
||||
|
||||
@@ -29,7 +29,7 @@ from django.urls import reverse
|
||||
def build_absolute_uri(urlname, args=None, kwargs=None):
|
||||
warnings.warn(
|
||||
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
|
||||
'Replace this usage with ',
|
||||
'Replace this usage with mainreverse_absolute.',
|
||||
DeprecationWarning
|
||||
)
|
||||
return mainreverse_absolute(urlname, args, kwargs)
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-07-15 14:20+0000\n"
|
||||
"Last-Translator: Llewelyn Barkhuizen <llewelyn@akademia.ac.za>\n"
|
||||
"Language-Team: Afrikaans <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
@@ -91,6 +91,10 @@ msgstr ""
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr ""
|
||||
@@ -410,6 +414,10 @@ msgstr ""
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -838,8 +846,7 @@ msgstr ""
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -856,7 +863,6 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr ""
|
||||
@@ -1587,9 +1593,8 @@ msgid "Invoice sender:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1601,8 +1606,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr ""
|
||||
|
||||
@@ -1610,8 +1614,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
@@ -1634,8 +1637,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1663,8 +1665,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2451,7 +2452,8 @@ msgstr ""
|
||||
msgid "Refund states"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -2956,13 +2958,11 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
@@ -2995,6 +2995,16 @@ msgstr ""
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5321,6 +5331,10 @@ msgstr ""
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr ""
|
||||
@@ -5358,6 +5372,10 @@ msgstr ""
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -5957,10 +5975,6 @@ msgstr ""
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr ""
|
||||
@@ -6869,7 +6883,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
@@ -7596,6 +7610,14 @@ msgstr ""
|
||||
msgid "Presale end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order creation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order expiry"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr ""
|
||||
@@ -7620,6 +7642,26 @@ msgstr ""
|
||||
msgid "Not set"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr ""
|
||||
@@ -8705,7 +8747,7 @@ msgstr ""
|
||||
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
|
||||
@@ -9286,6 +9328,25 @@ msgid ""
|
||||
"to get a ticket, such as a box office."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr ""
|
||||
@@ -12427,6 +12488,22 @@ msgid ""
|
||||
"card is issued in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "same as above"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in days"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
msgstr ""
|
||||
@@ -18577,6 +18654,7 @@ msgid "Top recommendation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr ""
|
||||
@@ -20228,6 +20306,24 @@ msgid ""
|
||||
"dietary requirements."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new order-level question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-ticket questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr ""
|
||||
@@ -20244,6 +20340,26 @@ msgstr ""
|
||||
msgid "All personalized products"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-order questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -20766,6 +20882,13 @@ msgstr ""
|
||||
msgid "(optional)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
msgid "Additional order information"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr ""
|
||||
@@ -20793,6 +20916,23 @@ msgstr ""
|
||||
msgid "Extend payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -21048,25 +21188,10 @@ msgstr ""
|
||||
msgid "Ticket page"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -22304,6 +22429,15 @@ msgstr ""
|
||||
msgid "Hardware model"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, python-format
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr ""
|
||||
@@ -25179,10 +25313,6 @@ msgstr ""
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr ""
|
||||
@@ -25328,7 +25458,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
@@ -25718,7 +25848,8 @@ msgstr ""
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -27760,6 +27891,29 @@ msgid ""
|
||||
"getting your money right away."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr ""
|
||||
@@ -27985,8 +28139,11 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
@@ -30719,10 +30876,6 @@ msgstr[1] ""
|
||||
msgid "No attendee name provided"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr ""
|
||||
@@ -31120,6 +31273,10 @@ msgstr ""
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -89,6 +89,10 @@ msgstr ""
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr ""
|
||||
@@ -408,6 +412,10 @@ msgstr ""
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -836,8 +844,7 @@ msgstr ""
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -854,7 +861,6 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr ""
|
||||
@@ -1585,9 +1591,8 @@ msgid "Invoice sender:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1599,8 +1604,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr ""
|
||||
|
||||
@@ -1608,8 +1612,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
@@ -1632,8 +1635,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1661,8 +1663,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2449,7 +2450,8 @@ msgstr ""
|
||||
msgid "Refund states"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -2954,13 +2956,11 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
@@ -2993,6 +2993,16 @@ msgstr ""
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5319,6 +5329,10 @@ msgstr ""
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr ""
|
||||
@@ -5356,6 +5370,10 @@ msgstr ""
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -5955,10 +5973,6 @@ msgstr ""
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr ""
|
||||
@@ -6867,7 +6881,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
@@ -7594,6 +7608,14 @@ msgstr ""
|
||||
msgid "Presale end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order creation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order expiry"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr ""
|
||||
@@ -7618,6 +7640,26 @@ msgstr ""
|
||||
msgid "Not set"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr ""
|
||||
@@ -8703,7 +8745,7 @@ msgstr ""
|
||||
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
|
||||
@@ -9284,6 +9326,25 @@ msgid ""
|
||||
"to get a ticket, such as a box office."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr ""
|
||||
@@ -12425,6 +12486,22 @@ msgid ""
|
||||
"card is issued in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "same as above"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in days"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
msgstr ""
|
||||
@@ -18575,6 +18652,7 @@ msgid "Top recommendation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr ""
|
||||
@@ -20226,6 +20304,24 @@ msgid ""
|
||||
"dietary requirements."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new order-level question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-ticket questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr ""
|
||||
@@ -20242,6 +20338,26 @@ msgstr ""
|
||||
msgid "All personalized products"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-order questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -20764,6 +20880,13 @@ msgstr ""
|
||||
msgid "(optional)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
msgid "Additional order information"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr ""
|
||||
@@ -20791,6 +20914,23 @@ msgstr ""
|
||||
msgid "Extend payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -21046,25 +21186,10 @@ msgstr ""
|
||||
msgid "Ticket page"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -22302,6 +22427,15 @@ msgstr ""
|
||||
msgid "Hardware model"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, python-format
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr ""
|
||||
@@ -25177,10 +25311,6 @@ msgstr ""
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr ""
|
||||
@@ -25326,7 +25456,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
@@ -25716,7 +25846,8 @@ msgstr ""
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -27758,6 +27889,29 @@ msgid ""
|
||||
"getting your money right away."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr ""
|
||||
@@ -27983,8 +28137,11 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
@@ -30717,10 +30874,6 @@ msgstr[1] ""
|
||||
msgid "No attendee name provided"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr ""
|
||||
@@ -31118,6 +31271,10 @@ msgstr ""
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-07-20 08:01+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/ar/"
|
||||
@@ -92,6 +92,10 @@ msgstr "اليونانية"
|
||||
msgid "Hebrew"
|
||||
msgstr "العبرية"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr "الأندونيسية"
|
||||
@@ -421,6 +425,12 @@ msgstr "الوسيط متصل بحدث آخر"
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "لا يمكنك استبدال هذا الطلب (مقاس متوسط بمقاس متوسط)."
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "Product does not support medium exchange."
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr "هذا المنتج لا يدعم استبدال الحجم المتوسط."
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -866,8 +876,7 @@ msgstr "معلومات عن الحدث أو التاريخ"
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -884,7 +893,6 @@ msgstr "المشارك"
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr "البريد الإلكتروني للمشارك"
|
||||
@@ -1635,9 +1643,8 @@ msgid "Invoice sender:"
|
||||
msgstr "مرسل الفاتورة:"
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1649,8 +1656,7 @@ msgstr "العنوان"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr "الرمز البريدي"
|
||||
|
||||
@@ -1658,8 +1664,7 @@ msgstr "الرمز البريدي"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr "المدينة"
|
||||
|
||||
@@ -1682,8 +1687,7 @@ msgstr "الولاية"
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1711,8 +1715,7 @@ msgstr "مستلم الفاتورة:"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2521,7 +2524,8 @@ msgstr "حالات الدفع"
|
||||
msgid "Refund states"
|
||||
msgstr "حالات استعادة المبلغ"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -3039,14 +3043,12 @@ msgstr ""
|
||||
"قم بتحميل صورة صالحة. الملف الذي قمت بتحميله ليس صورة أو هو صورة تالفة."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr "إذا تركت هذا الحقل فارغًا، فستكون التذكرة صالحة اعتبارًا من وقت الشراء."
|
||||
msgid "Street"
|
||||
msgstr "الشارع"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "الشارع والرقم"
|
||||
msgid "State"
|
||||
msgstr "ولاية"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, python-brace-format
|
||||
@@ -3078,6 +3080,16 @@ msgstr "يرجى إدخال تاريخ ووقت لا يسبقان {min}."
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr "يرجى إدخال تاريخ ووقت لا يتجاوزان {max}."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr "إذا تركت هذا الحقل فارغًا، فستكون التذكرة صالحة اعتبارًا من وقت الشراء."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "الشارع والرقم"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5620,6 +5632,10 @@ msgstr "النوع الذي تم اختياره لا ينتمي إلى هذا ا
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr "يجب أن يكون العدد مساويا للصفر أو أكبر منه."
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "حالة الطلب"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr "رقم"
|
||||
@@ -5657,6 +5673,10 @@ msgstr "التاريخ والوقت"
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr "رمز البلد (ISO 3166-1 alpha-2)"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -6299,10 +6319,6 @@ msgstr "بطاقة هدايا"
|
||||
msgid "Value"
|
||||
msgstr "القيمة"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "حالة الطلب"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr "معرف عربة التسوق (مثل مفتاح الجلسة)"
|
||||
@@ -7297,8 +7313,11 @@ msgid "The payment for this invoice has already been received."
|
||||
msgstr "تم استلام دفعة هذه الفاتورة سابقا."
|
||||
|
||||
#: pretix/base/payment.py
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This payment is already being processed and can not be canceled any more."
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr "يجري حالياً معالجة عملية الدفع هذه، ولا يمكن إلغاؤها في الوقت الحالي."
|
||||
|
||||
#: pretix/base/payment.py
|
||||
@@ -8060,6 +8079,18 @@ msgstr "بداية إعلان ما قبل البيع"
|
||||
msgid "Presale end"
|
||||
msgstr "نهاية إعلان ما قبل البيع"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order email"
|
||||
msgid "Order creation"
|
||||
msgstr "البريد الإلكتروني الخاص بالطلب"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order expired"
|
||||
msgid "Order expiry"
|
||||
msgstr "الطلب منتهي الصلاحية"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr "قبل"
|
||||
@@ -8084,6 +8115,26 @@ msgstr "وقت نسبي:"
|
||||
msgid "Not set"
|
||||
msgstr "غير محدد"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr "عشوائي (افتراضي، يعمل مع جميع تطبيقات pretix)"
|
||||
@@ -9446,7 +9497,7 @@ msgstr "تم خصم المبلغ من بطاقتك."
|
||||
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr "يرجى تحويل المبلغ إلى هذا الحساب المصرفي: 9999-9999-9999-9999"
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr "سيتم استبدال هذه القيمة بناءً على معلمات ديناميكية."
|
||||
|
||||
@@ -10142,6 +10193,25 @@ msgstr ""
|
||||
"لهذه الفعالية. يمكنك استخدامه لوصف الخيارات الأخرى للحصول على تذكرة ، على "
|
||||
"سبيل المثال شباك التذاكر."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr "نص للتوجيه"
|
||||
@@ -13966,6 +14036,30 @@ msgstr ""
|
||||
"لقد قمت بتعيين صلاحية بطاقات الهدايا لتكون صالحة لمدة {} سنوات بالإضافة إلى "
|
||||
"السنة التي تم إصدار البطاقة فيها."
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment date"
|
||||
msgid "Payment term"
|
||||
msgstr "يوم الدفع او الاستحقاق"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "(Same as above)"
|
||||
msgid "same as above"
|
||||
msgstr "(كما ورد أعلاه)"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in days"
|
||||
msgid "different payment term in days"
|
||||
msgstr "مدة السداد بالأيام"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in minutes"
|
||||
msgid "different payment term in minutes"
|
||||
msgstr "مدة السداد بالدقائق"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
msgstr "الأسعار شاملة الضريبة"
|
||||
@@ -20716,6 +20810,7 @@ msgid "Top recommendation"
|
||||
msgstr "التوصية الأولى"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr "ميزة تجريبية"
|
||||
@@ -22576,6 +22671,30 @@ msgstr ""
|
||||
"أسئلة تسمح الحضور لملء بيانات إضافية عن تذاكرهم. إذا كنت توفير الغذاء، قد "
|
||||
"يكون مثال واحد أن يسأل المستخدمين حول المتطلبات الغذائية."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new question"
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr "إنشاء سؤال جديد"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new question"
|
||||
msgid "Create a new order-level question"
|
||||
msgstr "إنشاء سؤال جديد"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Delete question"
|
||||
msgid "Per-ticket questions"
|
||||
msgstr "مسألة الحذف"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr "إنشاء سؤال جديد"
|
||||
@@ -22592,6 +22711,30 @@ msgstr "طرح خلال التسجيل في مكتب الاستقبال"
|
||||
msgid "All personalized products"
|
||||
msgstr "جميع المنتجات المخصصة"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Include questions"
|
||||
msgid "Per-order questions"
|
||||
msgstr "تضمين الأسئلة"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "The question has been reordered."
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr "تمت إعادة ترتيب السؤال."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -23178,6 +23321,15 @@ msgstr "معلومات الفاتورة"
|
||||
msgid "(optional)"
|
||||
msgstr "(اختياري)"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
#, fuzzy
|
||||
#| msgid "Additional information"
|
||||
msgid "Additional order information"
|
||||
msgstr "معلومة اضافية"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr "أجل حذف"
|
||||
@@ -23207,6 +23359,25 @@ msgstr "نعم، وتنكر ترتيب"
|
||||
msgid "Extend payment term"
|
||||
msgstr "تمديد فترة السداد"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
"وقد تم تحميل هذا الملف من قبل المستخدم، ويمكن أن تحتوي على فيروسات أو غيرها "
|
||||
"من المحتويات الضارة."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "UNSAFE"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr "بلا جواب"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -23479,27 +23650,10 @@ msgstr "%(datetime_range)s صالح"
|
||||
msgid "Ticket page"
|
||||
msgstr "صفحة التذاكر"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr "بلا جواب"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr "سوف يتم طرح هذا السؤال أثناء التسجيل في مكتب الاستقبال."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
"وقد تم تحميل هذا الملف من قبل المستخدم، ويمكن أن تحتوي على فيروسات أو غيرها "
|
||||
"من المحتويات الضارة."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "UNSAFE"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -24852,6 +25006,16 @@ msgstr "توصيل جهاز"
|
||||
msgid "Hardware model"
|
||||
msgstr "نموذج الأجهزة"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Begin: %(time)s"
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr "البداية: %(time)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr "لم يتم تهيئة بعد"
|
||||
@@ -28045,10 +28209,6 @@ msgstr "تم تحديث ترتيب الفئات."
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr "لم يتم تحديد جميع العناصر."
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr "الشارع"
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr "عدم وجود مسألة المطلوبة."
|
||||
@@ -28265,7 +28425,7 @@ msgstr ""
|
||||
#| "You should update the DNS settings of your domain to include this system "
|
||||
#| "in the SPF record."
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
@@ -28706,7 +28866,8 @@ msgstr "الموضوع: {subject}"
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr "وقد اصطف رسالتك وسوف يتم ارسالهم الى {}."
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -30934,6 +31095,29 @@ msgstr ""
|
||||
"امنح عملاءك إمكانية الشراء الآن (حتى حد معين) والدفع على أقساط متعددة أو في "
|
||||
"غضون 30 يومًا. وأنت، بصفتك التاجر، ستحصل على أموالك على الفور."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr "-- تلقائي --"
|
||||
@@ -31184,11 +31368,12 @@ msgstr "يرجى اختيار طريقة الدفع."
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
"لقد فشلت محاولتنا لتنفيذ عملية الدفع الخاصة بك عبر PayPal. يرجى المحاولة مرة "
|
||||
"أخرى أو الاتصال بنا."
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
msgid ""
|
||||
@@ -34214,10 +34399,6 @@ msgstr[5] "تم استخدام هذه التذكرة %(count)s مرة."
|
||||
msgid "No attendee name provided"
|
||||
msgstr "لم يتم تقديم اسم الحضور"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "الصورة التي قمت بتحميلها سابقًا"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr "تم تخفيض سعر هذا المنتج بسبب خصم تلقائي."
|
||||
@@ -34651,6 +34832,10 @@ msgstr "%(value)sبدون ضرائب"
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr "تحديد السعر بـ %(currency)s لـ %(item)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "الصورة التي قمت بتحميلها سابقًا"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
@@ -36381,6 +36566,13 @@ msgstr "الوصول إلى الكتابة"
|
||||
msgid "Kosovo"
|
||||
msgstr "كوسوفو"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Our attempt to execute your payment via PayPal has failed. Please try "
|
||||
#~ "again or contact us."
|
||||
#~ msgstr ""
|
||||
#~ "لقد فشلت محاولتنا لتنفيذ عملية الدفع الخاصة بك عبر PayPal. يرجى المحاولة "
|
||||
#~ "مرة أخرى أو الاتصال بنا."
|
||||
|
||||
#~ msgid "All actions"
|
||||
#~ msgstr "جميع الإجراءات"
|
||||
|
||||
@@ -36504,9 +36696,6 @@ msgstr "كوسوفو"
|
||||
#~ msgid "Please do not use special characters in names."
|
||||
#~ msgstr "الرجاء إدخال اسم أقصر."
|
||||
|
||||
#~ msgid "State"
|
||||
#~ msgstr "ولاية"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Printing date and time"
|
||||
#~ msgid "Program times: date and time"
|
||||
@@ -37097,9 +37286,6 @@ msgstr "كوسوفو"
|
||||
#~ msgid "Badge design"
|
||||
#~ msgstr "تصميم شارة"
|
||||
|
||||
#~ msgid "(Same as above)"
|
||||
#~ msgstr "(كما ورد أعلاه)"
|
||||
|
||||
#~ msgid "Translation in development"
|
||||
#~ msgstr "الترجمة في التنمية"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2024-12-11 01:00+0000\n"
|
||||
"Last-Translator: Neriman Memmedli <nerim4n@pm.me>\n"
|
||||
"Language-Team: Azerbaijani <https://translate.pretix.eu/projects/pretix/"
|
||||
@@ -91,6 +91,10 @@ msgstr "Yunanca"
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr "İndoneziya dili"
|
||||
@@ -410,6 +414,10 @@ msgstr ""
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -838,8 +846,7 @@ msgstr ""
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -856,7 +863,6 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr ""
|
||||
@@ -1587,9 +1593,8 @@ msgid "Invoice sender:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1601,8 +1606,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr ""
|
||||
|
||||
@@ -1610,8 +1614,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
@@ -1634,8 +1637,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1663,8 +1665,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2451,7 +2452,8 @@ msgstr ""
|
||||
msgid "Refund states"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -2956,13 +2958,11 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
@@ -2995,6 +2995,16 @@ msgstr ""
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5321,6 +5331,10 @@ msgstr ""
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr ""
|
||||
@@ -5358,6 +5372,10 @@ msgstr ""
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -5957,10 +5975,6 @@ msgstr ""
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr ""
|
||||
@@ -6869,7 +6883,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
@@ -7596,6 +7610,14 @@ msgstr ""
|
||||
msgid "Presale end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order creation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order expiry"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr ""
|
||||
@@ -7620,6 +7642,26 @@ msgstr ""
|
||||
msgid "Not set"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr ""
|
||||
@@ -8705,7 +8747,7 @@ msgstr ""
|
||||
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
|
||||
@@ -9286,6 +9328,25 @@ msgid ""
|
||||
"to get a ticket, such as a box office."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr ""
|
||||
@@ -12427,6 +12488,22 @@ msgid ""
|
||||
"card is issued in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "same as above"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in days"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
msgstr ""
|
||||
@@ -18577,6 +18654,7 @@ msgid "Top recommendation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr ""
|
||||
@@ -20228,6 +20306,24 @@ msgid ""
|
||||
"dietary requirements."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new order-level question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-ticket questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr ""
|
||||
@@ -20244,6 +20340,26 @@ msgstr ""
|
||||
msgid "All personalized products"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-order questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -20766,6 +20882,13 @@ msgstr ""
|
||||
msgid "(optional)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
msgid "Additional order information"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr ""
|
||||
@@ -20793,6 +20916,23 @@ msgstr ""
|
||||
msgid "Extend payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -21048,25 +21188,10 @@ msgstr ""
|
||||
msgid "Ticket page"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -22304,6 +22429,15 @@ msgstr ""
|
||||
msgid "Hardware model"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, python-format
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr ""
|
||||
@@ -25179,10 +25313,6 @@ msgstr ""
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr ""
|
||||
@@ -25328,7 +25458,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
@@ -25718,7 +25848,8 @@ msgstr ""
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -27762,6 +27893,29 @@ msgid ""
|
||||
"getting your money right away."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr ""
|
||||
@@ -27987,8 +28141,11 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
@@ -30721,10 +30878,6 @@ msgstr[1] ""
|
||||
msgid "No attendee name provided"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr ""
|
||||
@@ -31122,6 +31275,10 @@ msgstr ""
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-06-25 14:00+0000\n"
|
||||
"Last-Translator: Kim Lozano <joaquim.lozano@upc.edu>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
@@ -91,6 +91,10 @@ msgstr "Grec"
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr ""
|
||||
@@ -460,6 +464,12 @@ msgstr ""
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "No podeu modificar aquesta comanda."
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "You cannot change this order."
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr "No podeu modificar aquesta comanda."
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -959,8 +969,7 @@ msgstr "Enviar registre"
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -979,7 +988,6 @@ msgstr "Nom de l'assistent"
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr "Correu electrònic de l'assistent"
|
||||
@@ -1799,9 +1807,8 @@ msgid "Invoice sender:"
|
||||
msgstr "Emissor de la factura:"
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1813,8 +1820,7 @@ msgstr "Adreça"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr "Codi postal"
|
||||
|
||||
@@ -1822,8 +1828,7 @@ msgstr "Codi postal"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr "Ciutat"
|
||||
|
||||
@@ -1846,8 +1851,7 @@ msgstr "Estat"
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1875,8 +1879,7 @@ msgstr "Receptor de la factura:"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2735,7 +2738,8 @@ msgstr "Estats del pagament"
|
||||
msgid "Refund states"
|
||||
msgstr "Estats del reemborsament"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -3283,14 +3287,14 @@ msgstr ""
|
||||
"està corromput."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
msgid "Street"
|
||||
msgstr "Carrer"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Carrer i número"
|
||||
#, fuzzy
|
||||
#| msgid "Status"
|
||||
msgid "State"
|
||||
msgstr "Estat"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, fuzzy, python-brace-format
|
||||
@@ -3327,6 +3331,16 @@ msgstr "Si us plau, introduïu un canal de venda vàlid."
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr "Si us plau, introduïu un nom més curt."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Carrer i número"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -6046,6 +6060,10 @@ msgstr "La variació triada no pertany a aquest element."
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr "El recompte ha de ser igual o major que zero."
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Posició de la comanda"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr "Número"
|
||||
@@ -6083,6 +6101,10 @@ msgstr "Dia i hora"
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr "Codi de país (ISO 3166-1 alpha-2)"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
#, fuzzy
|
||||
#| msgid "The property name may only contain letters, numbers and underscores."
|
||||
@@ -6748,10 +6770,6 @@ msgstr "Targeta regal"
|
||||
msgid "Value"
|
||||
msgstr "Valor"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Posició de la comanda"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr "Identificador del carretó (ex. clau de sessió)"
|
||||
@@ -7771,7 +7789,7 @@ msgstr "Ja s'ha rebut el pagament per a aquesta factura."
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
@@ -8633,6 +8651,18 @@ msgstr "Inici de la prevenda"
|
||||
msgid "Presale end"
|
||||
msgstr "Finalització de la prevenda"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order details"
|
||||
msgid "Order creation"
|
||||
msgstr "Detalls de la comanda"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order expired"
|
||||
msgid "Order expiry"
|
||||
msgstr "Ha expirat la comanda"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "days before"
|
||||
@@ -8659,6 +8689,26 @@ msgstr "Temps relatiu:"
|
||||
msgid "Not set"
|
||||
msgstr "No s'ha establert"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr "Aleatori (per defecte, funciona amb totes les apps de pretix)"
|
||||
@@ -10003,7 +10053,7 @@ msgstr ""
|
||||
"Si us plau, feu una transferència a aquest compte bancari: "
|
||||
"9999-9999-9999-9999"
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
|
||||
@@ -10743,6 +10793,25 @@ msgstr ""
|
||||
"la finestra de temps de venda. Podeu utilitzar-ho per descriure altres "
|
||||
"opcions per obtenir un tiquet, com per exemple una oficina."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr "Text de guia"
|
||||
@@ -14757,6 +14826,30 @@ msgid ""
|
||||
"card is issued in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment date"
|
||||
msgid "Payment term"
|
||||
msgstr "Data de pagament"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "(Same as above)"
|
||||
msgid "same as above"
|
||||
msgstr "(Igual que aquí al damunt)"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in days"
|
||||
msgid "different payment term in days"
|
||||
msgstr "Termini de pagament en dies"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in minutes"
|
||||
msgid "different payment term in minutes"
|
||||
msgstr "Termini de pagament en minuts"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "including all taxes"
|
||||
@@ -21746,6 +21839,7 @@ msgid "Top recommendation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
#, fuzzy
|
||||
#| msgid "Use feature"
|
||||
@@ -23594,6 +23688,30 @@ msgid ""
|
||||
"dietary requirements."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new question"
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr "Crear una nova pregunta"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new question"
|
||||
msgid "Create a new order-level question"
|
||||
msgstr "Crear una nova pregunta"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Delete question"
|
||||
msgid "Per-ticket questions"
|
||||
msgstr "Esborrar pregunta"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr "Crear una nova pregunta"
|
||||
@@ -23612,6 +23730,30 @@ msgstr ""
|
||||
msgid "All personalized products"
|
||||
msgstr "Tots els productes d'admissió"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Required question"
|
||||
msgid "Per-order questions"
|
||||
msgstr "Pregunta obligatòria"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "The task has been completed."
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr "S'ha completat la tasca."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -24158,6 +24300,15 @@ msgstr "Dades de la factura"
|
||||
msgid "(optional)"
|
||||
msgstr "(opcional)"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
#, fuzzy
|
||||
#| msgid "Meta information"
|
||||
msgid "Additional order information"
|
||||
msgstr "Informació meta"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr "Esborrar comanda"
|
||||
@@ -24185,6 +24336,23 @@ msgstr "Sí, denegar la comanda"
|
||||
msgid "Extend payment term"
|
||||
msgstr "Ampliar el termini de pagament"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -24479,25 +24647,10 @@ msgstr "Entrada: %(datetime)s"
|
||||
msgid "Ticket page"
|
||||
msgstr "Tiquet"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -25887,6 +26040,16 @@ msgstr "Connectar un dispositiu"
|
||||
msgid "Hardware model"
|
||||
msgstr "Model de hardware"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Begin: %(time)s"
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr "Inici: %(time)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr "Encara no està inicialitzat"
|
||||
@@ -29031,10 +29194,6 @@ msgstr ""
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr "Aquesta posició de la comanda s'ha cancel·lat."
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr "Carrer"
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr ""
|
||||
@@ -29182,7 +29341,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
@@ -29616,7 +29775,8 @@ msgstr ""
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -31911,6 +32071,29 @@ msgid ""
|
||||
"getting your money right away."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
#, fuzzy
|
||||
#| msgid "Stripe account"
|
||||
@@ -32177,8 +32360,11 @@ msgstr "Si us plau, trieu com voleu fer el pagament."
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
@@ -35320,10 +35506,6 @@ msgstr[1] "Ja s'ha validat aquest tiquet."
|
||||
msgid "No attendee name provided"
|
||||
msgstr "Mostres els noms dels assistents a les factures"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr ""
|
||||
@@ -35779,6 +35961,10 @@ msgstr "Valor total (sense imposts)"
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
@@ -37705,11 +37891,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "Please do not use special characters in names."
|
||||
#~ msgstr "Si us plau, introduïu un nom més curt."
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Status"
|
||||
#~ msgid "State"
|
||||
#~ msgstr "Estat"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Printing date and time"
|
||||
#~ msgid "Program times: date and time"
|
||||
@@ -38154,9 +38335,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "Badge design"
|
||||
#~ msgstr "Disseny de la tarja d'identificació"
|
||||
|
||||
#~ msgid "(Same as above)"
|
||||
#~ msgstr "(Igual que aquí al damunt)"
|
||||
|
||||
#~ msgid "Translation in development"
|
||||
#~ msgstr "Traducció en progrés"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-01-29 19:42+0000\n"
|
||||
"Last-Translator: Jiří Pastrňák <jiri@pastrnak.email>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/"
|
||||
@@ -91,6 +91,10 @@ msgstr "Řečtina"
|
||||
msgid "Hebrew"
|
||||
msgstr "Hebrejština"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr "Indonéština"
|
||||
@@ -438,6 +442,12 @@ msgstr "Médium je napojené na jinou událost"
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "Tuto objednávku nelze změnit."
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "You cannot change this order."
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr "Tuto objednávku nelze změnit."
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -901,8 +911,7 @@ msgstr "Odeslat informace o obnovení"
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -921,7 +930,6 @@ msgstr "Jméno účastníka"
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr "Email účastníka"
|
||||
@@ -1708,9 +1716,8 @@ msgid "Invoice sender:"
|
||||
msgstr "Odesílatel faktury:"
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1722,8 +1729,7 @@ msgstr "Adresa"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr "PSČ"
|
||||
|
||||
@@ -1731,8 +1737,7 @@ msgstr "PSČ"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr "Město"
|
||||
|
||||
@@ -1755,8 +1760,7 @@ msgstr "Stát"
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1784,8 +1788,7 @@ msgstr "Příjemce faktury:"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2598,7 +2601,8 @@ msgstr "Stavy platby"
|
||||
msgid "Refund states"
|
||||
msgstr "Stavy vrácení peněz"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -3117,16 +3121,12 @@ msgstr ""
|
||||
"byl poškozený."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
"Pokud toto políčko necháte prázdné, bude vstupenka platná od okamžiku "
|
||||
"zakoupení."
|
||||
msgid "Street"
|
||||
msgstr "Ulice"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Ulice a číslo popisné"
|
||||
msgid "State"
|
||||
msgstr "Stav"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, fuzzy, python-brace-format
|
||||
@@ -3163,6 +3163,18 @@ msgstr "Zadejte prosím platný prodejní kanál."
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr "Zadejte prosím správný výsledek."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
"Pokud toto políčko necháte prázdné, bude vstupenka platná od okamžiku "
|
||||
"zakoupení."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Ulice a číslo popisné"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5725,6 +5737,10 @@ msgstr "Tato varianta nepatří k tomuto produktu."
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr "Počet musí být alespoň nula."
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Objednaný produkt"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr "Číslo"
|
||||
@@ -5762,6 +5778,10 @@ msgstr "Datum a čas"
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr "Země (ISO3166-1-alpha-2-kód zemí)"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -6420,10 +6440,6 @@ msgstr "Dárkový poukaz"
|
||||
msgid "Value"
|
||||
msgstr "Hodnota"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Objednaný produkt"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr "ID košíku (např. klíč relace)"
|
||||
@@ -7414,7 +7430,7 @@ msgstr "Platba za tuto fakturu již byla přijata."
|
||||
#, fuzzy
|
||||
#| msgid "This payment can not be canceled at the moment."
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr "Tuto platbu nelze dočasně zrušit."
|
||||
|
||||
#: pretix/base/payment.py
|
||||
@@ -8212,6 +8228,18 @@ msgstr "Zahájení předprodeje"
|
||||
msgid "Presale end"
|
||||
msgstr "Konec předprodeje"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order details"
|
||||
msgid "Order creation"
|
||||
msgstr "Podrobnosti o objednávce"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order expired"
|
||||
msgid "Order expiry"
|
||||
msgstr "Objednávka vypršela"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr "před"
|
||||
@@ -8236,6 +8264,26 @@ msgstr "Relativní čas:"
|
||||
msgid "Not set"
|
||||
msgstr "Nenastaveno"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr "Náhodný (výchozí, funguje se všemi aplikacemi pretix)"
|
||||
@@ -9529,7 +9577,7 @@ msgstr "Částka byla stržena z vaší karty."
|
||||
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr "Peníze prosím převeďte na tento bankovní účet: 9999-9999-9999-9999"
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
|
||||
@@ -10228,6 +10276,25 @@ msgstr ""
|
||||
"prodeje pro tuto akci. Můžete jej použít k popisu dalších možností, jak "
|
||||
"získat vstupenku, například na pokladně."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr "Nápověda"
|
||||
@@ -14111,6 +14178,30 @@ msgstr ""
|
||||
"Nastavili jste platnost dárkových karet na {} let plus rok vydání dárkové "
|
||||
"karty."
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment date"
|
||||
msgid "Payment term"
|
||||
msgstr "Datum splatnosti"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "(Same as above)"
|
||||
msgid "same as above"
|
||||
msgstr "(Stejně jako výše)"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in days"
|
||||
msgid "different payment term in days"
|
||||
msgstr "Doba splatnosti ve dnech"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in minutes"
|
||||
msgid "different payment term in minutes"
|
||||
msgstr "Platební lhůta v minutách"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Price including add-ons"
|
||||
@@ -20975,6 +21066,7 @@ msgid "Top recommendation"
|
||||
msgstr "Doporučené rozšíření"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr "Experimentální funkce"
|
||||
@@ -22720,6 +22812,30 @@ msgid ""
|
||||
"dietary requirements."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new customer"
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr "Vytvořit nového zákazníka"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new event"
|
||||
msgid "Create a new order-level question"
|
||||
msgstr "Vytvořit novou událost"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Delete question"
|
||||
msgid "Per-ticket questions"
|
||||
msgstr "Smazat otázku"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr ""
|
||||
@@ -22736,6 +22852,30 @@ msgstr ""
|
||||
msgid "All personalized products"
|
||||
msgstr "Všechny personalizované produkty"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Include questions"
|
||||
msgid "Per-order questions"
|
||||
msgstr "Zobrazit otázky"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "The question has been reordered."
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr "Otázka byla přeřazena."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -23276,6 +23416,15 @@ msgstr "Fakturační údaje"
|
||||
msgid "(optional)"
|
||||
msgstr "(volitelné)"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
#, fuzzy
|
||||
#| msgid "Additional information"
|
||||
msgid "Additional order information"
|
||||
msgstr "Další informace"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr ""
|
||||
@@ -23303,6 +23452,23 @@ msgstr ""
|
||||
msgid "Extend payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "NEZABEZPEČENÉ"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr "není odpovězeno"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -23579,25 +23745,10 @@ msgstr "Platný %(datetime_range)s"
|
||||
msgid "Ticket page"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr "není odpovězeno"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "NEZABEZPEČENÉ"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -24925,6 +25076,16 @@ msgstr "Připojit zařízení"
|
||||
msgid "Hardware model"
|
||||
msgstr "Model hardwaru"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Begin: %(time)s"
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr "Začátek: %(time)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr "Zatím neinicializováno"
|
||||
@@ -27992,10 +28153,6 @@ msgstr ""
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr "Ulice"
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr ""
|
||||
@@ -28187,7 +28344,7 @@ msgstr ""
|
||||
#| "You should update the DNS settings of your domain to include this system "
|
||||
#| "in the SPF record."
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
@@ -28593,7 +28750,8 @@ msgstr "Předmět: {subject}"
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -30772,6 +30930,29 @@ msgid ""
|
||||
"getting your money right away."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr "-- Automaticky --"
|
||||
@@ -31009,13 +31190,13 @@ msgid "Please select how you want to pay."
|
||||
msgstr "Vyberte, jak chcete platit."
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
"Náš pokus o provedení vaší platby přes PayPal se nezdařil. Zkuste to prosím "
|
||||
"znovu nebo kontaktujte nás."
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
msgid ""
|
||||
@@ -33950,10 +34131,6 @@ msgstr[2] "Vstupenka byla použita %(count)s krát."
|
||||
msgid "No attendee name provided"
|
||||
msgstr "Nebylo uvedeno žádné jméno účastníka"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "Obrázek, který jste předtím nahráli"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr "Cena tohoto produktu byla snížena díky automatické slevě."
|
||||
@@ -34380,6 +34557,10 @@ msgstr "%(value)s Celková cena (bez DPH)"
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr "Nastavení ceny v %(currency)s pro %(item)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "Obrázek, který jste předtím nahráli"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
@@ -36150,6 +36331,14 @@ msgstr "Přístup k zápisu"
|
||||
msgid "Kosovo"
|
||||
msgstr "Kosovo"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid ""
|
||||
#~ "Our attempt to execute your payment via PayPal has failed. Please try "
|
||||
#~ "again or contact us."
|
||||
#~ msgstr ""
|
||||
#~ "Náš pokus o provedení vaší platby přes PayPal se nezdařil. Zkuste to "
|
||||
#~ "prosím znovu nebo kontaktujte nás."
|
||||
|
||||
#~ msgid "All actions"
|
||||
#~ msgstr "Všechny akce"
|
||||
|
||||
@@ -36247,9 +36436,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "Please do not use special characters in names."
|
||||
#~ msgstr "Ve jméně není dovoleno používat speciální znaky."
|
||||
|
||||
#~ msgid "State"
|
||||
#~ msgstr "Stav"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Printing date and time"
|
||||
#~ msgid "Program times: date and time"
|
||||
@@ -36796,9 +36982,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "minutes before"
|
||||
#~ msgstr "Minuty před"
|
||||
|
||||
#~ msgid "(Same as above)"
|
||||
#~ msgstr "(Stejně jako výše)"
|
||||
|
||||
#~ msgid "Translation in development"
|
||||
#~ msgstr "Překlad je v průběhu"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2023-11-14 11:00+0000\n"
|
||||
"Last-Translator: Charliecoleg <DM229135@colegsirgar.ac.uk>\n"
|
||||
"Language-Team: Welsh <https://translate.pretix.eu/projects/pretix/pretix/cy/"
|
||||
@@ -92,6 +92,10 @@ msgstr "Groeg"
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr "Indonesaidd"
|
||||
@@ -415,6 +419,10 @@ msgstr ""
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -853,8 +861,7 @@ msgstr ""
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -871,7 +878,6 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr ""
|
||||
@@ -1609,9 +1615,8 @@ msgid "Invoice sender:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1623,8 +1628,7 @@ msgstr "Cyfeiriad"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr ""
|
||||
|
||||
@@ -1632,8 +1636,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr "Dinas"
|
||||
|
||||
@@ -1656,8 +1659,7 @@ msgstr "Talaith"
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1685,8 +1687,7 @@ msgstr ""
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2473,7 +2474,8 @@ msgstr ""
|
||||
msgid "Refund states"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -2978,14 +2980,15 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
#| msgctxt "address"
|
||||
#| msgid "State"
|
||||
msgid "State"
|
||||
msgstr "Talaith"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, python-brace-format
|
||||
@@ -3017,6 +3020,16 @@ msgstr ""
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5349,6 +5362,10 @@ msgstr ""
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr ""
|
||||
@@ -5386,6 +5403,10 @@ msgstr ""
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -5985,10 +6006,6 @@ msgstr ""
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr ""
|
||||
@@ -6904,7 +6921,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/payment.py
|
||||
@@ -7640,6 +7657,16 @@ msgstr ""
|
||||
msgid "Presale end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Create"
|
||||
msgid "Order creation"
|
||||
msgstr "Creu"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order expiry"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr ""
|
||||
@@ -7664,6 +7691,26 @@ msgstr ""
|
||||
msgid "Not set"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr ""
|
||||
@@ -8761,7 +8808,7 @@ msgstr ""
|
||||
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
|
||||
@@ -9342,6 +9389,25 @@ msgid ""
|
||||
"to get a ticket, such as a box office."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr ""
|
||||
@@ -12498,6 +12564,24 @@ msgid ""
|
||||
"card is issued in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment"
|
||||
msgid "Payment term"
|
||||
msgstr "Taliad"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "same as above"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in days"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
msgstr ""
|
||||
@@ -18686,6 +18770,7 @@ msgid "Top recommendation"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr ""
|
||||
@@ -20370,6 +20455,24 @@ msgid ""
|
||||
"dietary requirements."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new order-level question"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-ticket questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr ""
|
||||
@@ -20386,6 +20489,26 @@ msgstr ""
|
||||
msgid "All personalized products"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-order questions"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -20914,6 +21037,15 @@ msgstr ""
|
||||
msgid "(optional)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
#, fuzzy
|
||||
#| msgid "Additional fee"
|
||||
msgid "Additional order information"
|
||||
msgstr "Ffi ychwanegol"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr ""
|
||||
@@ -20941,6 +21073,23 @@ msgstr ""
|
||||
msgid "Extend payment term"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "ANNIOGEL"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -21196,25 +21345,10 @@ msgstr ""
|
||||
msgid "Ticket page"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "ANNIOGEL"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -22466,6 +22600,15 @@ msgstr ""
|
||||
msgid "Hardware model"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, python-format
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr ""
|
||||
@@ -25367,10 +25510,6 @@ msgstr ""
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr ""
|
||||
@@ -25524,7 +25663,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
@@ -25914,7 +26053,8 @@ msgstr ""
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -27971,6 +28111,29 @@ msgid ""
|
||||
"getting your money right away."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr ""
|
||||
@@ -28196,8 +28359,11 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
@@ -30946,10 +31112,6 @@ msgstr[1] ""
|
||||
msgid "No attendee name provided"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr ""
|
||||
@@ -31356,6 +31518,10 @@ msgstr ""
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||
"PO-Revision-Date: 2026-04-22 18:00+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 18:00+0000\n"
|
||||
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"da/>\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.17\n"
|
||||
"X-Generator: Weblate 2026.7.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -1306,62 +1306,62 @@ msgstr ""
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "January"
|
||||
msgstr "Januar"
|
||||
msgstr "januar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "February"
|
||||
msgstr "Februar"
|
||||
msgstr "februar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "March"
|
||||
msgstr "Marts"
|
||||
msgstr "marts"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "April"
|
||||
msgstr "April"
|
||||
msgstr "april"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "May"
|
||||
msgstr "Maj"
|
||||
msgstr "maj"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "June"
|
||||
msgstr "Juni"
|
||||
msgstr "juni"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "July"
|
||||
msgstr "Juli"
|
||||
msgstr "juli"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "August"
|
||||
msgstr "August"
|
||||
msgstr "august"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "September"
|
||||
msgstr "September"
|
||||
msgstr "september"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "October"
|
||||
msgstr "Oktober"
|
||||
msgstr "oktober"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "November"
|
||||
msgstr "November"
|
||||
msgstr "november"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||
msgid "December"
|
||||
msgstr "December"
|
||||
msgstr "december"
|
||||
|
||||
#~ msgid "iDEAL"
|
||||
#~ msgstr "iDEAL"
|
||||
|
||||
@@ -4,8 +4,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"PO-Revision-Date: 2026-07-30 18:31+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"de/>\n"
|
||||
@@ -14,7 +14,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.7.1\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
@@ -89,6 +89,10 @@ msgstr "Griechisch"
|
||||
msgid "Hebrew"
|
||||
msgstr "Hebräisch"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr "Ungarisch"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr "Indonesisch"
|
||||
@@ -429,6 +433,10 @@ msgstr "Medium ist mit anderer Veranstaltung verbunden"
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "Sie können kein Medium gegen ein Medium eintauschen."
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr "Ein Medientausch kann nicht simuliert werden."
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -883,8 +891,7 @@ msgstr "Veranstaltungs- oder Termininformationen"
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -901,7 +908,6 @@ msgstr "Teilnehmer*in"
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr "E-Mail Teilnehmer*in"
|
||||
@@ -1648,9 +1654,8 @@ msgid "Invoice sender:"
|
||||
msgstr "Rechnungsabsender:"
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1662,8 +1667,7 @@ msgstr "Adresse"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr "Postleitzahl"
|
||||
|
||||
@@ -1671,8 +1675,7 @@ msgstr "Postleitzahl"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr "Stadt"
|
||||
|
||||
@@ -1695,8 +1698,7 @@ msgstr "Bundesstaat"
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1724,8 +1726,7 @@ msgstr "Rechnungsempfänger:"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2541,7 +2542,8 @@ msgstr "Zahlungsstatus"
|
||||
msgid "Refund states"
|
||||
msgstr "Erstattungsstatus"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -3063,15 +3065,12 @@ msgid ""
|
||||
msgstr "Die hochgeladene Datei ist kein Bild oder ist beschädigt."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
"Wenn Sie dieses Feld leer lassen, ist das Ticket ab dem Kaufzeitpunkt gültig."
|
||||
msgid "Street"
|
||||
msgstr "Straße"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Straße und Hausnummer"
|
||||
msgid "State"
|
||||
msgstr "Zustand"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, python-brace-format
|
||||
@@ -3103,6 +3102,17 @@ msgstr "Bitte geben Sie einen Zeitpunkt nicht vor {min} ein."
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr "Bitte geben Sie einen Zeitpunkt nicht nach {max} ein."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
"Wenn Sie dieses Feld leer lassen, ist das Ticket ab dem Kaufzeitpunkt gültig."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Straße und Hausnummer"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5746,6 +5756,10 @@ msgstr "Diese Variante gehört nicht zu diesem Produkt."
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr "Die Anzahl muss mindestens null sein."
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Bestellposition"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr "Zahl"
|
||||
@@ -5783,6 +5797,10 @@ msgstr "Datum und Uhrzeit"
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr "Land (ISO3166-1-alpha-2-Ländercode)"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr "Gefragt bei"
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -6445,10 +6463,6 @@ msgstr "Wertgutschein / Geschenkgutschein"
|
||||
msgid "Value"
|
||||
msgstr "Wert"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Bestellposition"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr "Warenkorb-ID (z.B. Session-ID)"
|
||||
@@ -7488,7 +7502,7 @@ msgstr "Die Rechnung wurde bereits beglichen."
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr ""
|
||||
"Diese Zahlung wird bereits verarbeitet und kann nicht mehr abgebrochen "
|
||||
"werden."
|
||||
@@ -8267,6 +8281,14 @@ msgstr "Start des Vorverkaufs"
|
||||
msgid "Presale end"
|
||||
msgstr "Ende des Vorverkaufs"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order creation"
|
||||
msgstr "Bestelldatum"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order expiry"
|
||||
msgstr "Zahlungsfrist"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr "bevor"
|
||||
@@ -8291,6 +8313,26 @@ msgstr "Relativ:"
|
||||
msgid "Not set"
|
||||
msgstr "Nicht gesetzt"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr "Ein relatives Datum kann nicht als \"vor {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr "Ein relatives Datum kann nicht als \"nach {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr "Ein relativer Zeitpunkt kann nicht als \"vor {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr "Ein relativer Zeitpunkt kann nicht als \"nach {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr "Zufällig (Standard, funktioniert mit allen pretix-Apps)"
|
||||
@@ -9591,7 +9633,7 @@ msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr ""
|
||||
"Bitte überweisen Sie den vollen Betrag auf das Bankkonto 9999-9999-9999-"
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
"Dieser Textbaustein wird aufgrund verschiedener Parameter dynamisch "
|
||||
@@ -10306,6 +10348,30 @@ msgstr ""
|
||||
"Verkaufszeitraum beendet ist. Sie können hier andere Wege ein Ticket zu "
|
||||
"bekommen, wie z.B. eine Abendkasse, beschreiben."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr "Keine Termine passen zu Ihren Kriterien."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr "Text für leere Terminanzeige"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
"Dieser Text wird im Kalender oder der Terminliste angezeigt, wenn sie leer "
|
||||
"ist, weil z.B. der Monat keine Termine enthält oder der vom Nutzer gesetzte "
|
||||
"Filter keine Treffer hat. Sie können hier Wege bewerben, wie man Kontakt für "
|
||||
"zusätzliche Termine aufnehmen kann. Wir empfehlen nicht mehr als ein oder "
|
||||
"zwei Sätze."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr "Hilfetext"
|
||||
@@ -14287,6 +14353,22 @@ msgstr ""
|
||||
"Sie haben eingestellt, dass Wertgutscheine {} Jahre nach Ende des Jahres der "
|
||||
"Ausstellung ablaufen."
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Payment term"
|
||||
msgstr "Zahlungsfrist"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "same as above"
|
||||
msgstr "wie oben"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in days"
|
||||
msgstr "abweichendes Zahlungsziel in Tagen"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in minutes"
|
||||
msgstr "abweichendes Zahlungsziel in Minuten"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
msgstr "Preis inklusive Steuern"
|
||||
@@ -16850,8 +16932,8 @@ msgid ""
|
||||
"%(max_redeemed)s times."
|
||||
msgstr ""
|
||||
"Sie können die maximale Anzahl Einlösungen nicht auf %(max_usages)s "
|
||||
"reduzieren, weil mindestens einer der gewählten Gutscheine bereits %"
|
||||
"(max_redeemed)s mal eingelöst wurde."
|
||||
"reduzieren, weil mindestens einer der gewählten Gutscheine bereits "
|
||||
"%(max_redeemed)s mal eingelöst wurde."
|
||||
|
||||
#: pretix/control/forms/vouchers.py
|
||||
msgid ""
|
||||
@@ -21204,6 +21286,7 @@ msgid "Top recommendation"
|
||||
msgstr "Empfohlene Erweiterung"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr "Experimentelle Funktion"
|
||||
@@ -23115,6 +23198,26 @@ msgstr ""
|
||||
"auszufüllen. Wenn Ihre Veranstaltung Verpflegung beinhaltet, könnten Sie "
|
||||
"z.B. nach Allergien Ihrer Teilnehmer fragen."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr "Neue Frage für jedes Ticket erstellen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new order-level question"
|
||||
msgstr "Neue Frage für jede Bestellung erstellen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-ticket questions"
|
||||
msgstr "Fragen pro Ticket"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
"Diese Fragen werden für jedes Ticket abgefragt, also ggf. mehrfach pro "
|
||||
"Bestellung."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr "Neue Frage erstellen"
|
||||
@@ -23131,6 +23234,30 @@ msgstr "Beim Check-in fragen"
|
||||
msgid "All personalized products"
|
||||
msgstr "Alle personalisierten Produkte"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-order questions"
|
||||
msgstr "Fragen pro Bestellung"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
"Diese Funktion befindet sich in aktiver Entwicklung und wird sich über die "
|
||||
"nächsten Monate stark verändern."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
"Fragen pro Bestellung werden derzeit von pretixPOS weder für Anzeige noch "
|
||||
"Abfrage unterstützt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr "Diese Fragen werden einmal pro Bestellung gestellt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -23735,6 +23862,13 @@ msgstr "Rechnungsinformationen"
|
||||
msgid "(optional)"
|
||||
msgstr "(optional)"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
msgid "Additional order information"
|
||||
msgstr "Zusätzliche Bestellinformationen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr "Bestellung löschen"
|
||||
@@ -23764,6 +23898,25 @@ msgstr "Ja, Bestellung ablehnen"
|
||||
msgid "Extend payment term"
|
||||
msgstr "Zahlungsfrist verändern"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
"Diese Datei wurde von einem Nutzer hochgeladen und kann Viren oder anderen "
|
||||
"schädlichen Inhalt enthalten."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "UNSICHER"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr "nicht beantwortet"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -24048,27 +24201,10 @@ msgstr "Gültig %(datetime_range)s"
|
||||
msgid "Ticket page"
|
||||
msgstr "Ticket-Seite"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr "nicht beantwortet"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr "Diese Frage wird beim Check-In gestellt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
"Diese Datei wurde von einem Nutzer hochgeladen und kann Viren oder anderen "
|
||||
"schädlichen Inhalt enthalten."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "UNSICHER"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -25440,6 +25576,15 @@ msgstr "Neues Gerät verbinden"
|
||||
msgid "Hardware model"
|
||||
msgstr "Hardware-Modell"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, python-format
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr "Zuletzt gesehen: %(time)s Uhr"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr "Kein kürzlicher Kontakt"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr "Noch nicht installiert"
|
||||
@@ -28735,10 +28880,6 @@ msgstr "Die Reihenfolge der Kategorien wurde gespeichert."
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr "Es wurden nicht alle Objekte ausgewählt."
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr "Straße"
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr "Die ausgewählte Frage existiert nicht."
|
||||
@@ -28913,11 +29054,11 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
"Wir haben keinen DMARC-Eintrag für die Domain gefunden Es gibt daher eine "
|
||||
"Wir haben keinen DMARC-Eintrag für die Domain gefunden. Es gibt daher eine "
|
||||
"hohe Chance, dass die E-Mails abgelehnt werden oder im Spam landen. Sie "
|
||||
"sollten die DNS-Einstellungen Ihrer Domain anpassen."
|
||||
|
||||
@@ -29377,7 +29518,8 @@ msgstr "Betreff: {subject}"
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr "Die Nachricht wurde gespeichert und wird an {} versendet."
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -31658,6 +31800,37 @@ msgstr ""
|
||||
"Limit) in mehreren Raten oder binnen 30 Tagen zu bezahlen. Sie, als der "
|
||||
"Händler, erhalten Ihr Geld jedoch sofort."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr "Erlaube weitere Zahlungsversuche während PayPal eine Zahlung überprüft"
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
"PayPals Betrugserkennung blockiert die Verarbeitung von Zahlungen in "
|
||||
"Einzelfällen für signifikante Zeiträume. Die Zahlung wird in diesem Zeitraum "
|
||||
"als \"ausstehend\" angezeigt. Sie können Ihren Kunden erlauben, in diesem "
|
||||
"Zeitfenster weitere Zahlungsversuche zu starten. Dies kann dazu führen, dass "
|
||||
"mehrere Zahlungen stattfinden, wenn der ursprüngliche Versuch freigegeben "
|
||||
"wird."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr "Zeitlimit für weitere Zahlungsversuche"
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
"Zeit in Minuten nach denen ein weitere Zahlungsversuch möglich ist während "
|
||||
"der vorherige noch untersucht wird."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr "-- Automatisch --"
|
||||
@@ -31928,11 +32101,18 @@ msgstr "Bitte wählen Sie die gewünschte Zahlungsmethode aus."
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
"Unser Versuch, die Zahlung mit PayPal auszuführen, ist gescheitert. Bitte "
|
||||
"probieren Sie es erneut oder kontaktieren Sie uns."
|
||||
"Ihre Zahlung wird von PayPal verarbeitet. Dies braucht länger als üblich. "
|
||||
"Sie können warten, bis PayPal die Zahlung bestätigt oder Sie können die "
|
||||
"Zahlung erneut mit dieser oder einer anderen Zahlungsmethode versuchen. Dies "
|
||||
"kann dazu führen, dass Ihnen der Betrag doppelt abgebucht wird, wenn PayPal "
|
||||
"den ursprünglichen Versuch bestätigt. Bitte kontaktieren Sie uns in diesem "
|
||||
"Fall."
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
msgid ""
|
||||
@@ -34989,10 +35169,6 @@ msgstr[1] "Das Ticket wurde %(count)s-mal eingelöst."
|
||||
msgid "No attendee name provided"
|
||||
msgstr "Name der teilnehmenden Person nicht angegeben"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "Das zuvor hochgeladene Bild"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr ""
|
||||
@@ -35434,6 +35610,10 @@ msgstr "%(value)s netto"
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr "Preis in %(currency)s für %(item)s setzen"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "Das zuvor hochgeladene Bild"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
@@ -37237,6 +37417,21 @@ msgstr "Schreibzugriff"
|
||||
msgid "Kosovo"
|
||||
msgstr "Kosovo"
|
||||
|
||||
#, python-brace-format
|
||||
#~ msgid "A relative date cannot be expressed as 'before' for '{}'"
|
||||
#~ msgstr "Ein relatives Datum kann nicht als \"vor {}\" ausgedrückt werden"
|
||||
|
||||
#, python-brace-format
|
||||
#~ msgid "A relative date cannot be expressed as 'after' for '{}'"
|
||||
#~ msgstr "Ein relatives Datum kann nicht als \"nach {}\" ausgedrückt werden"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Our attempt to execute your payment via PayPal has failed. Please try "
|
||||
#~ "again or contact us."
|
||||
#~ msgstr ""
|
||||
#~ "Unser Versuch, die Zahlung mit PayPal auszuführen, ist gescheitert. Bitte "
|
||||
#~ "probieren Sie es erneut oder kontaktieren Sie uns."
|
||||
|
||||
#~ msgid "All actions"
|
||||
#~ msgstr "Alle Aktionen"
|
||||
|
||||
@@ -37386,9 +37581,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "Please do not use special characters in names."
|
||||
#~ msgstr "Bitte verwenden Sie keine Sonderzeichen in Namen."
|
||||
|
||||
#~ msgid "State"
|
||||
#~ msgstr "Zustand"
|
||||
|
||||
#~ msgid "Program times: date and time"
|
||||
#~ msgstr "Programmzeiten: Datum und Uhrzeit"
|
||||
|
||||
@@ -38116,9 +38308,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "Badge design"
|
||||
#~ msgstr "Badge-Design"
|
||||
|
||||
#~ msgid "(Same as above)"
|
||||
#~ msgstr "(wie oben)"
|
||||
|
||||
#~ msgid "Translation in development"
|
||||
#~ msgstr "Übersetzung in Arbeit"
|
||||
|
||||
@@ -38817,10 +39006,6 @@ msgstr "Kosovo"
|
||||
#~ msgstr ""
|
||||
#~ "Wir haben Ihnen in den letzten 24 Stunden bereits eine E-Mail geschickt."
|
||||
|
||||
#~ msgctxt "attendee_data"
|
||||
#~ msgid "Asked"
|
||||
#~ msgstr "Abfragen"
|
||||
|
||||
#~ msgid "Underpaid"
|
||||
#~ msgstr "Unterzahlt"
|
||||
|
||||
|
||||
@@ -293,6 +293,7 @@ Pago
|
||||
parsen
|
||||
Pay
|
||||
PayPal
|
||||
PayPals
|
||||
PayPal-Account
|
||||
PayPal-Accounts
|
||||
PayPal-Konto
|
||||
@@ -340,6 +341,7 @@ pretix-Kontos
|
||||
pretix-Logo
|
||||
pretix-Logos
|
||||
pretix-Plugins
|
||||
pretixPOS
|
||||
pretixPRINT
|
||||
pretixPRINT-Version
|
||||
pretixSCAN
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-30 13:52+0000\n"
|
||||
"PO-Revision-Date: 2026-07-30 18:31+0000\n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.7.1\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -91,6 +91,10 @@ msgstr "Griechisch"
|
||||
msgid "Hebrew"
|
||||
msgstr "Hebräisch"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr "Ungarisch"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr "Indonesisch"
|
||||
@@ -431,6 +435,10 @@ msgstr "Medium ist mit anderer Veranstaltung verbunden"
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "Du kannst kein Medium gegen ein Medium eintauschen."
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr "Ein Medientausch kann nicht simuliert werden."
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
@@ -885,8 +893,7 @@ msgstr "Veranstaltungs- oder Termininformationen"
|
||||
#: pretix/control/forms/organizer.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/views/item.py pretix/plugins/badges/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/badges/exporters.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_change_form.html
|
||||
@@ -903,7 +910,6 @@ msgstr "Teilnehmer*in"
|
||||
#: pretix/base/forms/questions.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/base/pdf.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "Attendee email"
|
||||
msgstr "E-Mail Teilnehmer*in"
|
||||
@@ -1650,9 +1656,8 @@ msgid "Invoice sender:"
|
||||
msgstr "Rechnungsabsender:"
|
||||
|
||||
#: pretix/base/exporters/invoices.py pretix/base/exporters/orderlist.py
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/base/modelimport_orders.py pretix/base/models/customers.py
|
||||
#: pretix/base/models/orders.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
@@ -1664,8 +1669,7 @@ msgstr "Adresse"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "ZIP code"
|
||||
msgstr "Postleitzahl"
|
||||
|
||||
@@ -1673,8 +1677,7 @@ msgstr "Postleitzahl"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/base/models/customers.py pretix/base/models/orders.py
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
msgid "City"
|
||||
msgstr "Stadt"
|
||||
|
||||
@@ -1697,8 +1700,7 @@ msgstr "Bundesstaat"
|
||||
#: pretix/base/settings.py pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/email/login_notice.txt
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Country"
|
||||
@@ -1726,8 +1728,7 @@ msgstr "Rechnungsempfänger:"
|
||||
#: pretix/base/forms/questions.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/views/item.py pretix/plugins/checkinlists/exporters.py
|
||||
#: pretix/plugins/reports/exporters.py
|
||||
#: pretix/plugins/checkinlists/exporters.py pretix/plugins/reports/exporters.py
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Company"
|
||||
@@ -2543,7 +2544,8 @@ msgstr "Zahlungsstatus"
|
||||
msgid "Refund states"
|
||||
msgstr "Erstattungsstatus"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/orders.py
|
||||
#: pretix/base/exporters/orderlist.py pretix/base/models/items.py
|
||||
#: pretix/base/models/orders.py
|
||||
#: pretix/control/templates/pretixcontrol/datasync/failed_jobs.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html
|
||||
@@ -3065,15 +3067,12 @@ msgid ""
|
||||
msgstr "Die hochgeladene Datei ist kein Bild oder ist beschädigt."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
"Wenn du dieses Feld leer lässt, ist das Ticket ab dem Kaufzeitpunkt gültig."
|
||||
msgid "Street"
|
||||
msgstr "Straße"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Straße und Hausnummer"
|
||||
msgid "State"
|
||||
msgstr "Zustand"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, python-brace-format
|
||||
@@ -3105,6 +3104,17 @@ msgstr "Bitte gib einen Zeitpunkt nicht vor {min} ein."
|
||||
msgid "Please enter a date and time no later than {max}."
|
||||
msgstr "Bitte gib einen Zeitpunkt nicht nach {max} ein."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"If you keep this empty, the ticket will be valid starting at the time of "
|
||||
"purchase."
|
||||
msgstr ""
|
||||
"Wenn du dieses Feld leer lässt, ist das Ticket ab dem Kaufzeitpunkt gültig."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid "Street and Number"
|
||||
msgstr "Straße und Hausnummer"
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgid ""
|
||||
"Optional, but depending on the country you reside in we might need to charge "
|
||||
@@ -5743,6 +5753,10 @@ msgstr "Diese Variante gehört nicht zu diesem Produkt."
|
||||
msgid "The count needs to be equal to or greater than zero."
|
||||
msgstr "Die Anzahl muss mindestens null sein."
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Bestellposition"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Number"
|
||||
msgstr "Zahl"
|
||||
@@ -5780,6 +5794,10 @@ msgstr "Datum und Uhrzeit"
|
||||
msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr "Land (ISO3166-1-alpha-2-Ländercode)"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr "Gefragt bei"
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
"The identifier may only contain letters, numbers, dots, dashes, and "
|
||||
@@ -6442,10 +6460,6 @@ msgstr "Wertgutschein / Geschenkgutschein"
|
||||
msgid "Value"
|
||||
msgstr "Wert"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Order position"
|
||||
msgstr "Bestellposition"
|
||||
|
||||
#: pretix/base/models/orders.py
|
||||
msgid "Cart ID (e.g. session key)"
|
||||
msgstr "Warenkorb-ID (z.B. Session-ID)"
|
||||
@@ -7481,7 +7495,7 @@ msgstr "Die Rechnung wurde bereits beglichen."
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid ""
|
||||
"This payment is already being processed and can not be canceled any more."
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr ""
|
||||
"Diese Zahlung wird bereits verarbeitet und kann nicht mehr abgebrochen "
|
||||
"werden."
|
||||
@@ -8260,6 +8274,14 @@ msgstr "Start des Vorverkaufs"
|
||||
msgid "Presale end"
|
||||
msgstr "Ende des Vorverkaufs"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order creation"
|
||||
msgstr "Bestelldatum"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "Order expiry"
|
||||
msgstr "Zahlungsfrist"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
msgstr "bevor"
|
||||
@@ -8284,6 +8306,26 @@ msgstr "Relativ:"
|
||||
msgid "Not set"
|
||||
msgstr "Nicht gesetzt"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr "Ein relatives Datum kann nicht als \"vor {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr "Ein relatives Datum kann nicht als \"nach {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr "Ein relativer Zeitpunkt kann nicht als \"vor {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr "Ein relativer Zeitpunkt kann nicht als \"nach {}\" ausgedrückt werden"
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
msgstr "Zufällig (Standard, funktioniert mit allen pretix-Apps)"
|
||||
@@ -9579,7 +9621,7 @@ msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
|
||||
msgstr ""
|
||||
"Bitte überweise den vollen Betrag auf das Bankkonto 9999-9999-9999-9999"
|
||||
|
||||
#: pretix/base/services/placeholders.py pretix/control/views/organizer.py
|
||||
#: pretix/base/services/placeholders.py
|
||||
msgid "This value will be replaced based on dynamic parameters."
|
||||
msgstr ""
|
||||
"Dieser Textbaustein wird aufgrund verschiedener Parameter dynamisch "
|
||||
@@ -10294,6 +10336,30 @@ msgstr ""
|
||||
"Verkaufszeitraum beendet ist. Du kannst hier andere Wege ein Ticket zu "
|
||||
"bekommen, wie z.B. eine Abendkasse, beschreiben."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr "Keine Termine passen zu deinen Kriterien."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr "Text für leere Terminanzeige"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid ""
|
||||
"This text will be shown if the calendar or list of dates is empty, e.g. "
|
||||
"because a month does not contain any dates or a filter chosen by the user "
|
||||
"does not find any results. You can use this to advertise ways to get in "
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
"Dieser Text wird im Kalender oder der Terminliste angezeigt, wenn sie leer "
|
||||
"ist, weil z.B. der Monat keine Termine enthält oder der vom Nutzer gesetzte "
|
||||
"Filter keine Treffer hat. Du kannst hier Wege bewerben, wie man Kontakt für "
|
||||
"zusätzliche Termine aufnehmen kann. Wir empfehlen nicht mehr als ein oder "
|
||||
"zwei Sätze."
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
msgstr "Hilfetext"
|
||||
@@ -14263,6 +14329,22 @@ msgstr ""
|
||||
"Du hast eingestellt, dass Wertgutscheine {} Jahre nach Ende des Jahres der "
|
||||
"Ausstellung ablaufen."
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Payment term"
|
||||
msgstr "Zahlungsfrist"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "same as above"
|
||||
msgstr "wie oben"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in days"
|
||||
msgstr "abweichendes Zahlungsziel in Tagen"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "different payment term in minutes"
|
||||
msgstr "abweichendes Zahlungsziel in Minuten"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
msgstr "Preis inklusive Steuern"
|
||||
@@ -16827,8 +16909,8 @@ msgid ""
|
||||
"%(max_redeemed)s times."
|
||||
msgstr ""
|
||||
"Du kannst die maximale Anzahl Einlösungen nicht auf %(max_usages)s "
|
||||
"reduzieren, weil mindestens einer der gewählten Gutscheine bereits %"
|
||||
"(max_redeemed)s mal eingelöst wurde."
|
||||
"reduzieren, weil mindestens einer der gewählten Gutscheine bereits "
|
||||
"%(max_redeemed)s mal eingelöst wurde."
|
||||
|
||||
#: pretix/control/forms/vouchers.py
|
||||
msgid ""
|
||||
@@ -21176,6 +21258,7 @@ msgid "Top recommendation"
|
||||
msgstr "Empfohlene Erweiterung"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html
|
||||
msgid "Experimental feature"
|
||||
msgstr "Experimentelle Funktion"
|
||||
@@ -23085,6 +23168,26 @@ msgstr ""
|
||||
"Ticket auszufüllen. Wenn deine Veranstaltung Verpflegung beinhaltet, "
|
||||
"könntest du z.B. nach Allergien deiner Teilnehmer*innen fragen."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr "Neue Frage für jedes Ticket erstellen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new order-level question"
|
||||
msgstr "Neue Frage für jede Bestellung erstellen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-ticket questions"
|
||||
msgstr "Fragen pro Ticket"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
"Diese Fragen werden für jedes Ticket abgefragt, also ggf. mehrfach pro "
|
||||
"Bestellung."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
msgstr "Neue Frage erstellen"
|
||||
@@ -23101,6 +23204,30 @@ msgstr "Beim Check-in fragen"
|
||||
msgid "All personalized products"
|
||||
msgstr "Alle personalisierten Produkte"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Per-order questions"
|
||||
msgstr "Fragen pro Bestellung"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr ""
|
||||
"Diese Funktion befindet sich in aktiver Entwicklung und wird sich über die "
|
||||
"nächsten Monate stark verändern."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr ""
|
||||
"Fragen pro Bestellung werden derzeit von pretixPOS weder für Anzeige noch "
|
||||
"Abfrage unterstützt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr "Diese Fragen werden einmal pro Bestellung gestellt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
#, python-format
|
||||
@@ -23705,6 +23832,13 @@ msgstr "Rechnungsinformationen"
|
||||
msgid "(optional)"
|
||||
msgstr "(optional)"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/change_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
msgid "Additional order information"
|
||||
msgstr "Zusätzliche Bestellinformationen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
msgstr "Bestellung löschen"
|
||||
@@ -23734,6 +23868,25 @@ msgstr "Ja, Bestellung ablehnen"
|
||||
msgid "Extend payment term"
|
||||
msgstr "Zahlungsfrist verändern"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
"Diese Datei wurde von einem Nutzer hochgeladen und kann Viren oder anderen "
|
||||
"schädlichen Inhalt enthalten."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "UNSICHER"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/fragment_question_answer.html
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "not answered"
|
||||
msgstr "nicht beantwortet"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
@@ -24017,27 +24170,10 @@ msgstr "Gültig %(datetime_range)s"
|
||||
msgid "Ticket page"
|
||||
msgstr "Ticket-Seite"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "not answered"
|
||||
msgstr "nicht beantwortet"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "This question will be asked during check-in."
|
||||
msgstr "Diese Frage wird beim Check-In gestellt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid ""
|
||||
"This file has been uploaded by a user and could contain viruses or other "
|
||||
"malicious content."
|
||||
msgstr ""
|
||||
"Diese Datei wurde von einem Nutzer hochgeladen und kann Viren oder anderen "
|
||||
"schädlichen Inhalt enthalten."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
msgid "UNSAFE"
|
||||
msgstr "UNSICHER"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/overview.html
|
||||
#: pretix/plugins/reports/accountingreport.py
|
||||
@@ -25406,6 +25542,15 @@ msgstr "Neues Gerät verbinden"
|
||||
msgid "Hardware model"
|
||||
msgstr "Hardware-Modell"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, python-format
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr "Zuletzt gesehen: %(time)s Uhr"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr "Kein kürzlicher Kontakt"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
msgstr "Noch nicht installiert"
|
||||
@@ -28694,10 +28839,6 @@ msgstr "Die Reihenfolge der Kategorien wurde gespeichert."
|
||||
msgid "Not all objects have been selected."
|
||||
msgstr "Es wurden nicht alle Objekte ausgewählt."
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "Street"
|
||||
msgstr "Straße"
|
||||
|
||||
#: pretix/control/views/item.py
|
||||
msgid "The requested question does not exist."
|
||||
msgstr "Die ausgewählte Frage existiert nicht."
|
||||
@@ -28872,13 +29013,13 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid ""
|
||||
"We did not find DMARC record for your domain. This means that there is a "
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
"Wir haben keinen DMARC-Eintrag für die Domain gefunden Es gibt daher eine "
|
||||
"Wir haben keinen DMARC-Eintrag für die Domain gefunden. Es gibt daher eine "
|
||||
"hohe Chance, dass die E-Mails abgelehnt werden oder im Spam landen. Du "
|
||||
"solltest die DNS-Einstellungen deiner Domain anpassen."
|
||||
"solltest die DNS-Einstellungen Ihrer Domain anpassen."
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid "The verification code was incorrect, please try again."
|
||||
@@ -29334,7 +29475,8 @@ msgstr "Betreff: {subject}"
|
||||
msgid "Your message has been queued and will be sent to {}."
|
||||
msgstr "Die Nachricht wurde gespeichert und wird an {} versendet."
|
||||
|
||||
#: pretix/control/views/orders.py pretix/presale/views/order.py
|
||||
#: pretix/control/views/orders.py pretix/presale/views/cart.py
|
||||
#: pretix/presale/views/order.py
|
||||
msgid ""
|
||||
"This link is no longer valid. Please go back, refresh the page, and try "
|
||||
"again."
|
||||
@@ -31614,6 +31756,37 @@ msgstr ""
|
||||
"Limit) in mehreren Raten oder binnen 30 Tagen zu bezahlen. Du, als der "
|
||||
"Händler, erhältst dein Geld jedoch sofort."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr "Erlaube weitere Zahlungsversuche während PayPal eine Zahlung überprüft"
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"PayPals fraud prevention might block processing of individual payments for a "
|
||||
"considerable amount of time. The payment is marked as \"pending\" during "
|
||||
"this time window. You can allow your customers to start another payment "
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
"PayPals Betrugserkennung blockiert die Verarbeitung von Zahlungen in "
|
||||
"Einzelfällen für signifikante Zeiträume. Die Zahlung wird in diesem Zeitraum "
|
||||
"als \"ausstehend\" angezeigt. Du kannst deinen Kunden erlauben, in diesem "
|
||||
"Zeitfenster weitere Zahlungsversuche zu starten. Dies kann dazu führen, dass "
|
||||
"mehrere Zahlungen stattfinden, wenn der ursprüngliche Versuch freigegeben "
|
||||
"wird."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr "Zeitlimit für weitere Zahlungsversuche"
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr ""
|
||||
"Zeit in Minuten nach denen ein weitere Zahlungsversuch möglich ist während "
|
||||
"der vorherige noch untersucht wird."
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
msgstr "-- Automatisch --"
|
||||
@@ -31882,11 +32055,17 @@ msgstr "Bitte wähle die gewünschte Zahlungsmethode aus."
|
||||
|
||||
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html
|
||||
msgid ""
|
||||
"Our attempt to execute your payment via PayPal has failed. Please try again "
|
||||
"or contact us."
|
||||
"Your payment is being processed by PayPal. This takes longer than usual. You "
|
||||
"can wait until PayPal acknowledges the payment or you can try paying again "
|
||||
"with this or another payment method. This might result in you being charged "
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
"Unser Versuch, die Zahlung mit PayPal auszuführen, ist gescheitert. Bitte "
|
||||
"probiere es erneut oder kontaktiere uns."
|
||||
"Deine Zahlung wird von PayPal verarbeitet. Dies braucht länger als üblich. "
|
||||
"Du kannst warten, bis PayPal die Zahlung bestätigt oder du kannst die "
|
||||
"Zahlung erneut mit dieser oder einer anderen Zahlungsmethode versuchen. Dies "
|
||||
"kann dazu führen, dass dir der Betrag doppelt abgebucht wird, wenn PayPal "
|
||||
"den ursprünglichen Versuch bestätigt. Bitte kontaktiere uns in diesem Fall."
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
msgid ""
|
||||
@@ -34935,10 +35114,6 @@ msgstr[1] "Das Ticket wurde %(count)s-mal eingelöst."
|
||||
msgid "No attendee name provided"
|
||||
msgstr "Name der teilnehmenden Person nicht angegeben"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "Das zuvor hochgeladene Bild"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
|
||||
msgid "The price of this product was reduced because of an automatic discount."
|
||||
msgstr ""
|
||||
@@ -35378,6 +35553,10 @@ msgstr "%(value)s netto"
|
||||
msgid "Set price in %(currency)s for %(item)s"
|
||||
msgstr "Preis in %(currency)s für %(item)s setzen"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_question_answer.html
|
||||
msgid "The image you previously uploaded"
|
||||
msgstr "Das zuvor hochgeladene Bild"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_quota_left.html
|
||||
#, python-format
|
||||
msgid "%(num)s currently available"
|
||||
@@ -37180,6 +37359,21 @@ msgstr "Schreibzugriff"
|
||||
msgid "Kosovo"
|
||||
msgstr "Kosovo"
|
||||
|
||||
#, python-brace-format
|
||||
#~ msgid "A relative date cannot be expressed as 'before' for '{}'"
|
||||
#~ msgstr "Ein relatives Datum kann nicht als \"vor {}\" ausgedrückt werden"
|
||||
|
||||
#, python-brace-format
|
||||
#~ msgid "A relative date cannot be expressed as 'after' for '{}'"
|
||||
#~ msgstr "Ein relatives Datum kann nicht als \"nach {}\" ausgedrückt werden"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Our attempt to execute your payment via PayPal has failed. Please try "
|
||||
#~ "again or contact us."
|
||||
#~ msgstr ""
|
||||
#~ "Unser Versuch, die Zahlung mit PayPal auszuführen, ist gescheitert. Bitte "
|
||||
#~ "probiere es erneut oder kontaktiere uns."
|
||||
|
||||
#~ msgid "All actions"
|
||||
#~ msgstr "Alle Aktionen"
|
||||
|
||||
@@ -37329,9 +37523,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "Please do not use special characters in names."
|
||||
#~ msgstr "Bitte verwende keine Sonderzeichen in Namen."
|
||||
|
||||
#~ msgid "State"
|
||||
#~ msgstr "Zustand"
|
||||
|
||||
#~ msgid "Program times: date and time"
|
||||
#~ msgstr "Programmzeiten: Datum und Uhrzeit"
|
||||
|
||||
@@ -38062,9 +38253,6 @@ msgstr "Kosovo"
|
||||
#~ msgid "Badge design"
|
||||
#~ msgstr "Badge-Design"
|
||||
|
||||
#~ msgid "(Same as above)"
|
||||
#~ msgstr "(wie oben)"
|
||||
|
||||
#~ msgid "Translation in development"
|
||||
#~ msgstr "Übersetzung in Arbeit"
|
||||
|
||||
@@ -38706,10 +38894,6 @@ msgstr "Kosovo"
|
||||
#~ msgstr ""
|
||||
#~ "Wir haben dir in den letzten 24 Stunden bereits eine E-Mail geschickt."
|
||||
|
||||
#~ msgctxt "attendee_data"
|
||||
#~ msgid "Asked"
|
||||
#~ msgstr "Abfragen"
|
||||
|
||||
#~ msgid "Underpaid"
|
||||
#~ msgstr "Unterzahlt"
|
||||
|
||||
|
||||
@@ -293,6 +293,7 @@ Pago
|
||||
parsen
|
||||
Pay
|
||||
PayPal
|
||||
PayPals
|
||||
PayPal-Account
|
||||
PayPal-Accounts
|
||||
PayPal-Konto
|
||||
@@ -340,6 +341,7 @@ pretix-Kontos
|
||||
pretix-Logo
|
||||
pretix-Logos
|
||||
pretix-Plugins
|
||||
pretixPOS
|
||||
pretixPRINT
|
||||
pretixPRINT-Version
|
||||
pretixSCAN
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user