forked from CGM_Public/pretix_original
Compare commits
138
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
267bac7a9d | ||
|
|
c7b951346b | ||
|
|
d8adfdd06f | ||
|
|
0279ca7d94 | ||
|
|
d1989c3cd3 | ||
|
|
61cb2e15cf | ||
|
|
f2ee1d00b3 | ||
|
|
e8e9698a31 | ||
|
|
a1bf7be244 | ||
|
|
f4ca9a5681 | ||
|
|
e6d984538f | ||
|
|
9f1ee9157f | ||
|
|
242e5af4b5 | ||
|
|
7d6e98e6da | ||
|
|
27f964f3ae | ||
|
|
84b3060c0f | ||
|
|
25dcb72f92 | ||
|
|
4b078867c6 | ||
|
|
c595a59d4a | ||
|
|
f164daeaee | ||
|
|
c6b6dd8d49 | ||
|
|
8038c87963 | ||
|
|
c45a970d32 | ||
|
|
a34517233d | ||
|
|
8fb2e5383c | ||
|
|
86a00f3338 | ||
|
|
c8c0d3e7f5 | ||
|
|
7dd455ce15 | ||
|
|
391eda25da | ||
|
|
fcff5a522d | ||
|
|
7e93d38a01 | ||
|
|
6469381899 | ||
|
|
761706c60c | ||
|
|
f91315c88e | ||
|
|
bc05afeab9 | ||
|
|
02d495d287 | ||
|
|
894878d9da | ||
|
|
5896ca0197 | ||
|
|
fe6fc8df32 | ||
|
|
9de8f3a775 | ||
|
|
c92bb9cb8b | ||
|
|
76ecec8b98 | ||
|
|
4b8416df8f | ||
|
|
a601c75923 | ||
|
|
f94227f00f | ||
|
|
a0c1e5369c | ||
|
|
633bfcf73a | ||
|
|
0d3b5b82c1 | ||
|
|
ab95f33546 | ||
|
|
5034b366c5 | ||
|
|
03d3c389da | ||
|
|
3e934acfa0 | ||
|
|
d2a364e848 | ||
|
|
2824b40299 | ||
|
|
c6c2c90908 | ||
|
|
d4ae7df2ec | ||
|
|
79dd7fb596 | ||
|
|
5ed87cd019 | ||
|
|
ccdcbe0cc5 | ||
|
|
4f8607a9db | ||
|
|
57ecaa2676 | ||
|
|
96fd2b1a95 | ||
|
|
5cf24fb6a6 | ||
|
|
1d2ea35a39 | ||
|
|
ac98ae7941 | ||
|
|
a0d055e202 | ||
|
|
27ec5ca006 | ||
|
|
9d2edc405d | ||
|
|
fb95fe7cf6 | ||
|
|
5b5360ef8b | ||
|
|
129d10ca35 | ||
|
|
093a705ff9 | ||
|
|
6130ae4630 | ||
|
|
11a8ed6c7a | ||
|
|
f6392592c5 | ||
|
|
ecb9ad28ea | ||
|
|
45a506fd37 | ||
|
|
3b16e6356b | ||
|
|
9583a50c4e | ||
|
|
6e6d6b2746 | ||
|
|
7266d90c6b | ||
|
|
5e4e88c91d | ||
|
|
e74d12e8b8 | ||
|
|
a5c39271dd | ||
|
|
3170744c56 | ||
|
|
9ec161561b | ||
|
|
aff4f4b8f8 | ||
|
|
75addfe9f4 | ||
|
|
4b05ce5835 | ||
|
|
34c247f423 | ||
|
|
3aad6852cb | ||
|
|
5cdb07bce6 | ||
|
|
6cb2d68948 | ||
|
|
4a7a6273c6 | ||
|
|
ebe343458a | ||
|
|
f9a93b765c | ||
|
|
5aba1f9a23 | ||
|
|
a4eed87396 | ||
|
|
08879d0d55 | ||
|
|
c276a19bcc | ||
|
|
1e3c6e0b68 | ||
|
|
4e283eb560 | ||
|
|
52a1983630 | ||
|
|
3d85d9d865 | ||
|
|
4ca9a43890 | ||
|
|
d8bac7db65 | ||
|
|
91de0f93e6 | ||
|
|
901565203b | ||
|
|
14c6c9c0d7 | ||
|
|
6de6cf6c08 | ||
|
|
29306b3a4d | ||
|
|
ca69996611 | ||
|
|
16419b6ae4 | ||
|
|
d6258b9b54 | ||
|
|
6f75608196 | ||
|
|
6ef88e009b | ||
|
|
957100a195 | ||
|
|
112ef0908f | ||
|
|
91aaff7359 | ||
|
|
8ab61e2c38 | ||
|
|
c8ba5cc427 | ||
|
|
5ebad31b7d | ||
|
|
0429377f7d | ||
|
|
76e4b797a1 | ||
|
|
5f0009c996 | ||
|
|
de63a4be01 | ||
|
|
f3432139cb | ||
|
|
0b82ac9115 | ||
|
|
eb685b5141 | ||
|
|
5f7f0bd8f1 | ||
|
|
9fcef2dcaa | ||
|
|
fc3b186b93 | ||
|
|
a406884575 | ||
|
|
57ccd5f289 | ||
|
|
f4ac7e7f65 | ||
|
|
81d7045b31 | ||
|
|
f9502a3212 | ||
|
|
a31f624417 |
@@ -288,6 +288,7 @@ Example::
|
|||||||
[django]
|
[django]
|
||||||
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
|
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
|
||||||
debug=off
|
debug=off
|
||||||
|
passwords_argon2=on
|
||||||
|
|
||||||
``secret``
|
``secret``
|
||||||
The secret to be used by Django for signing and verification purposes. If this
|
The secret to be used by Django for signing and verification purposes. If this
|
||||||
@@ -303,6 +304,10 @@ Example::
|
|||||||
|
|
||||||
.. WARNING:: Never set this to ``True`` in production!
|
.. WARNING:: Never set this to ``True`` in production!
|
||||||
|
|
||||||
|
``passwords_argon``
|
||||||
|
Use the ``argon2`` algorithm for password hashing. Disable on systems with a small number of CPU cores (currently
|
||||||
|
less than 8).
|
||||||
|
|
||||||
``profile``
|
``profile``
|
||||||
Enable code profiling for a random subset of requests. Disabled by default, see
|
Enable code profiling for a random subset of requests. Disabled by default, see
|
||||||
:ref:`perf-monitoring` for details.
|
:ref:`perf-monitoring` for details.
|
||||||
|
|||||||
@@ -231,11 +231,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 default_server;
|
listen 443 ssl default_server;
|
||||||
listen [::]:443 ipv6only=on default_server;
|
listen [::]:443 ipv6only=on ssl default_server;
|
||||||
server_name pretix.mydomain.com;
|
server_name pretix.mydomain.com;
|
||||||
|
|
||||||
ssl on;
|
|
||||||
ssl_certificate /path/to/cert.chain.pem;
|
ssl_certificate /path/to/cert.chain.pem;
|
||||||
ssl_certificate_key /path/to/key.pem;
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
|||||||
@@ -216,11 +216,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 default_server;
|
listen 443 ssl default_server;
|
||||||
listen [::]:443 ipv6only=on default_server;
|
listen [::]:443 ipv6only=on ssl default_server;
|
||||||
server_name pretix.mydomain.com;
|
server_name pretix.mydomain.com;
|
||||||
|
|
||||||
ssl on;
|
|
||||||
ssl_certificate /path/to/cert.chain.pem;
|
ssl_certificate /path/to/cert.chain.pem;
|
||||||
ssl_certificate_key /path/to/key.pem;
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ subevent integer ID of the date
|
|||||||
position_count integer Number of tickets that match this list (read-only).
|
position_count integer Number of tickets that match this list (read-only).
|
||||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
|
||||||
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
|
|
||||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||||
@@ -91,10 +89,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -146,10 +141,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -246,10 +238,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -271,10 +260,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
||||||
@@ -326,10 +312,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
@@ -342,7 +325,7 @@ Endpoints
|
|||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
||||||
|
|
||||||
Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
|
Delete a check-in list. **Note that this also deletes the information on all check-ins performed via this list.**
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
|
|||||||
@@ -352,12 +352,12 @@ Fetching individual invoices
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
|
:param number: The ``number`` field of the invoice to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/download/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/download/
|
||||||
|
|
||||||
Download an invoice in PDF format.
|
Download an invoice in PDF format.
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ Fetching individual invoices
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
|
:param number: The ``number`` field of the invoice to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
@@ -397,7 +397,7 @@ Modifying invoices
|
|||||||
|
|
||||||
Invoices cannot be edited directly, but the following actions can be triggered:
|
Invoices cannot be edited directly, but the following actions can be triggered:
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/reissue/
|
||||||
|
|
||||||
Cancels the invoice and creates a new one.
|
Cancels the invoice and creates a new one.
|
||||||
|
|
||||||
@@ -419,13 +419,13 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to reissue
|
:param number: The ``number`` field of the invoice to reissue
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/regenerate/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/regenerate/
|
||||||
|
|
||||||
Re-generates the invoice from order data.
|
Re-generates the invoice from order data.
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to regenerate
|
:param number: The ``number`` field of the invoice to regenerate
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ url string The full URL to
|
|||||||
payments list of objects List of payment processes (see below)
|
payments list of objects List of payment processes (see below)
|
||||||
refunds list of objects List of refund processes (see below)
|
refunds list of objects List of refund processes (see below)
|
||||||
last_modified datetime Last modification of this object
|
last_modified datetime Last modification of this object
|
||||||
|
cancellation_date datetime Time of order cancellation (or ``null``). **Note**:
|
||||||
|
Will not be set for partial cancellations and is not
|
||||||
|
reliable for orders that have been cancelled,
|
||||||
|
reactivated and cancelled again.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +155,9 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``expires`` attribute can now be passed during order creation.
|
The ``expires`` attribute can now be passed during order creation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.11
|
||||||
|
|
||||||
|
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
@@ -464,14 +471,15 @@ List of all orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
||||||
``last_modified``, and ``status``. Default: ``datetime``
|
``last_modified``, ``status`` and ``cancellation_date``. Default: ``datetime``
|
||||||
:query string code: Only return orders that match the given order code
|
:query string code: Only return orders that match the given order code
|
||||||
:query string status: Only return orders in the given order status (see above)
|
:query string status: Only return orders in the given order status (see above)
|
||||||
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||||
@@ -703,7 +711,8 @@ Fetching individual orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
|
|||||||
@@ -100,3 +100,7 @@ API
|
|||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
:no-index:
|
||||||
:members: validate_event_settings, api_event_settings_fields
|
:members: validate_event_settings, api_event_settings_fields
|
||||||
|
|
||||||
|
.. automodule:: pretix.api.signals
|
||||||
|
:no-index:
|
||||||
|
:members: register_device_security_profile
|
||||||
|
|||||||
+6
-7
@@ -29,7 +29,7 @@ dependencies = [
|
|||||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.12.*",
|
"BeautifulSoup4==4.12.*",
|
||||||
"bleach==5.0.*",
|
"bleach==6.2.*",
|
||||||
"celery==5.4.*",
|
"celery==5.4.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=3.4.2",
|
"cryptography>=3.4.2",
|
||||||
@@ -53,7 +53,7 @@ dependencies = [
|
|||||||
"django-phonenumber-field==7.3.*",
|
"django-phonenumber-field==7.3.*",
|
||||||
"django-redis==5.4.*",
|
"django-redis==5.4.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.5.*",
|
"django-statici18n==2.6.*",
|
||||||
"djangorestframework==3.15.*",
|
"djangorestframework==3.15.*",
|
||||||
"dnspython==2.7.*",
|
"dnspython==2.7.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
@@ -76,7 +76,7 @@ dependencies = [
|
|||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==11.0.*",
|
"Pillow==11.0.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==5.28.*",
|
"protobuf==5.29.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
@@ -91,9 +91,8 @@ dependencies = [
|
|||||||
"redis==5.2.*",
|
"redis==5.2.*",
|
||||||
"reportlab==4.2.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.17.*",
|
"sentry-sdk==2.18.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
@@ -108,7 +107,7 @@ dependencies = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.10.*",
|
"aiohttp==3.11.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.26.*",
|
"fakeredis==2.26.*",
|
||||||
@@ -117,7 +116,7 @@ dev = [
|
|||||||
"isort==5.13.*",
|
"isort==5.13.*",
|
||||||
"pep8-naming==0.14.*",
|
"pep8-naming==0.14.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio>=0.24",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.11.0.dev0"
|
__version__ = "2024.12.0.dev0"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from rest_framework import exceptions
|
|||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
from pretix.api.auth.devicesecurity import (
|
from pretix.api.auth.devicesecurity import (
|
||||||
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
|
FullAccessSecurityProfile, get_all_security_profiles,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Device
|
from pretix.base.models import Device
|
||||||
|
|
||||||
@@ -58,7 +58,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
r = super().authenticate(request)
|
r = super().authenticate(request)
|
||||||
if r and isinstance(r[1], Device):
|
if r and isinstance(r[1], Device):
|
||||||
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
|
profiles = get_all_security_profiles()
|
||||||
|
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
|
||||||
if not profile.is_allowed(request):
|
if not profile.is_allowed(request):
|
||||||
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -20,13 +20,40 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.api.signals import register_device_security_profile
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
_ALL_PROFILES = None
|
||||||
|
|
||||||
|
|
||||||
class FullAccessSecurityProfile:
|
class BaseSecurityProfile:
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
"""
|
||||||
|
Unique identifier for this profile.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Human-readable name (can be a ``gettext_lazy`` object).
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def is_allowed(self, request) -> bool:
|
||||||
|
"""
|
||||||
|
Return whether a given request should be allowed.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class FullAccessSecurityProfile(BaseSecurityProfile):
|
||||||
identifier = 'full'
|
identifier = 'full'
|
||||||
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
||||||
|
|
||||||
@@ -34,7 +61,7 @@ class FullAccessSecurityProfile:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class AllowListSecurityProfile:
|
class AllowListSecurityProfile(BaseSecurityProfile):
|
||||||
allowlist = ()
|
allowlist = ()
|
||||||
|
|
||||||
def is_allowed(self, request):
|
def is_allowed(self, request):
|
||||||
@@ -157,88 +184,28 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PretixPosSecurityProfile(AllowListSecurityProfile):
|
def get_all_security_profiles():
|
||||||
identifier = 'pretixpos'
|
global _ALL_PROFILES
|
||||||
verbose_name = _('pretixPOS')
|
|
||||||
allowlist = (
|
if _ALL_PROFILES:
|
||||||
('GET', 'api-v1:version'),
|
return _ALL_PROFILES
|
||||||
('GET', 'api-v1:device.eventselection'),
|
|
||||||
('GET', 'api-v1:idempotency.query'),
|
types = OrderedDict()
|
||||||
('GET', 'api-v1:device.info'),
|
for recv, ret in register_device_security_profile.send(None):
|
||||||
('POST', 'api-v1:device.update'),
|
if isinstance(ret, (list, tuple)):
|
||||||
('POST', 'api-v1:device.revoke'),
|
for r in ret:
|
||||||
('POST', 'api-v1:device.roll'),
|
types[r.identifier] = r
|
||||||
('GET', 'api-v1:event-list'),
|
else:
|
||||||
('GET', 'api-v1:event-detail'),
|
types[ret.identifier] = ret
|
||||||
('GET', 'api-v1:subevent-list'),
|
_ALL_PROFILES = types
|
||||||
('GET', 'api-v1:subevent-detail'),
|
return types
|
||||||
('GET', 'api-v1:itemcategory-list'),
|
|
||||||
('GET', 'api-v1:item-list'),
|
|
||||||
('GET', 'api-v1:question-list'),
|
|
||||||
('GET', 'api-v1:quota-list'),
|
|
||||||
('GET', 'api-v1:taxrule-list'),
|
|
||||||
('GET', 'api-v1:ticketlayout-list'),
|
|
||||||
('GET', 'api-v1:ticketlayoutitem-list'),
|
|
||||||
('GET', 'api-v1:badgelayout-list'),
|
|
||||||
('GET', 'api-v1:badgeitem-list'),
|
|
||||||
('GET', 'api-v1:voucher-list'),
|
|
||||||
('GET', 'api-v1:voucher-detail'),
|
|
||||||
('GET', 'api-v1:order-list'),
|
|
||||||
('POST', 'api-v1:order-list'),
|
|
||||||
('GET', 'api-v1:order-detail'),
|
|
||||||
('DELETE', 'api-v1:orderposition-detail'),
|
|
||||||
('PATCH', 'api-v1:orderposition-detail'),
|
|
||||||
('GET', 'api-v1:orderposition-list'),
|
|
||||||
('GET', 'api-v1:orderposition-answer'),
|
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
|
||||||
('POST', 'api-v1:orderposition-printlog'),
|
|
||||||
('POST', 'api-v1:order-mark-canceled'),
|
|
||||||
('POST', 'api-v1:orderpayment-list'),
|
|
||||||
('POST', 'api-v1:orderrefund-list'),
|
|
||||||
('POST', 'api-v1:orderrefund-done'),
|
|
||||||
('POST', 'api-v1:cartposition-list'),
|
|
||||||
('POST', 'api-v1:cartposition-bulk-create'),
|
|
||||||
('GET', 'api-v1:checkinlist-list'),
|
|
||||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:order.poslock'),
|
|
||||||
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
|
|
||||||
('DELETE', 'api-v1:cartposition-detail'),
|
|
||||||
('GET', 'api-v1:giftcard-list'),
|
|
||||||
('POST', 'api-v1:giftcard-transact'),
|
|
||||||
('PATCH', 'api-v1:giftcard-detail'),
|
|
||||||
('GET', 'plugins:pretix_posbackend:posclosing-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
|
|
||||||
('GET', 'plugins:pretix_posbackend:poscashier-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
|
|
||||||
('PUT', 'plugins:pretix_posbackend:file.upload'),
|
|
||||||
('GET', 'api-v1:revokedsecrets-list'),
|
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
|
||||||
('GET', 'api-v1:event.settings'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.event'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.event.subevent'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.plan'),
|
|
||||||
('GET', 'plugins:pretix_seating:selection.simple'),
|
|
||||||
('POST', 'api-v1:upload'),
|
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
|
||||||
('POST', 'api-v1:reusablemedium-lookup'),
|
|
||||||
('GET', 'api-v1:reusablemedium-list'),
|
|
||||||
('POST', 'api-v1:reusablemedium-list'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SECURITY_PROFILES = {
|
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
|
||||||
k.identifier: k() for k in (
|
def register_default_webhook_events(sender, **kwargs):
|
||||||
FullAccessSecurityProfile,
|
return (
|
||||||
PretixScanSecurityProfile,
|
FullAccessSecurityProfile(),
|
||||||
PretixScanNoSyncSecurityProfile,
|
PretixScanSecurityProfile(),
|
||||||
PretixScanNoSyncNoSearchSecurityProfile,
|
PretixScanNoSyncSecurityProfile(),
|
||||||
PretixPosSecurityProfile,
|
PretixScanNoSyncNoSearchSecurityProfile(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
return cid
|
return cid
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data.pop('sales_channel')
|
validated_data.pop('sales_channel', None)
|
||||||
addons_data = validated_data.pop('addons', None)
|
addons_data = validated_data.pop('addons', None)
|
||||||
bundled_data = validated_data.pop('bundled', None)
|
bundled_data = validated_data.pop('bundled', None)
|
||||||
|
|
||||||
|
|||||||
@@ -26,31 +26,22 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
from pretix.base.models import Checkin, CheckinList
|
||||||
|
|
||||||
|
|
||||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||||
checkin_count = serializers.IntegerField(read_only=True)
|
checkin_count = serializers.IntegerField(read_only=True)
|
||||||
position_count = serializers.IntegerField(read_only=True)
|
position_count = serializers.IntegerField(read_only=True)
|
||||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
|
||||||
slug_field="identifier",
|
|
||||||
queryset=SalesChannel.objects.none(),
|
|
||||||
required=False,
|
|
||||||
allow_empty=True,
|
|
||||||
many=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
'include_pending', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||||
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
|
||||||
|
|
||||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||||
|
|
||||||
|
|||||||
@@ -753,12 +753,12 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||||
'url', 'customer', 'valid_if_pending', 'api_meta'
|
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
||||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.serializers import AsymmetricField
|
from pretix.api.serializers import AsymmetricField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
@@ -297,6 +298,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
revoked = serializers.BooleanField(read_only=True)
|
revoked = serializers.BooleanField(read_only=True)
|
||||||
initialized = serializers.DateTimeField(read_only=True)
|
initialized = serializers.DateTimeField(read_only=True)
|
||||||
initialization_token = serializers.DateTimeField(read_only=True)
|
initialization_token = serializers.DateTimeField(read_only=True)
|
||||||
|
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
@@ -306,6 +308,10 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
|
||||||
|
|
||||||
|
|
||||||
class TeamInviteSerializer(serializers.ModelSerializer):
|
class TeamInviteSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -32,10 +32,17 @@ from pretix.helpers.periodic import minimum_interval
|
|||||||
register_webhook_events = Signal()
|
register_webhook_events = Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known webhook events. Receivers should return an
|
This signal is sent out to get all known webhook events. Receivers should return an
|
||||||
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
|
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
register_device_security_profile = Signal()
|
||||||
|
"""
|
||||||
|
This signal is sent out to get all known device security_profiles. Receivers should
|
||||||
|
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
||||||
|
or a list of such instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task)
|
@receiver(periodic_task)
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
'subevent__seat_category_mappings', 'subevent__meta_values',
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -143,7 +143,9 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
|
instance.checkins.all().delete()
|
||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.checkinlist.deleted',
|
'pretix.event.checkinlist.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class OrderViewSetMixin:
|
|||||||
queryset = Order.objects.none()
|
queryset = Order.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
ordering = ('datetime',)
|
ordering = ('datetime',)
|
||||||
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
|
ordering_fields = ('datetime', 'code', 'status', 'last_modified', 'cancellation_date')
|
||||||
filterset_class = OrderFilter
|
filterset_class = OrderFilter
|
||||||
lookup_field = 'code'
|
lookup_field = 'code'
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class NativeAuthBackend(BaseAuthBackend):
|
|||||||
to log in.
|
to log in.
|
||||||
"""
|
"""
|
||||||
d = OrderedDict([
|
d = OrderedDict([
|
||||||
('email', forms.EmailField(label=_("E-mail"), max_length=254,
|
('email', forms.EmailField(label=_("Email"), max_length=254,
|
||||||
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
||||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
||||||
max_length=4096)),
|
max_length=4096)),
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
|||||||
|
|
||||||
class BaseHTMLMailRenderer:
|
class BaseHTMLMailRenderer:
|
||||||
"""
|
"""
|
||||||
This is the base class for all HTML e-mail renderers.
|
This is the base class for all HTML email renderers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, event: Event, organizer=None):
|
def __init__(self, event: Event, organizer=None):
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
|||||||
_('Customer ID'),
|
_('Customer ID'),
|
||||||
_('SSO provider'),
|
_('SSO provider'),
|
||||||
_('External identifier'),
|
_('External identifier'),
|
||||||
_('E-mail'),
|
_('Email'),
|
||||||
_('Phone number'),
|
_('Phone number'),
|
||||||
_('Full name'),
|
_('Full name'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
|||||||
_('Invoice number'),
|
_('Invoice number'),
|
||||||
_('Date'),
|
_('Date'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('E-mail address'),
|
_('Email address'),
|
||||||
_('Invoice type'),
|
_('Invoice type'),
|
||||||
_('Cancellation of'),
|
_('Cancellation of'),
|
||||||
_('Language'),
|
_('Language'),
|
||||||
@@ -326,7 +326,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
|||||||
_('Event start date'),
|
_('Event start date'),
|
||||||
_('Date'),
|
_('Date'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('E-mail address'),
|
_('Email address'),
|
||||||
_('Invoice type'),
|
_('Invoice type'),
|
||||||
_('Cancellation of'),
|
_('Cancellation of'),
|
||||||
_('Invoice sender:') + ' ' + _('Name'),
|
_('Invoice sender:') + ' ' + _('Name'),
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(_('Comment'))
|
headers.append(_('Comment'))
|
||||||
headers.append(_('Follow-up date'))
|
headers.append(_('Follow-up date'))
|
||||||
headers.append(_('Positions'))
|
headers.append(_('Positions'))
|
||||||
headers.append(_('E-mail address verified'))
|
headers.append(_('Email address verified'))
|
||||||
headers.append(_('External customer ID'))
|
headers.append(_('External customer ID'))
|
||||||
headers.append(_('Payment providers'))
|
headers.append(_('Payment providers'))
|
||||||
if form_data.get('include_payment_amounts'):
|
if form_data.get('include_payment_amounts'):
|
||||||
@@ -655,7 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers += [
|
headers += [
|
||||||
_('Sales channel'),
|
_('Sales channel'),
|
||||||
_('Order locale'),
|
_('Order locale'),
|
||||||
_('E-mail address verified'),
|
_('Email address verified'),
|
||||||
_('External customer ID'),
|
_('External customer ID'),
|
||||||
_('Check-in lists'),
|
_('Check-in lists'),
|
||||||
_('Payment providers'),
|
_('Payment providers'),
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class PasswordRecoverForm(forms.Form):
|
|||||||
|
|
||||||
class PasswordForgotForm(forms.Form):
|
class PasswordForgotForm(forms.Form):
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label=_('E-mail'),
|
label=_('Email'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ from django.core.validators import (
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.forms import Select, widgets
|
from django.forms import Select, widgets
|
||||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@@ -77,7 +78,7 @@ from pretix.base.i18n import (
|
|||||||
get_babel_locale, get_language_without_region, language,
|
get_babel_locale, get_language_without_region, language,
|
||||||
)
|
)
|
||||||
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
|
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
|
||||||
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
|
from pretix.base.models.tax import ask_for_vat_id
|
||||||
from pretix.base.services.tax import (
|
from pretix.base.services.tax import (
|
||||||
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
||||||
)
|
)
|
||||||
@@ -276,6 +277,10 @@ class NamePartsFormField(forms.MultiValueField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def name_parts_is_empty(name_parts_dict):
|
||||||
|
return not any(k != "_scheme" and v for k, v in name_parts_dict.items())
|
||||||
|
|
||||||
|
|
||||||
class WrappedPhonePrefixSelect(Select):
|
class WrappedPhonePrefixSelect(Select):
|
||||||
initial = None
|
initial = None
|
||||||
|
|
||||||
@@ -602,6 +607,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
questions = pos.item.questions_to_ask
|
questions = pos.item.questions_to_ask
|
||||||
event = kwargs.pop('event')
|
event = kwargs.pop('event')
|
||||||
self.all_optional = kwargs.pop('all_optional', False)
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -676,7 +682,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
|
|
||||||
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
|
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
|
||||||
add_fields['street'] = forms.CharField(
|
add_fields['street'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=self.attendee_addresses_required,
|
||||||
label=_('Address'),
|
label=_('Address'),
|
||||||
widget=forms.Textarea(attrs={
|
widget=forms.Textarea(attrs={
|
||||||
'rows': 2,
|
'rows': 2,
|
||||||
@@ -686,7 +692,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
initial=(cartpos.street if cartpos else orderpos.street),
|
initial=(cartpos.street if cartpos else orderpos.street),
|
||||||
)
|
)
|
||||||
add_fields['zipcode'] = forms.CharField(
|
add_fields['zipcode'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=False,
|
||||||
max_length=30,
|
max_length=30,
|
||||||
label=_('ZIP code'),
|
label=_('ZIP code'),
|
||||||
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||||
@@ -695,7 +701,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
add_fields['city'] = forms.CharField(
|
add_fields['city'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=False,
|
||||||
label=_('City'),
|
label=_('City'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
initial=(cartpos.city if cartpos else orderpos.city),
|
initial=(cartpos.city if cartpos else orderpos.city),
|
||||||
@@ -707,11 +713,12 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
add_fields['country'] = CountryField(
|
add_fields['country'] = CountryField(
|
||||||
countries=CachedCountries
|
countries=CachedCountries
|
||||||
).formfield(
|
).formfield(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=self.attendee_addresses_required,
|
||||||
label=_('Country'),
|
label=_('Country'),
|
||||||
initial=country,
|
initial=country,
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={
|
||||||
'autocomplete': 'country',
|
'autocomplete': 'country',
|
||||||
|
'data-country-information-url': reverse('js_helpers.states'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
@@ -946,9 +953,9 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|
||||||
if self.address_validation:
|
if self.address_validation:
|
||||||
self.cleaned_data = d = validate_address(d, True)
|
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
|
||||||
|
|
||||||
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
if not d.get('state'):
|
if not d.get('state'):
|
||||||
self.add_error('state', _('This field is required.'))
|
self.add_error('state', _('This field is required.'))
|
||||||
|
|
||||||
@@ -1005,7 +1012,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'street': forms.Textarea(attrs={
|
'street': forms.Textarea(attrs={
|
||||||
'rows': 2,
|
'rows': 2,
|
||||||
'placeholder': _('Street and Number'),
|
'placeholder': _('Street and Number'),
|
||||||
'autocomplete': 'street-address'
|
'autocomplete': 'street-address',
|
||||||
}),
|
}),
|
||||||
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
||||||
'country': forms.Select(attrs={
|
'country': forms.Select(attrs={
|
||||||
@@ -1021,7 +1028,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'data-display-dependency': '#id_is_business_1',
|
'data-display-dependency': '#id_is_business_1',
|
||||||
'autocomplete': 'organization',
|
'autocomplete': 'organization',
|
||||||
}),
|
}),
|
||||||
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
|
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||||
'internal_reference': forms.TextInput,
|
'internal_reference': forms.TextInput,
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
@@ -1055,6 +1062,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
])
|
])
|
||||||
|
|
||||||
self.fields['country'].choices = CachedCountries()
|
self.fields['country'].choices = CachedCountries()
|
||||||
|
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
|
||||||
|
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
fprefix = self.prefix + '-' if self.prefix else ''
|
fprefix = self.prefix + '-' if self.prefix else ''
|
||||||
@@ -1083,6 +1091,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
self.fields['state'].widget.is_required = True
|
self.fields['state'].widget.is_required = True
|
||||||
|
|
||||||
|
self.fields['street'].required = False
|
||||||
|
self.fields['zipcode'].required = False
|
||||||
|
self.fields['city'].required = False
|
||||||
|
|
||||||
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
|
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
|
||||||
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
|
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
|
||||||
self.data = self.data.copy()
|
self.data = self.data.copy()
|
||||||
@@ -1135,16 +1147,19 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
validate_address # local import to prevent impact on startup time
|
validate_address # local import to prevent impact on startup time
|
||||||
|
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
|
|
||||||
if not data.get('is_business'):
|
if not data.get('is_business'):
|
||||||
data['company'] = ''
|
data['company'] = ''
|
||||||
data['vat_id'] = ''
|
data['vat_id'] = ''
|
||||||
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
|
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
|
||||||
data['vat_id'] = ''
|
data['vat_id'] = ''
|
||||||
if self.event.settings.invoice_address_required:
|
if self.address_validation and self.event.settings.invoice_address_required and not self.all_optional:
|
||||||
if data.get('is_business') and not data.get('company'):
|
if data.get('is_business') and not data.get('company'):
|
||||||
raise ValidationError(_('You need to provide a company name.'))
|
raise ValidationError({"company": _('You need to provide a company name.')})
|
||||||
if not data.get('is_business') and not data.get('name_parts'):
|
if not data.get('is_business') and name_parts_is_empty(data.get('name_parts', {})):
|
||||||
raise ValidationError(_('You need to provide your name.'))
|
raise ValidationError(_('You need to provide your name.'))
|
||||||
|
if not data.get('street') and not data.get('zipcode') and not data.get('city'):
|
||||||
|
raise ValidationError({"street": _('This field is required.')})
|
||||||
|
|
||||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
@@ -1156,7 +1171,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
if all(
|
if all(
|
||||||
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
||||||
) and len(data.get('name_parts', {})) == 1:
|
) and name_parts_is_empty(data.get('name_parts', {})):
|
||||||
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
||||||
self.cleaned_data['country'] = ''
|
self.cleaned_data['country'] = ''
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ from pretix.control.forms import SingleLanguageWidget
|
|||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
|
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||||
"Please choose a different one."),
|
"Please choose a different one."),
|
||||||
'pw_current': _("Please enter your current password if you want to change your e-mail "
|
'pw_current': _("Please enter your current password if you want to change your email address "
|
||||||
"address or password."),
|
"or password."),
|
||||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||||
'pw_mismatch': _("Please enter the same password twice"),
|
'pw_mismatch': _("Please enter the same password twice"),
|
||||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
def _clean_text(self, text, tags=None):
|
def _clean_text(self, text, tags=None):
|
||||||
return self._normalize(bleach.clean(
|
return self._normalize(bleach.clean(
|
||||||
text,
|
text,
|
||||||
tags=tags or []
|
tags=set(tags) if tags else set()
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
|
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_event(self, canvas):
|
def _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(txt)
|
txt = str(txt)
|
||||||
txt = bleach.clean(txt, tags=[]).strip()
|
txt = bleach.clean(txt, tags=set()).strip()
|
||||||
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ class BaseMediaType:
|
|||||||
def verbose_name(self):
|
def verbose_name(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""
|
||||||
|
This can be:
|
||||||
|
|
||||||
|
- The name of a Font Awesome icon to represent this channel type.
|
||||||
|
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||||
|
"""
|
||||||
|
return "circle"
|
||||||
|
|
||||||
def generate_identifier(self, organizer):
|
def generate_identifier(self, organizer):
|
||||||
if self.medium_created_by_server:
|
if self.medium_created_by_server:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -59,6 +69,7 @@ class BaseMediaType:
|
|||||||
class BarcodePlainMediaType(BaseMediaType):
|
class BarcodePlainMediaType(BaseMediaType):
|
||||||
identifier = 'barcode'
|
identifier = 'barcode'
|
||||||
verbose_name = _('Barcode / QR-Code')
|
verbose_name = _('Barcode / QR-Code')
|
||||||
|
icon = 'qrcode'
|
||||||
medium_created_by_server = True
|
medium_created_by_server = True
|
||||||
supports_giftcard = False
|
supports_giftcard = False
|
||||||
supports_orderposition = True
|
supports_orderposition = True
|
||||||
@@ -75,6 +86,7 @@ class BarcodePlainMediaType(BaseMediaType):
|
|||||||
class NfcUidMediaType(BaseMediaType):
|
class NfcUidMediaType(BaseMediaType):
|
||||||
identifier = 'nfc_uid'
|
identifier = 'nfc_uid'
|
||||||
verbose_name = _('NFC UID-based')
|
verbose_name = _('NFC UID-based')
|
||||||
|
icon = 'pretixbase/img/media/nfc_uid.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
@@ -114,6 +126,7 @@ class NfcUidMediaType(BaseMediaType):
|
|||||||
class NfcMf0aesMediaType(BaseMediaType):
|
class NfcMf0aesMediaType(BaseMediaType):
|
||||||
identifier = 'nfc_mf0aes'
|
identifier = 'nfc_mf0aes'
|
||||||
verbose_name = 'NFC Mifare Ultralight AES'
|
verbose_name = 'NFC Mifare Ultralight AES'
|
||||||
|
icon = 'pretixbase/img/media/nfc_secure.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
|
|||||||
('password', models.CharField(verbose_name='password', max_length=128)),
|
('password', models.CharField(verbose_name='password', max_length=128)),
|
||||||
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
|
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
|
||||||
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
||||||
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
|
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='Email', null=True,
|
||||||
db_index=True)),
|
db_index=True)),
|
||||||
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
|
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
|
||||||
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
|
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from decimal import Decimal
|
|||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import i18nfield.fields
|
import i18nfield.fields
|
||||||
|
from argon2.exceptions import HashingError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -25,7 +26,14 @@ def initial_user(apps, schema_editor):
|
|||||||
user = User(email='admin@localhost')
|
user = User(email='admin@localhost')
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.is_superuser = True
|
user.is_superuser = True
|
||||||
user.password = make_password('admin')
|
try:
|
||||||
|
user.password = make_password('admin')
|
||||||
|
except HashingError:
|
||||||
|
raise Exception(
|
||||||
|
"Could not hash password of initial user with argon2id. If this is a system with less than 8 CPU cores, "
|
||||||
|
"you might need to disable argon2id by setting `passwords_argon2=off` in the `[django]` section of the "
|
||||||
|
"pretix.cfg configuration file."
|
||||||
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +56,7 @@ class Migration(migrations.Migration):
|
|||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='Email')),
|
||||||
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
|
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
|
||||||
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
|
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||||
@@ -232,7 +240,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
||||||
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||||
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
||||||
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
||||||
('datetime', models.DateTimeField(verbose_name='Date')),
|
('datetime', models.DateTimeField(verbose_name='Date')),
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
||||||
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||||
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
||||||
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
||||||
('datetime', models.DateTimeField(verbose_name='Date')),
|
('datetime', models.DateTimeField(verbose_name='Date')),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
||||||
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
|
('email', models.EmailField(max_length=254, verbose_name='Email address')),
|
||||||
('locale', models.CharField(default='en', max_length=190)),
|
('locale', models.CharField(default='en', max_length=190)),
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
||||||
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
||||||
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
|
('email', models.EmailField(max_length=254, verbose_name='Email address')),
|
||||||
('locale', models.CharField(default='en', max_length=190)),
|
('locale', models.CharField(default='en', max_length=190)),
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
||||||
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
||||||
|
|||||||
+1
-1
@@ -163,7 +163,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('action_type', models.CharField(max_length=255)),
|
('action_type', models.CharField(max_length=255)),
|
||||||
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
|
||||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
to='pretixbase.Event')),
|
to='pretixbase.Event')),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('action_type', models.CharField(max_length=255)),
|
('action_type', models.CharField(max_length=255)),
|
||||||
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
|
||||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
|
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
('enabled', models.BooleanField(default=True)),
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-10-29 15:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_autocheckin(apps, schema_editor):
|
||||||
|
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||||
|
AutoCheckinRule = apps.get_model("autocheckin", "AutoCheckinRule")
|
||||||
|
|
||||||
|
for cl in CheckinList.objects.filter(auto_checkin_sales_channels__isnull=False).select_related("event", "event__organizer"):
|
||||||
|
sales_channels = cl.auto_checkin_sales_channels.all()
|
||||||
|
all_sales_channels = cl.event.organizer.sales_channels.all()
|
||||||
|
|
||||||
|
if "pretix.plugins.autocheckin" not in cl.event.plugins:
|
||||||
|
cl.event.plugins = cl.event.plugins + ",pretix.plugins.autocheckin"
|
||||||
|
cl.event.save()
|
||||||
|
|
||||||
|
r = AutoCheckinRule.objects.get_or_create(
|
||||||
|
list=cl,
|
||||||
|
event=cl.event,
|
||||||
|
all_products=True,
|
||||||
|
all_payment_methods=True,
|
||||||
|
defaults=dict(
|
||||||
|
mode="placed",
|
||||||
|
all_sales_channels=len(sales_channels) == len(all_sales_channels),
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
if len(sales_channels) != len(all_sales_channels):
|
||||||
|
r.limit_sales_channels.set(sales_channels)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0272_printlog"),
|
||||||
|
("autocheckin", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_autocheckin,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="checkinlist",
|
||||||
|
name="auto_checkin_sales_channels",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -256,6 +256,9 @@ class SubeventColumnMixin:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
if value in self._subevent_cache:
|
if value in self._subevent_cache:
|
||||||
return self._subevent_cache[value]
|
return self._subevent_cache[value]
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from pretix.base.signals import order_import_columns
|
|||||||
|
|
||||||
class EmailColumn(ImportColumn):
|
class EmailColumn(ImportColumn):
|
||||||
identifier = 'email'
|
identifier = 'email'
|
||||||
verbose_name = gettext_lazy('E-mail address')
|
verbose_name = gettext_lazy('Email address')
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
@@ -322,7 +322,7 @@ class AttendeeNamePart(ImportColumn):
|
|||||||
|
|
||||||
class AttendeeEmail(ImportColumn):
|
class AttendeeEmail(ImportColumn):
|
||||||
identifier = 'attendee_email'
|
identifier = 'attendee_email'
|
||||||
verbose_name = gettext_lazy('Attendee e-mail address')
|
verbose_name = gettext_lazy('Attendee email address')
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
REQUIRED_FIELDS = []
|
REQUIRED_FIELDS = []
|
||||||
|
|
||||||
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
|
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
|
||||||
verbose_name=_('E-mail'), max_length=190)
|
verbose_name=_('Email'), max_length=190)
|
||||||
fullname = models.CharField(max_length=255, blank=True, null=True,
|
fullname = models.CharField(max_length=255, blank=True, null=True,
|
||||||
verbose_name=_('Full name'))
|
verbose_name=_('Full name'))
|
||||||
is_active = models.BooleanField(default=True,
|
is_active = models.BooleanField(default=True,
|
||||||
|
|||||||
@@ -99,14 +99,6 @@ class CheckinList(LoggedModel):
|
|||||||
verbose_name=_('Automatically check out everyone at'),
|
verbose_name=_('Automatically check out everyone at'),
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
auto_checkin_sales_channels = models.ManyToManyField(
|
|
||||||
"SalesChannel",
|
|
||||||
verbose_name=_('Sales channels to automatically check in'),
|
|
||||||
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
|
|
||||||
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
|
|
||||||
'to use the new plugin.'),
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
rules = models.JSONField(default=dict, blank=True)
|
rules = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
@@ -141,7 +133,7 @@ class CheckinList(LoggedModel):
|
|||||||
return self.positions_query(ignore_status=False)
|
return self.positions_query(ignore_status=False)
|
||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def positions_inside_query(self, ignore_status=False, at_time=None):
|
def _filter_positions_inside(self, qs, at_time=None):
|
||||||
if at_time is None:
|
if at_time is None:
|
||||||
c_q = []
|
c_q = []
|
||||||
else:
|
else:
|
||||||
@@ -149,7 +141,7 @@ class CheckinList(LoggedModel):
|
|||||||
|
|
||||||
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
||||||
# Use a simple approach that works on all databases
|
# Use a simple approach that works on all databases
|
||||||
qs = self.positions_query(ignore_status=ignore_status).annotate(
|
qs = qs.annotate(
|
||||||
last_entry=Subquery(
|
last_entry=Subquery(
|
||||||
Checkin.objects.filter(
|
Checkin.objects.filter(
|
||||||
*c_q,
|
*c_q,
|
||||||
@@ -202,7 +194,7 @@ class CheckinList(LoggedModel):
|
|||||||
.values("position_id", "type", "datetime", "cnt_exists_after")
|
.values("position_id", "type", "datetime", "cnt_exists_after")
|
||||||
.query.sql_with_params()
|
.query.sql_with_params()
|
||||||
)
|
)
|
||||||
return self.positions_query(ignore_status=ignore_status).filter(
|
return qs.filter(
|
||||||
pk__in=RawSQL(
|
pk__in=RawSQL(
|
||||||
f"""
|
f"""
|
||||||
SELECT "position_id"
|
SELECT "position_id"
|
||||||
@@ -214,6 +206,10 @@ class CheckinList(LoggedModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
|
def positions_inside_query(self, ignore_status=False, at_time=None):
|
||||||
|
return self._filter_positions_inside(self.positions_query(ignore_status=ignore_status), at_time=at_time)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positions_inside(self):
|
def positions_inside(self):
|
||||||
return self.positions_inside_query(None)
|
return self.positions_inside_query(None)
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class Customer(LoggedModel):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
|
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('Email'), max_length=190)
|
||||||
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
|
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
|
||||||
password = models.CharField(verbose_name=_('Password'), max_length=128)
|
password = models.CharField(verbose_name=_('Password'), max_length=128)
|
||||||
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||||
@@ -392,7 +392,7 @@ class CustomerSSOClient(LoggedModel):
|
|||||||
SCOPE_CHOICES = (
|
SCOPE_CHOICES = (
|
||||||
('openid', _('OpenID Connect access (required)')),
|
('openid', _('OpenID Connect access (required)')),
|
||||||
('profile', _('Profile data (name, addresses)')),
|
('profile', _('Profile data (name, addresses)')),
|
||||||
('email', _('E-mail address')),
|
('email', _('Email address')),
|
||||||
('phone', _('Phone number')),
|
('phone', _('Phone number')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from django.utils.crypto import get_random_string
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
|
|
||||||
|
|
||||||
@@ -161,7 +160,6 @@ class Device(LoggedModel):
|
|||||||
)
|
)
|
||||||
security_profile = models.CharField(
|
security_profile = models.CharField(
|
||||||
max_length=190,
|
max_length=190,
|
||||||
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
|
|
||||||
default='full',
|
default='full',
|
||||||
null=True,
|
null=True,
|
||||||
blank=False
|
blank=False
|
||||||
|
|||||||
@@ -1024,10 +1024,9 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
checkin_list_map = {}
|
checkin_list_map = {}
|
||||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
||||||
'limit_products', 'auto_checkin_sales_channels'
|
'limit_products'
|
||||||
):
|
):
|
||||||
items = list(cl.limit_products.all())
|
items = list(cl.limit_products.all())
|
||||||
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
|
|
||||||
checkin_list_map[cl.pk] = cl
|
checkin_list_map[cl.pk] = cl
|
||||||
cl.pk = None
|
cl.pk = None
|
||||||
cl._prefetched_objects_cache = {}
|
cl._prefetched_objects_cache = {}
|
||||||
@@ -1039,8 +1038,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
cl.log_action('pretix.object.cloned')
|
cl.log_action('pretix.object.cloned')
|
||||||
for i in items:
|
for i in items:
|
||||||
cl.limit_products.add(item_map[i.pk])
|
cl.limit_products.add(item_map[i.pk])
|
||||||
if auto_checkin_sales_channels:
|
|
||||||
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
|
|
||||||
|
|
||||||
if other.seating_plan:
|
if other.seating_plan:
|
||||||
if other.seating_plan.organizer_id == self.organizer_id:
|
if other.seating_plan.organizer_id == self.organizer_id:
|
||||||
|
|||||||
@@ -159,10 +159,24 @@ class Membership(models.Model):
|
|||||||
de = date_format(self.date_end, 'SHORT_DATE_FORMAT')
|
de = date_format(self.date_end, 'SHORT_DATE_FORMAT')
|
||||||
return f'{self.membership_type.name}: {self.attendee_name} ({ds} – {de})'
|
return f'{self.membership_type.name}: {self.attendee_name} ({ds} – {de})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage_used(self):
|
||||||
|
if self.membership_type.max_usages and self.usages:
|
||||||
|
return int(self.usages / self.membership_type.max_usages * 100)
|
||||||
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attendee_name(self):
|
def attendee_name(self):
|
||||||
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
return time_machine_now() > self.date_end
|
||||||
|
|
||||||
|
@property
|
||||||
|
def not_yet_valid(self):
|
||||||
|
return time_machine_now() < self.date_start
|
||||||
|
|
||||||
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
||||||
if valid_from_not_chosen:
|
if valid_from_not_chosen:
|
||||||
return not self.canceled and self.date_end >= time_machine_now()
|
return not self.canceled and self.date_end >= time_machine_now()
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class NotificationSetting(models.Model):
|
|||||||
:type enabled: bool
|
:type enabled: bool
|
||||||
"""
|
"""
|
||||||
CHANNELS = (
|
CHANNELS = (
|
||||||
('mail', _('E-mail')),
|
('mail', _('Email')),
|
||||||
)
|
)
|
||||||
user = models.ForeignKey('User', on_delete=models.CASCADE,
|
user = models.ForeignKey('User', on_delete=models.CASCADE,
|
||||||
related_name='notification_settings')
|
related_name='notification_settings')
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
)
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
verbose_name=_('E-mail')
|
verbose_name=_('Email')
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@@ -317,7 +317,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
)
|
)
|
||||||
email_known_to_work = models.BooleanField(
|
email_known_to_work = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('E-mail address verified')
|
verbose_name=_('Email address verified')
|
||||||
)
|
)
|
||||||
invoice_dirty = models.BooleanField(
|
invoice_dirty = models.BooleanField(
|
||||||
# Invoice needs to be re-issued when the order is paid again
|
# Invoice needs to be re-issued when the order is paid again
|
||||||
@@ -3204,9 +3204,9 @@ class InvoiceAddress(models.Model):
|
|||||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
||||||
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||||
name_parts = models.JSONField(default=dict)
|
name_parts = models.JSONField(default=dict)
|
||||||
street = models.TextField(verbose_name=_('Address'), blank=False)
|
street = models.TextField(verbose_name=_('Address'), blank=True)
|
||||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True)
|
||||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True)
|
||||||
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
||||||
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
|
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
|
||||||
countries=CachedCountries)
|
countries=CachedCountries)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class WaitingListEntry(LoggedModel):
|
|||||||
blank=True, default=dict
|
blank=True, default=dict
|
||||||
)
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
verbose_name=_("E-mail address")
|
verbose_name=_("Email address")
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
|
|||||||
@@ -343,11 +343,13 @@ class CartManager:
|
|||||||
err = error_messages['some_subevent_not_started']
|
err = error_messages['some_subevent_not_started']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
|
|
||||||
if cp.subevent:
|
if cp.subevent:
|
||||||
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
@@ -360,6 +362,7 @@ class CartManager:
|
|||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def _update_subevents_cache(self, se_ids: List[int]):
|
def _update_subevents_cache(self, se_ids: List[int]):
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ from pretix.base.models import (
|
|||||||
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
|
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
|
||||||
OrderPosition, QuestionOption,
|
OrderPosition, QuestionOption,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
from pretix.base.signals import checkin_created, periodic_task
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.jsonlogic import Logic
|
from pretix.helpers.jsonlogic import Logic
|
||||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||||
@@ -1154,23 +1154,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
|
|
||||||
def order_placed(sender, **kwargs):
|
|
||||||
order = kwargs['order']
|
|
||||||
event = sender
|
|
||||||
|
|
||||||
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
|
|
||||||
'limit_products'))
|
|
||||||
if not cls:
|
|
||||||
return
|
|
||||||
for op in order.positions.all():
|
|
||||||
for cl in cls:
|
|
||||||
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
|
||||||
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
|
|
||||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
|
|
||||||
checkin_created.send(event, checkin=ci)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def process_exit_all(sender, **kwargs):
|
def process_exit_all(sender, **kwargs):
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||||
else:
|
else:
|
||||||
# Backwards compatibility
|
# Backwards compatibility
|
||||||
warnings.warn('E-mail renderer called without position argument because position argument is not '
|
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||||
'supported.',
|
'supported.',
|
||||||
DeprecationWarning)
|
DeprecationWarning)
|
||||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||||
|
|||||||
@@ -209,13 +209,24 @@ def get_best_name(position_or_address, parts=False):
|
|||||||
def base_placeholders(sender, **kwargs):
|
def base_placeholders(sender, **kwargs):
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
def _event_sample(event):
|
||||||
|
if event.has_subevents:
|
||||||
|
se = event.subevents.first()
|
||||||
|
if se:
|
||||||
|
return se.name
|
||||||
|
return event.name
|
||||||
|
|
||||||
ph = [
|
ph = [
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
||||||
lambda event_or_subevent: event_or_subevent.name
|
_event_sample,
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_series_name', ['event', 'event_or_subevent'], lambda event, event_or_subevent: event.name,
|
||||||
|
lambda event: event.name
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'type': bool,
|
'type': bool,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Require a business addresses"),
|
label=_("Require a business address"),
|
||||||
help_text=_('This will require users to enter a company name.'),
|
help_text=_('This will require users to enter a company name.'),
|
||||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}),
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -287,9 +287,9 @@ class PhoneNumberShredder(BaseDataShredder):
|
|||||||
|
|
||||||
|
|
||||||
class EmailAddressShredder(BaseDataShredder):
|
class EmailAddressShredder(BaseDataShredder):
|
||||||
verbose_name = _('E-mails')
|
verbose_name = _('Emails')
|
||||||
identifier = 'order_emails'
|
identifier = 'order_emails'
|
||||||
description = _('This will remove all e-mail addresses from orders and attendees, as well as logged email '
|
description = _('This will remove all email addresses from orders and attendees, as well as logged email '
|
||||||
'contents. This will also remove the association to customer accounts.')
|
'contents. This will also remove the association to customer accounts.')
|
||||||
|
|
||||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def icon(key, *args, **kwargs):
|
||||||
|
return format_html(
|
||||||
|
'<span class="fa fa-{} {}" aria-hidden="true"></span>',
|
||||||
|
key,
|
||||||
|
kwargs["class"] if "class" in kwargs else "",
|
||||||
|
)
|
||||||
@@ -54,7 +54,7 @@ from tlds import tld_set
|
|||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
ALLOWED_TAGS_SNIPPET = [
|
ALLOWED_TAGS_SNIPPET = {
|
||||||
'a',
|
'a',
|
||||||
'abbr',
|
'abbr',
|
||||||
'acronym',
|
'acronym',
|
||||||
@@ -68,8 +68,8 @@ ALLOWED_TAGS_SNIPPET = [
|
|||||||
'strike',
|
'strike',
|
||||||
's',
|
's',
|
||||||
# Update doc/user/markdown.rst if you change this!
|
# Update doc/user/markdown.rst if you change this!
|
||||||
]
|
}
|
||||||
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET | {
|
||||||
'blockquote',
|
'blockquote',
|
||||||
'li',
|
'li',
|
||||||
'ol',
|
'ol',
|
||||||
@@ -91,7 +91,7 @@ ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
|||||||
'h6',
|
'h6',
|
||||||
'pre',
|
'pre',
|
||||||
# Update doc/user/markdown.rst if you change this!
|
# Update doc/user/markdown.rst if you change this!
|
||||||
]
|
}
|
||||||
|
|
||||||
ALLOWED_ATTRIBUTES = {
|
ALLOWED_ATTRIBUTES = {
|
||||||
'a': ['href', 'title', 'class'],
|
'a': ['href', 'title', 'class'],
|
||||||
@@ -106,7 +106,7 @@ ALLOWED_ATTRIBUTES = {
|
|||||||
# Update doc/user/markdown.rst if you change this!
|
# Update doc/user/markdown.rst if you change this!
|
||||||
}
|
}
|
||||||
|
|
||||||
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
|
ALLOWED_PROTOCOLS = {'http', 'https', 'mailto', 'tel'}
|
||||||
|
|
||||||
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
||||||
|
|
||||||
@@ -211,9 +211,9 @@ class CleanPostprocessor(Postprocessor):
|
|||||||
def run(self, text):
|
def run(self, text):
|
||||||
return bleach.clean(
|
return bleach.clean(
|
||||||
text,
|
text,
|
||||||
tags=self.tags,
|
tags=set(self.tags),
|
||||||
attributes=self.attributes,
|
attributes=self.attributes,
|
||||||
protocols=self.protocols,
|
protocols=set(self.protocols),
|
||||||
strip=self.strip
|
strip=self.strip
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
|
|||||||
EmailNl2BrExtension(),
|
EmailNl2BrExtension(),
|
||||||
LinkifyAndCleanExtension(
|
LinkifyAndCleanExtension(
|
||||||
linker,
|
linker,
|
||||||
tags=allowed_tags,
|
tags=set(allowed_tags),
|
||||||
attributes=allowed_attributes,
|
attributes=allowed_attributes,
|
||||||
protocols=ALLOWED_PROTOCOLS,
|
protocols=ALLOWED_PROTOCOLS,
|
||||||
strip=False,
|
strip=False,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html, mark_safe
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def textbubble(type, *args, **kwargs):
|
||||||
|
return format_html(
|
||||||
|
'<span class="textbubble-{}">{}',
|
||||||
|
type or "info",
|
||||||
|
"" if "icon" not in kwargs else format_html(
|
||||||
|
'<i class="fa fa-{}" aria-hidden="true"></i> ',
|
||||||
|
kwargs["icon"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def endtextbubble():
|
||||||
|
return mark_safe('</span>')
|
||||||
@@ -22,16 +22,30 @@
|
|||||||
import pycountry
|
import pycountry
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from pretix.base.addressvalidation import (
|
||||||
|
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
|
||||||
|
)
|
||||||
|
from pretix.base.models.tax import VAT_ID_COUNTRIES
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
|
|
||||||
|
|
||||||
def states(request):
|
def states(request):
|
||||||
cc = request.GET.get("country", "DE")
|
cc = request.GET.get("country", "DE")
|
||||||
|
info = {
|
||||||
|
'street': {'required': True},
|
||||||
|
'zipcode': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
|
||||||
|
'city': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
|
||||||
|
'state': {'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS, 'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS},
|
||||||
|
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
|
||||||
|
}
|
||||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
return JsonResponse({'data': []})
|
return JsonResponse({'data': [], **info, })
|
||||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||||
return JsonResponse({'data': [
|
return JsonResponse({
|
||||||
{'name': s.name, 'code': s.code[3:]}
|
'data': [
|
||||||
for s in sorted(statelist, key=lambda s: s.name)
|
{'name': s.name, 'code': s.code[3:]}
|
||||||
]})
|
for s in sorted(statelist, key=lambda s: s.name)
|
||||||
|
],
|
||||||
|
**info,
|
||||||
|
})
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ from django_scopes.forms import (
|
|||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
from pretix.base.models import Gate
|
from pretix.base.models import Gate
|
||||||
from pretix.base.models.checkin import Checkin, CheckinList
|
from pretix.base.models.checkin import Checkin, CheckinList
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import ItemMultipleChoiceField
|
||||||
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
|
||||||
)
|
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
|
|
||||||
|
|
||||||
@@ -67,10 +65,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
kwargs.pop('locales', None)
|
kwargs.pop('locales', None)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.fields['limit_products'].queryset = self.event.items.all()
|
self.fields['limit_products'].queryset = self.event.items.all()
|
||||||
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
|
||||||
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
|
|
||||||
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.event.organizer.gates.exists():
|
if not self.event.organizer.gates.exists():
|
||||||
del self.fields['gates']
|
del self.fields['gates']
|
||||||
@@ -102,7 +96,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'limit_products',
|
'limit_products',
|
||||||
'subevent',
|
'subevent',
|
||||||
'include_pending',
|
'include_pending',
|
||||||
'auto_checkin_sales_channels',
|
|
||||||
'allow_multiple_entries',
|
'allow_multiple_entries',
|
||||||
'allow_entry_after_exit',
|
'allow_entry_after_exit',
|
||||||
'rules',
|
'rules',
|
||||||
@@ -125,7 +118,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'limit_products': ItemMultipleChoiceField,
|
'limit_products': ItemMultipleChoiceField,
|
||||||
'gates': SafeModelMultipleChoiceField,
|
'gates': SafeModelMultipleChoiceField,
|
||||||
'subevent': SafeModelChoiceField,
|
'subevent': SafeModelChoiceField,
|
||||||
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
|
|
||||||
'exit_all_at': NextTimeField,
|
'exit_all_at': NextTimeField,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
label=_("Default language"),
|
label=_("Default language"),
|
||||||
)
|
)
|
||||||
|
no_taxes = forms.BooleanField(
|
||||||
|
label=_("I don't want to specify taxes now"),
|
||||||
|
help_text=_("You can always configure tax rates later."),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
tax_rate = forms.DecimalField(
|
tax_rate = forms.DecimalField(
|
||||||
label=_("Sales tax rate"),
|
label=_("Sales tax rate"),
|
||||||
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
|
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
|
||||||
@@ -223,6 +228,11 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'timezone': _('Your default locale must be specified.')
|
'timezone': _('Your default locale must be specified.')
|
||||||
})
|
})
|
||||||
|
if not data.get("no_taxes") and not data.get("tax_rate"):
|
||||||
|
raise ValidationError({
|
||||||
|
'tax_rate': _('You have not specified a tax rate. If you do not want us to compute sales taxes, please '
|
||||||
|
'check "{field}" above.').format(field=self.fields["no_taxes"].label)
|
||||||
|
})
|
||||||
|
|
||||||
# change timezone
|
# change timezone
|
||||||
zone = ZoneInfo(data.get('timezone'))
|
zone = ZoneInfo(data.get('timezone'))
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
)
|
)
|
||||||
email = forms.CharField(
|
email = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('E-mail address')
|
label=_('Email address')
|
||||||
)
|
)
|
||||||
comment = forms.CharField(
|
comment = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -563,7 +563,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
email_known_to_work = forms.NullBooleanField(
|
email_known_to_work = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=FilterNullBooleanSelect,
|
widget=FilterNullBooleanSelect,
|
||||||
label=_('E-mail address verified'),
|
label=_('Email address verified'),
|
||||||
)
|
)
|
||||||
total = forms.DecimalField(
|
total = forms.DecimalField(
|
||||||
localize=True,
|
localize=True,
|
||||||
@@ -648,7 +648,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
)
|
)
|
||||||
self.fields['attendee_email'] = forms.CharField(
|
self.fields['attendee_email'] = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Attendee e-mail address')
|
label=_('Attendee email address')
|
||||||
)
|
)
|
||||||
self.fields['attendee_address_company'] = forms.CharField(
|
self.fields['attendee_address_company'] = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -1967,7 +1967,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
|
|||||||
if s == '1':
|
if s == '1':
|
||||||
qs = qs.filter(last_entry__isnull=False)
|
qs = qs.filter(last_entry__isnull=False)
|
||||||
elif s == '2':
|
elif s == '2':
|
||||||
qs = qs.filter(pk__in=self.list.positions_inside.values_list('pk'))
|
qs = self.list._filter_positions_inside(qs)
|
||||||
elif s == '3':
|
elif s == '3':
|
||||||
qs = qs.filter(last_entry__isnull=False).filter(
|
qs = qs.filter(last_entry__isnull=False).filter(
|
||||||
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
|
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class UpdateSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
update_check_email = forms.EmailField(
|
update_check_email = forms.EmailField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_("E-mail notifications"),
|
label=_("Email notifications"),
|
||||||
help_text=_("We will notify you at this address if we detect that a new update is available. This "
|
help_text=_("We will notify you at this address if we detect that a new update is available. This "
|
||||||
"address will not be transmitted to pretix.eu, the emails will be sent by this server "
|
"address will not be transmitted to pretix.eu, the emails will be sent by this server "
|
||||||
"locally.")
|
"locally.")
|
||||||
|
|||||||
@@ -609,6 +609,49 @@ class OrderFeeChangeForm(forms.Form):
|
|||||||
change_decimal_field(self.fields['value'], instance.order.event.currency)
|
change_decimal_field(self.fields['value'], instance.order.event.currency)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFeeAddForm(forms.Form):
|
||||||
|
fee_type = forms.ChoiceField(choices=OrderFee.FEE_TYPES)
|
||||||
|
value = forms.DecimalField(
|
||||||
|
max_digits=13, decimal_places=2,
|
||||||
|
localize=True,
|
||||||
|
label=_('Price'),
|
||||||
|
help_text=_("including all taxes"),
|
||||||
|
)
|
||||||
|
tax_rule = forms.ModelChoiceField(
|
||||||
|
TaxRule.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
description = forms.CharField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
order = kwargs.pop('order')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['tax_rule'].queryset = order.event.tax_rules.all()
|
||||||
|
change_decimal_field(self.fields['value'], order.event.currency)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFeeAddFormset(forms.BaseFormSet):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.order = kwargs.pop('order', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _construct_form(self, i, **kwargs):
|
||||||
|
kwargs['order'] = self.order
|
||||||
|
return super()._construct_form(i, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def empty_form(self):
|
||||||
|
form = self.form(
|
||||||
|
auto_id=self.auto_id,
|
||||||
|
prefix=self.add_prefix('__prefix__'),
|
||||||
|
empty_permitted=True,
|
||||||
|
use_required_attribute=False,
|
||||||
|
order=self.order,
|
||||||
|
)
|
||||||
|
self.add_fields(form, None)
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class OrderContactForm(forms.ModelForm):
|
class OrderContactForm(forms.ModelForm):
|
||||||
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
|
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
|
||||||
help_text=_('Regenerates the order and ticket secrets. You will '
|
help_text=_('Regenerates the order and ticket secrets. You will '
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ from i18nfield.strings import LazyI18nString
|
|||||||
from phonenumber_field.formfields import PhoneNumberField
|
from phonenumber_field.formfields import PhoneNumberField
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.models import WebHook
|
from pretix.api.models import WebHook
|
||||||
from pretix.api.webhooks import get_all_webhook_events
|
from pretix.api.webhooks import get_all_webhook_events
|
||||||
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
|
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
|
||||||
@@ -311,6 +312,11 @@ class DeviceForm(forms.ModelForm):
|
|||||||
'-has_subevents', '-date_from'
|
'-has_subevents', '-date_from'
|
||||||
)
|
)
|
||||||
self.fields['gate'].queryset = organizer.gates.all()
|
self.fields['gate'].queryset = organizer.gates.all()
|
||||||
|
self.fields['security_profile'] = forms.ChoiceField(
|
||||||
|
label=self.fields['security_profile'].label,
|
||||||
|
help_text=self.fields['security_profile'].help_text,
|
||||||
|
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
@@ -344,6 +350,11 @@ class DeviceBulkEditForm(forms.ModelForm):
|
|||||||
'-has_subevents', '-date_from'
|
'-has_subevents', '-date_from'
|
||||||
)
|
)
|
||||||
self.fields['gate'].queryset = organizer.gates.all()
|
self.fields['gate'].queryset = organizer.gates.all()
|
||||||
|
self.fields['security_profile'] = forms.ChoiceField(
|
||||||
|
label=self.fields['security_profile'].label,
|
||||||
|
help_text=self.fields['security_profile'].help_text,
|
||||||
|
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class StaffSessionForm(forms.ModelForm):
|
|||||||
|
|
||||||
class UserEditForm(forms.ModelForm):
|
class UserEditForm(forms.ModelForm):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
|
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||||
"Please choose a different one."),
|
"Please choose a different one."),
|
||||||
'pw_mismatch': _("Please enter the same password twice"),
|
'pw_mismatch': _("Please enter the same password twice"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -613,7 +613,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
|
|
||||||
if logentry.action_type == 'pretix.event.order.consent':
|
if logentry.action_type == 'pretix.event.order.consent':
|
||||||
return _('The user confirmed the following message: "{}"').format(
|
return _('The user confirmed the following message: "{}"').format(
|
||||||
bleach.clean(logentry.parsed_data.get('msg'), tags=[], strip=True)
|
bleach.clean(logentry.parsed_data.get('msg'), tags=set(), strip=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
if logentry.action_type == 'pretix.event.order.canceled':
|
if logentry.action_type == 'pretix.event.order.canceled':
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def get_event_navigation(request: HttpRequest):
|
|||||||
'active': url.url_name == 'event.settings.tickets',
|
'active': url.url_name == 'event.settings.tickets',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('E-mail'),
|
'label': _('Email'),
|
||||||
'url': reverse('control:event.settings.mail', kwargs={
|
'url': reverse('control:event.settings.mail', kwargs={
|
||||||
'event': request.event.slug,
|
'event': request.event.slug,
|
||||||
'organizer': request.event.organizer.slug,
|
'organizer': request.event.organizer.slug,
|
||||||
@@ -132,16 +132,6 @@ def get_event_navigation(request: HttpRequest):
|
|||||||
'icon': 'wrench',
|
'icon': 'wrench',
|
||||||
'children': event_settings
|
'children': event_settings
|
||||||
})
|
})
|
||||||
if request.event.has_subevents:
|
|
||||||
nav.append({
|
|
||||||
'label': pgettext_lazy('subevent', 'Dates'),
|
|
||||||
'url': reverse('control:event.subevents', kwargs={
|
|
||||||
'event': request.event.slug,
|
|
||||||
'organizer': request.event.organizer.slug,
|
|
||||||
}),
|
|
||||||
'active': ('event.subevent' in url.url_name),
|
|
||||||
'icon': 'calendar',
|
|
||||||
})
|
|
||||||
|
|
||||||
if 'can_change_items' in request.eventpermset:
|
if 'can_change_items' in request.eventpermset:
|
||||||
nav.append({
|
nav.append({
|
||||||
@@ -197,6 +187,18 @@ def get_event_navigation(request: HttpRequest):
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if 'can_change_event_settings' in request.eventpermset:
|
||||||
|
if request.event.has_subevents:
|
||||||
|
nav.append({
|
||||||
|
'label': pgettext_lazy('subevent', 'Dates'),
|
||||||
|
'url': reverse('control:event.subevents', kwargs={
|
||||||
|
'event': request.event.slug,
|
||||||
|
'organizer': request.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'active': ('event.subevent' in url.url_name),
|
||||||
|
'icon': 'calendar',
|
||||||
|
})
|
||||||
|
|
||||||
if 'can_view_orders' in request.eventpermset:
|
if 'can_view_orders' in request.eventpermset:
|
||||||
children = [
|
children = [
|
||||||
{
|
{
|
||||||
@@ -496,7 +498,7 @@ def get_organizer_navigation(request):
|
|||||||
'active': url.url_name.startswith('organizer.propert'),
|
'active': url.url_name.startswith('organizer.propert'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('E-mail'),
|
'label': _('Email'),
|
||||||
'url': reverse('control:organizer.settings.mail', kwargs={
|
'url': reverse('control:organizer.settings.mail', kwargs={
|
||||||
'organizer': request.organizer.slug,
|
'organizer': request.organizer.slug,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
|
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
|
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
|
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{{ html_head|safe }}
|
{{ html_head|safe }}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
<strong>
|
<strong>
|
||||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=c.position.order.code %}">{{ c.position.order.code }}</a>-{{ c.position.positionid }}
|
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=c.position.order.code %}">{{ c.position.order.code }}</a>-{{ c.position.positionid }}
|
||||||
</strong>
|
</strong>
|
||||||
|
{% include "pretixcontrol/checkin/fragment_checkin_source_type.html" with source_type=c.raw_source_type %}
|
||||||
{% if c.position.attendee_name %}
|
{% if c.position.attendee_name %}
|
||||||
<br>
|
<br>
|
||||||
<small>
|
<small>
|
||||||
@@ -143,7 +144,7 @@
|
|||||||
</small>
|
</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="fa fa-qrcode fa-fw"></span>
|
{% include "pretixcontrol/checkin/fragment_checkin_source_type.html" with source_type=c.raw_source_type %}
|
||||||
<span title="{{ c.raw_barcode }}">
|
<span title="{{ c.raw_barcode }}">
|
||||||
{{ c.raw_barcode|slice:":16" }}{% if c.raw_barcode|length > 16 %}…{% endif %}
|
{{ c.raw_barcode|slice:":16" }}{% if c.raw_barcode|length > 16 %}…{% endif %}
|
||||||
<button type="button" class="btn btn-xs btn-link btn-clipboard" data-clipboard-text="{{ c.raw_barcode }}">
|
<button type="button" class="btn btn-xs btn-link btn-clipboard" data-clipboard-text="{{ c.raw_barcode }}">
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load getitem %}
|
||||||
|
|
||||||
|
{% if source_type %}
|
||||||
|
{% with media_types|getitem:source_type as media_type %}
|
||||||
|
{% if "." in media_type.icon %}
|
||||||
|
<img src="{% static media_type.icon %}" class="fa-like-image"
|
||||||
|
data-toggle="tooltip" title="{{ media_type.verbose_name }}">
|
||||||
|
{% else %}
|
||||||
|
<span class="fa fa-fw fa-{{ media_type.icon }} text-muted"
|
||||||
|
data-toggle="tooltip" title="{{ media_type.verbose_name }}"></span>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
@@ -185,6 +185,7 @@
|
|||||||
<span class="fa fa-magic text-muted"
|
<span class="fa fa-magic text-muted"
|
||||||
data-toggle="tooltip" title="{% trans "Checked in automatically" %}"></span>
|
data-toggle="tooltip" title="{% trans "Checked in automatically" %}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include "pretixcontrol/checkin/fragment_checkin_source_type.html" with source_type=e.last_entry_source_type %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -67,7 +67,6 @@
|
|||||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||||
{% bootstrap_field form.addon_match layout="control" %}
|
{% bootstrap_field form.addon_match layout="control" %}
|
||||||
{% bootstrap_field form.exit_all_at layout="control" %}
|
{% bootstrap_field form.exit_all_at layout="control" %}
|
||||||
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
|
||||||
{% if form.gates %}
|
{% if form.gates %}
|
||||||
{% bootstrap_field form.gates layout="control" %}
|
{% bootstrap_field form.gates layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -101,7 +101,6 @@
|
|||||||
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th class="iconcol">{% trans "Automated check-in" %}</th>
|
|
||||||
<th>{% trans "Products" %}</th>
|
<th>{% trans "Products" %}</th>
|
||||||
<th class="action-col-2"></th>
|
<th class="action-col-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -137,17 +136,6 @@
|
|||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
|
||||||
{% for channel in cl.auto_checkin_sales_channels.all %}
|
|
||||||
{% if "." in channel.icon %}
|
|
||||||
<img src="{% static channel.icon %}" class="fa-like-image"
|
|
||||||
data-toggle="tooltip" title="{{ channel.label }}">
|
|
||||||
{% else %}
|
|
||||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
|
||||||
data-toggle="tooltip" title="{{ channel.label }}"></span>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if cl.all_products %}
|
{% if cl.all_products %}
|
||||||
<em>{% trans "All" %}</em>
|
<em>{% trans "All" %}</em>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}{% trans "Organizer" %}{% endblock %}
|
{% block title %}{% trans "Organizer" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "E-mail sending" %}</h1>
|
<h1>{% trans "Email sending" %}</h1>
|
||||||
<form action="" method="post" class="form-horizontal">
|
<form action="" method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="panel-group" id="email">
|
<div class="panel-group" id="email">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="panel-body form-horizontal">
|
<div class="panel-body form-horizontal">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
E-mails will be sent through the system's default server. They will show the following
|
Emails will be sent through the system's default server. They will show the following
|
||||||
sender information:
|
sender information:
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<div class="panel-body form-horizontal">
|
<div class="panel-body form-horizontal">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
E-mails will be sent through the system's default server but with your own sender
|
Emails will be sent through the system's default server but with your own sender
|
||||||
address.
|
address.
|
||||||
This will make your emails look more personalized and coming directly from you, but it
|
This will make your emails look more personalized and coming directly from you, but it
|
||||||
also might require some extra steps to ensure good deliverability.
|
also might require some extra steps to ensure good deliverability.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}{% trans "Organizer" %}{% endblock %}
|
{% block title %}{% trans "Organizer" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "E-mail sending" %}</h1>
|
<h1>{% trans "Email sending" %}</h1>
|
||||||
<form action="" method="post" class="form-horizontal">
|
<form action="" method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for k, v in request.POST.items %}
|
{% for k, v in request.POST.items %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}{% trans "Organizer" %}{% endblock %}
|
{% block title %}{% trans "Organizer" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "E-mail sending" %}</h1>
|
<h1>{% trans "Email sending" %}</h1>
|
||||||
<form action="" method="post" class="form-horizontal">
|
<form action="" method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for k, v in request.POST.items %}
|
{% for k, v in request.POST.items %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% load hierarkey_form %}
|
{% load hierarkey_form %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "E-mail settings" %}</h1>
|
<h1>{% trans "Email settings" %}</h1>
|
||||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
|
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
|
||||||
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
|
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
{% bootstrap_field form.mail_attach_ical_description layout="control" %}
|
{% bootstrap_field form.mail_attach_ical_description layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "E-mail design" %}</legend>
|
<legend>{% trans "Email design" %}</legend>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for r in renderers.values %}
|
{% for r in renderers.values %}
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "E-mail content" %}</legend>
|
<legend>{% trans "Email content" %}</legend>
|
||||||
<h4>{% trans "Text" %}</h4>
|
<h4>{% trans "Text" %}</h4>
|
||||||
<div class="panel-group" id="questions_group">
|
<div class="panel-group" id="questions_group">
|
||||||
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
|
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<h4>{% trans "Customer data (once per order)" %}</h4>
|
<h4>{% trans "Customer data (once per order)" %}</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-md-3">
|
<label class="control-label col-md-3">
|
||||||
{% trans "E-mail" %}
|
{% trans "Email" %}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
|
|||||||
@@ -41,7 +41,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||||
{% bootstrap_field form.currency layout="control" %}
|
{% bootstrap_field form.currency layout="control" %}
|
||||||
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
|
{% bootstrap_field form.no_taxes layout="control" %}
|
||||||
|
<div data-display-dependency="#{{ form.no_taxes.id_for_label }}" data-inverse>
|
||||||
|
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Display settings" %}</legend>
|
<legend>{% trans "Display settings" %}</legend>
|
||||||
|
|||||||
@@ -16,8 +16,18 @@
|
|||||||
{% bootstrap_field form.internal_name layout="control" %}
|
{% bootstrap_field form.internal_name layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
{% bootstrap_field form.description layout="control" %}
|
{% bootstrap_field form.description layout="control" %}
|
||||||
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-lg-9" %}
|
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-md-9" %}
|
||||||
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-lg-9" %}
|
<div class="row" data-display-dependency="#id_category_type_2">
|
||||||
|
<div class="col-md-offset-3 col-md-9">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Please note that cross-selling categories are intended as a marketing feature and are not
|
||||||
|
suitable for strictly ensuring that products are only available in certain combinations.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-md-9" %}
|
||||||
{% bootstrap_field form.cross_selling_match_products layout="control" %}
|
{% bootstrap_field form.cross_selling_match_products layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -296,11 +296,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
<div class="formset" data-formset data-formset-prefix="{{ add_formset.prefix }}">
|
<div class="formset" data-formset data-formset-prefix="{{ add_position_formset.prefix }}">
|
||||||
{{ add_formset.management_form }}
|
{{ add_position_formset.management_form }}
|
||||||
{% bootstrap_formset_errors add_formset %}
|
{% bootstrap_formset_errors add_position_formset %}
|
||||||
<div data-formset-body>
|
<div data-formset-body>
|
||||||
{% for add_form in add_formset %}
|
{% for add_form in add_position_formset %}
|
||||||
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
@@ -351,25 +351,25 @@
|
|||||||
</button>
|
</button>
|
||||||
{% trans "Add product" %}
|
{% trans "Add product" %}
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
{{ add_formset.empty_form.id }}
|
{{ add_position_formset.empty_form.id }}
|
||||||
{% bootstrap_field add_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
{% bootstrap_field add_position_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
{% bootstrap_field add_formset.empty_form.itemvar layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
|
||||||
{% bootstrap_field add_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
||||||
{% if add_formset.empty_form.addon_to %}
|
{% if add_position_formset.empty_form.addon_to %}
|
||||||
{% bootstrap_field add_formset.empty_form.addon_to layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.addon_to layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if add_formset.empty_form.subevent %}
|
{% if add_position_formset.empty_form.subevent %}
|
||||||
{% bootstrap_field add_formset.empty_form.subevent layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.subevent layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if add_formset.empty_form.used_membership %}
|
{% if add_position_formset.empty_form.used_membership %}
|
||||||
{% bootstrap_field add_formset.empty_form.used_membership layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.used_membership layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field add_formset.empty_form.seat layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.seat layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,13 +431,77 @@
|
|||||||
{% bootstrap_field fee.form.operation_cancel layout='inline' %}
|
{% bootstrap_field fee.form.operation_cancel layout='inline' %}
|
||||||
{% if fee.fee_type == "payment" %}
|
{% if fee.fee_type == "payment" %}
|
||||||
<em class="text-danger">
|
<em class="text-danger">
|
||||||
{% trans "Manually modifying payment fees is discouraged since they might automatically be on subsequent order changes or when choosing a different payment method." %}
|
{% trans "Manually modifying payment fees is discouraged since they might automatically be updated on subsequent order changes or when choosing a different payment method." %}
|
||||||
</em>
|
</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="formset" data-formset data-formset-prefix="{{ add_fee_formset.prefix }}">
|
||||||
|
{{ add_fee_formset.management_form }}
|
||||||
|
{% bootstrap_formset_errors add_fee_formset %}
|
||||||
|
<div data-formset-body>
|
||||||
|
{% for add_form in add_fee_formset %}
|
||||||
|
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||||
|
data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
{% trans "Add fee" %}
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ add_form.id }}
|
||||||
|
{% bootstrap_field add_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-horizontal">
|
||||||
|
{% bootstrap_field add_form.fee_type layout='control' %}
|
||||||
|
{% bootstrap_field add_form.value addon_after=request.event.currency layout='control' %}
|
||||||
|
{% bootstrap_field add_form.tax_rule layout='control' %}
|
||||||
|
{% bootstrap_field add_form.description layout='control' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script type="form-template" data-formset-empty-form>
|
||||||
|
{% escapescript %}
|
||||||
|
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||||
|
data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
{% trans "Add fee" %}
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ add_fee_formset.empty_form.id }}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-horizontal">
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.fee_type layout='control' %}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.value addon_after=request.event.currency layout='control' %}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.tax_rule layout='control' %}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.description layout='control' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endescapescript %}
|
||||||
|
</script>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-primary" data-formset-add>
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Add fee" %}</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default items">
|
<div class="panel panel-default items">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
|
|||||||
@@ -46,11 +46,13 @@
|
|||||||
<div id="cp{{ pos.id }}">
|
<div id="cp{{ pos.id }}">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% for form in forms %}
|
{% for form in forms %}
|
||||||
{% if form.pos.item != pos.item %}
|
<div class="profile-scope">
|
||||||
{# Add-Ons #}
|
{% if form.pos.item != pos.item %}
|
||||||
<legend>+ {{ form.pos.item }}</legend>
|
{# Add-Ons #}
|
||||||
{% endif %}
|
<legend>+ {{ form.pos.item }}</legend>
|
||||||
{% bootstrap_form form layout="control" %}
|
{% endif %}
|
||||||
|
{% bootstrap_form form layout="control" %}
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -198,7 +198,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
{% if order.status == "n" %}
|
{% if order.status == "n" and not order.require_approval %}
|
||||||
<dt>{% trans "Expiry date" %}</dt>
|
<dt>{% trans "Expiry date" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}
|
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.method == "POST" %}
|
{% if request.method == "POST" %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "E-mail preview" %}</legend>
|
<legend>{% trans "Email preview" %}</legend>
|
||||||
<div class="tab-pane mail-preview-group">
|
<div class="tab-pane mail-preview-group">
|
||||||
<div lang="{{ order.locale }}" class="mail-preview">
|
<div lang="{{ order.locale }}" class="mail-preview">
|
||||||
<strong>{{ preview_output.subject }}</strong><br><br>
|
<strong>{{ preview_output.subject }}</strong><br><br>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
{% trans "active" %}
|
{% trans "active" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt>{% trans "E-mail" %}</dt>
|
<dt>{% trans "Email" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ customer.email|default_if_none:"" }}
|
{{ customer.email|default_if_none:"" }}
|
||||||
{% if customer.email and not customer.provider %}
|
{% if customer.email and not customer.provider %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}{% trans "Organizer" %}{% endblock %}
|
{% block title %}{% trans "Organizer" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "E-mail settings" %}</h1>
|
<h1>{% trans "Email settings" %}</h1>
|
||||||
|
|
||||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
|
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
|
||||||
mail-preview-url="{% url "control:organizer.settings.mail.preview" organizer=request.organizer.slug %}">
|
mail-preview-url="{% url "control:organizer.settings.mail.preview" organizer=request.organizer.slug %}">
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
{% bootstrap_field form.mail_bcc layout="control" %}
|
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "E-mail content" %}</legend>
|
<legend>{% trans "Email content" %}</legend>
|
||||||
<div class="panel-group" id="questions_group">
|
<div class="panel-group" id="questions_group">
|
||||||
{% blocktrans asvar title_customer_registration %}Customer account registration{% endblocktrans %}
|
{% blocktrans asvar title_customer_registration %}Customer account registration{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="customer_registration" title=title_customer_registration items="mail_subject_customer_registration,mail_text_customer_registration" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="customer_registration" title=title_customer_registration items="mail_subject_customer_registration,mail_text_customer_registration" %}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Notification type" %}</th>
|
<th>{% trans "Notification type" %}</th>
|
||||||
<th class="text-center">{% trans "E-Mail notification" %}</th>
|
<th class="text-center">{% trans "Email notification" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
{% trans "E-mail address" %}
|
{% trans "Email address" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ class Forgot(TemplateView):
|
|||||||
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
|
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
|
||||||
|
|
||||||
except SendMailException:
|
except SendMailException:
|
||||||
logger.exception('Sending password reset e-mail to \"' + email + '\" failed.')
|
logger.exception('Sending password reset email to \"' + email + '\" failed.')
|
||||||
|
|
||||||
except RepeatedResetDenied:
|
except RepeatedResetDenied:
|
||||||
pass
|
pass
|
||||||
@@ -354,10 +354,10 @@ class Forgot(TemplateView):
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
if has_redis:
|
if has_redis:
|
||||||
messages.info(request, _('If the address is registered to valid account, then we have sent you an e-mail containing further instructions. '
|
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions. '
|
||||||
'Please note that we will send at most one email every 24 hours.'))
|
'Please note that we will send at most one email every 24 hours.'))
|
||||||
else:
|
else:
|
||||||
messages.info(request, _('If the address is registered to valid account, then we have sent you an e-mail containing further instructions.'))
|
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions.'))
|
||||||
|
|
||||||
return redirect('control:auth.forgot')
|
return redirect('control:auth.forgot')
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from django.views.generic import FormView, ListView, TemplateView
|
|||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.api.views.checkin import _redeem_process
|
from pretix.api.views.checkin import _redeem_process
|
||||||
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, Order, OrderPosition
|
from pretix.base.models import Checkin, Order, OrderPosition
|
||||||
from pretix.base.models.checkin import CheckinList
|
from pretix.base.models.checkin import CheckinList
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
@@ -81,9 +82,7 @@ class CheckInListQueryMixin:
|
|||||||
position_id=OuterRef('pk'),
|
position_id=OuterRef('pk'),
|
||||||
list_id=self.list.pk,
|
list_id=self.list.pk,
|
||||||
type=Checkin.TYPE_ENTRY
|
type=Checkin.TYPE_ENTRY
|
||||||
).order_by().values('position_id').annotate(
|
).order_by('-datetime').values('position_id')
|
||||||
m=Max('datetime')
|
|
||||||
).values('m')
|
|
||||||
cqs_exit = Checkin.objects.filter(
|
cqs_exit = Checkin.objects.filter(
|
||||||
position_id=OuterRef('pk'),
|
position_id=OuterRef('pk'),
|
||||||
list_id=self.list.pk,
|
list_id=self.list.pk,
|
||||||
@@ -103,7 +102,7 @@ class CheckInListQueryMixin:
|
|||||||
status_q,
|
status_q,
|
||||||
order__event=self.request.event,
|
order__event=self.request.event,
|
||||||
).annotate(
|
).annotate(
|
||||||
last_entry=Subquery(cqs),
|
last_entry=Subquery(cqs[:1].values('datetime')),
|
||||||
last_exit=Subquery(cqs_exit),
|
last_exit=Subquery(cqs_exit),
|
||||||
auto_checked_in=Exists(
|
auto_checked_in=Exists(
|
||||||
Checkin.objects.filter(
|
Checkin.objects.filter(
|
||||||
@@ -112,7 +111,8 @@ class CheckInListQueryMixin:
|
|||||||
list_id=self.list.pk,
|
list_id=self.list.pk,
|
||||||
auto_checked_in=True
|
auto_checked_in=True
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
|
last_entry_source_type=Subquery(cqs[:1].values('raw_source_type'))
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'variation', 'order', 'addon_to'
|
'item', 'variation', 'order', 'addon_to'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
@@ -157,6 +157,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['media_types'] = MEDIA_TYPES
|
||||||
ctx['checkinlist'] = self.list
|
ctx['checkinlist'] = self.list
|
||||||
if self.request.event.has_subevents:
|
if self.request.event.has_subevents:
|
||||||
ctx['seats'] = (
|
ctx['seats'] = (
|
||||||
@@ -299,7 +300,7 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
|
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
|
||||||
"limit_products", "auto_checkin_sales_channels"
|
"limit_products",
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
@@ -497,6 +498,7 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
|
ctx['media_types'] = MEDIA_TYPES
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -807,7 +807,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
|||||||
r._csp_ignore = True
|
r._csp_ignore = True
|
||||||
return r
|
return r
|
||||||
else:
|
else:
|
||||||
raise Http404(_('Unknown e-mail renderer.'))
|
raise Http404(_('Unknown email renderer.'))
|
||||||
|
|
||||||
|
|
||||||
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
|
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
messages.success(self.request, _('Your changes have been saved.'))
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
if form.has_changed():
|
if form.has_changed():
|
||||||
self.object.log_action(
|
self.object.log_action(
|
||||||
'pretix.event.category.reordered', user=self.request.user, data={
|
'pretix.event.category.changed', user=self.request.user, data={
|
||||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -122,10 +122,11 @@ from pretix.control.forms.filter import (
|
|||||||
)
|
)
|
||||||
from pretix.control.forms.orders import (
|
from pretix.control.forms.orders import (
|
||||||
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
|
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
|
||||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm,
|
||||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
OrderFeeAddFormset, OrderFeeChangeForm, OrderLocaleForm, OrderMailForm,
|
||||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm,
|
||||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
OrderPositionMailForm, OrderRefundForm, OtherOperationsForm,
|
||||||
|
ReactivateOrderForm,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.rrule import RRuleForm
|
from pretix.control.forms.rrule import RRuleForm
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
@@ -1874,18 +1875,30 @@ class OrderChange(OrderView):
|
|||||||
data=self.request.POST if self.request.method == "POST" else None)
|
data=self.request.POST if self.request.method == "POST" else None)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def add_formset(self):
|
def add_position_formset(self):
|
||||||
ff = formset_factory(
|
ff = formset_factory(
|
||||||
OrderPositionAddForm, formset=OrderPositionAddFormset,
|
OrderPositionAddForm, formset=OrderPositionAddFormset,
|
||||||
can_order=False, can_delete=True, extra=0
|
can_order=False, can_delete=True, extra=0
|
||||||
)
|
)
|
||||||
return ff(
|
return ff(
|
||||||
prefix='add',
|
prefix='add_position',
|
||||||
order=self.order,
|
order=self.order,
|
||||||
items=self.items,
|
items=self.items,
|
||||||
data=self.request.POST if self.request.method == "POST" else None
|
data=self.request.POST if self.request.method == "POST" else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def add_fee_formset(self):
|
||||||
|
ff = formset_factory(
|
||||||
|
OrderFeeAddForm, formset=OrderFeeAddFormset,
|
||||||
|
can_order=False, can_delete=True, extra=0
|
||||||
|
)
|
||||||
|
return ff(
|
||||||
|
prefix='add_fee',
|
||||||
|
order=self.order,
|
||||||
|
data=self.request.POST if self.request.method == "POST" else None
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def items(self):
|
def items(self):
|
||||||
return self.request.event.items.prefetch_related('variations', 'tax_rule').all()
|
return self.request.event.items.prefetch_related('variations', 'tax_rule').all()
|
||||||
@@ -1914,7 +1927,8 @@ class OrderChange(OrderView):
|
|||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['positions'] = self.positions
|
ctx['positions'] = self.positions
|
||||||
ctx['fees'] = self.fees
|
ctx['fees'] = self.fees
|
||||||
ctx['add_formset'] = self.add_formset
|
ctx['add_position_formset'] = self.add_position_formset
|
||||||
|
ctx['add_fee_formset'] = self.add_fee_formset
|
||||||
ctx['other_form'] = self.other_form
|
ctx['other_form'] = self.other_form
|
||||||
ctx['use_revocation_list'] = self.request.event.ticket_secret_generator.use_revocation_list
|
ctx['use_revocation_list'] = self.request.event.ticket_secret_generator.use_revocation_list
|
||||||
return ctx
|
return ctx
|
||||||
@@ -1929,12 +1943,35 @@ class OrderChange(OrderView):
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _process_add(self, ocm):
|
def _process_add_fees(self, ocm):
|
||||||
if not self.add_formset.is_valid():
|
if not self.add_fee_formset.is_valid():
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
for f in self.add_formset.forms:
|
for f in self.add_fee_formset.forms:
|
||||||
if f in self.add_formset.deleted_forms or not f.has_changed():
|
if f in self.add_fee_formset.deleted_forms or not f.has_changed():
|
||||||
|
continue
|
||||||
|
|
||||||
|
f = OrderFee(
|
||||||
|
fee_type=f.cleaned_data['fee_type'],
|
||||||
|
value=f.cleaned_data['value'],
|
||||||
|
order=ocm.order,
|
||||||
|
tax_rule=f.cleaned_data['tax_rule'],
|
||||||
|
description=f.cleaned_data['description'],
|
||||||
|
)
|
||||||
|
f._calculate_tax()
|
||||||
|
try:
|
||||||
|
ocm.add_fee(f)
|
||||||
|
except OrderError as e:
|
||||||
|
f.custom_error = str(e)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _process_add_positions(self, ocm):
|
||||||
|
if not self.add_position_formset.is_valid():
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
for f in self.add_position_formset.forms:
|
||||||
|
if f in self.add_position_formset.deleted_forms or not f.has_changed():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if '-' in f.cleaned_data['itemvar']:
|
if '-' in f.cleaned_data['itemvar']:
|
||||||
@@ -1959,7 +1996,7 @@ class OrderChange(OrderView):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _process_fees(self, ocm):
|
def _process_change_fees(self, ocm):
|
||||||
for f in self.fees:
|
for f in self.fees:
|
||||||
if not f.form.is_valid():
|
if not f.form.is_valid():
|
||||||
return False
|
return False
|
||||||
@@ -1980,7 +2017,7 @@ class OrderChange(OrderView):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _process_change(self, ocm):
|
def _process_change_positions(self, ocm):
|
||||||
for p in self.positions:
|
for p in self.positions:
|
||||||
if not p.form.is_valid():
|
if not p.form.is_valid():
|
||||||
return False
|
return False
|
||||||
@@ -2061,7 +2098,11 @@ class OrderChange(OrderView):
|
|||||||
notify=notify,
|
notify=notify,
|
||||||
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
|
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
|
||||||
)
|
)
|
||||||
form_valid = self._process_add(ocm) and self._process_fees(ocm) and self._process_change(ocm) and self._process_other(ocm)
|
form_valid = (self._process_add_fees(ocm) and
|
||||||
|
self._process_add_positions(ocm) and
|
||||||
|
self._process_change_fees(ocm) and
|
||||||
|
self._process_change_positions(ocm) and
|
||||||
|
self._process_other(ocm))
|
||||||
|
|
||||||
if not form_valid:
|
if not form_valid:
|
||||||
messages.error(self.request, _('An error occurred. Please see the details below.'))
|
messages.error(self.request, _('An error occurred. Please see the details below.'))
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class UserResetView(AdministratorPermissionRequiredMixin, RecentAuthenticationRe
|
|||||||
|
|
||||||
self.object.log_action('pretix.control.auth.user.forgot_password.mail_sent',
|
self.object.log_action('pretix.control.auth.user.forgot_password.mail_sent',
|
||||||
user=request.user)
|
user=request.user)
|
||||||
messages.success(request, _('We sent out an e-mail containing further instructions.'))
|
messages.success(request, _('We sent out an email containing further instructions.'))
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
|
|||||||
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
_('Name'), _('E-mail address'), _('Phone number'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
|
_('Name'), _('Email address'), _('Phone number'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
|
||||||
_('Language'), _('Priority')
|
_('Language'), _('Priority')
|
||||||
]
|
]
|
||||||
if self.request.event.has_subevents:
|
if self.request.event.has_subevents:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -622,40 +622,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -656,40 +656,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
|
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr "البحث في الاستفسارات"
|
msgstr "البحث في الاستفسارات"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "الكل"
|
msgstr "الكل"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "لا شيء"
|
msgstr "لا شيء"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "المختارة فقط"
|
msgstr "المختارة فقط"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "اضغط لاغلاق الصفحة"
|
msgstr "اضغط لاغلاق الصفحة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "لم تقم بحفظ التعديلات!"
|
msgstr "لم تقم بحفظ التعديلات!"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
|
||||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -623,40 +623,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user