Compare commits

...

86 Commits

Author SHA1 Message Date
Raphael Michel
fe5a2286a4 Update cache handling 2021-01-01 20:13:09 +01:00
Raphael Michel
3d66bfee7f Update checkout.py 2021-01-01 20:13:09 +01:00
Raphael Michel
0b495a4070 Validate email addresses fo valid DNS names 2021-01-01 20:13:09 +01:00
Martin Gross
23aba9b5ef Move Mapquest-Geocoding to HTTPS 2020-12-30 13:11:10 +01:00
Raphael Michel
454f0f6fc8 Create log entry upon order email confirmation 2020-12-23 17:52:20 +01:00
Raphael Michel
002ff38fba Fix crash in add-on form (PRETIXEU-3GV) 2020-12-23 17:46:52 +01:00
luto
dc8bd59715 Add convenience redirect …/event/(org)/ => …/organizer/(org)/ (#1893) 2020-12-23 16:50:41 +01:00
Raphael Michel
56a2da08df Merge pull request #1891 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-12-23 16:31:41 +01:00
Maarten van den Berg
4762d6818f Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3909 of 3909 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2020-12-23 16:31:29 +01:00
Richard Schreiber
e99e91d20f Show event’s date and location in widget if event is subevent (#1892) 2020-12-23 16:31:24 +01:00
0xflotus
9fee2d0fbc Docs: Enabling Syntax Highlighting (#1890) 2020-12-23 10:24:39 +01:00
Raphael Michel
3f30ddc9ab Fix #1888 -- UnknownLocaleError if locale is set 2020-12-22 13:14:56 +01:00
Raphael Michel
641a848f30 Bump to 3.15.0.dev0 2020-12-22 12:40:04 +01:00
Raphael Michel
a582322847 Bump to 3.14.0 2020-12-22 12:39:31 +01:00
Raphael Michel
a7ec7491ec Merge pull request #1887 from pretix-translations/weblate-pretix-pretix 2020-12-22 12:38:16 +01:00
Raphael Michel
90ae8860dd Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3909 of 3909 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2020-12-22 12:38:06 +01:00
Raphael Michel
00ca75e119 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3909 of 3909 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2020-12-22 12:38:06 +01:00
Raphael Michel
455fb2e560 Make fake e-mail field look more consistent 2020-12-22 12:37:49 +01:00
Raphael Michel
1ec4c524f8 Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-12-22 12:06:11 +01:00
Raphael Michel
75b9b04c65 Merge pull request #1885 from pretix-translations/weblate-pretix-pretix 2020-12-22 12:05:34 +01:00
albert
bf0a9675f4 Translated on translate.pretix.eu (Catalan)
Currently translated at 49.3% (1923 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-22 11:46:56 +01:00
Maarten van den Berg
853877f2da Translated on translate.pretix.eu (Dutch (informal))
Currently translated at 100.0% (3900 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_Informal/

powered by weblate
2020-12-22 11:46:55 +01:00
Maarten van den Berg
2e44900c43 Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (3900 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2020-12-22 11:46:55 +01:00
albert
c5085bb46e Translated on translate.pretix.eu (Catalan)
Currently translated at 49.0% (1911 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-22 11:46:55 +01:00
albert
da859b9980 Translated on translate.pretix.eu (Catalan)
Currently translated at 47.4% (1849 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-22 11:46:55 +01:00
albert
b6f30f6996 Translated on translate.pretix.eu (Catalan)
Currently translated at 4.7% (6 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ca/

powered by weblate
2020-12-22 11:46:55 +01:00
albert
9fde378eac Translated on translate.pretix.eu (Catalan)
Currently translated at 46.3% (1807 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-22 11:46:55 +01:00
Raphael Michel
52e9525f64 Fix isort of test 2020-12-22 11:46:45 +01:00
Raphael Michel
80aeeed855 Fix bug in 8ed41a127 2020-12-22 11:33:37 +01:00
Raphael Michel
d207514c9a Change migration graph to be compatible with backports 2020-12-22 11:33:37 +01:00
Raphael Michel
1286e53b85 Reduce lifetime of export files 2020-12-22 10:48:06 +01:00
Raphael Michel
7c0df5b755 [SECURITY] Rate limiting for login 2020-12-22 10:47:47 +01:00
Raphael Michel
8889d8441e [SECURITY] Rate limiting for password change form 2020-12-22 10:47:47 +01:00
Raphael Michel
c60a25f2bc [SECURITY] Bind relevant cached file downloads to the current session 2020-12-22 10:47:47 +01:00
Raphael Michel
a3dd015c23 [SECURITY] Fix unvalidated redirect 2020-12-22 10:47:47 +01:00
Raphael Michel
736ecbd7b6 [SECURITY] Prevent phishing through misleading link titles 2020-12-22 10:47:47 +01:00
Raphael Michel
8ed41a1276 Add csp_additional_header config option 2020-12-21 19:16:09 +01:00
Raphael Michel
06643232cf Fix #1420 -- Hide "shop not available" from log files 2020-12-19 20:04:48 +01:00
Raphael Michel
90399d2567 Remove subevent=None from suggested voucher URLs 2020-12-19 19:55:28 +01:00
Raphael Michel
609203196b SMTP settings: Timeout during testing 2020-12-19 19:46:23 +01:00
Raphael Michel
070b871254 Bank transfer import: Auto-detect valid split payments 2020-12-19 16:55:06 +01:00
Raphael Michel
cbadb2c395 Bank transfer API: Block concurrent jobs 2020-12-19 16:27:43 +01:00
Raphael Michel
0e9951f964 Backend order detail page: Show pending sum 2020-12-19 16:25:32 +01:00
Richard Schreiber
6afb954b93 Fix #1879 -- Do not add a tab’s hash/id to location.hash if it is inside a .panel (#1881)
Co-authored-by: Raphael Michel <michel@rami.io>
2020-12-18 09:33:27 +01:00
Richard Schreiber
bdf1fc2c23 Added combined radiob uttons for <name>_asked and <name>_required fields (#1880) 2020-12-18 09:33:07 +01:00
Raphael Michel
9c0c8a95fa Merge pull request #1883 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-12-18 09:32:50 +01:00
albert
356a2dc9c5 Translated on translate.pretix.eu (Catalan)
Currently translated at 43.0% (1678 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-18 08:00:11 +01:00
Raphael Michel
4f5a9284ca Merge pull request #1878 from pretix-translations/weblate-pretix-pretix 2020-12-17 16:07:18 +01:00
albert
130b06d26b Translated on translate.pretix.eu (Catalan)
Currently translated at 41.8% (1629 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-16 12:49:37 +01:00
albert
ab4dd9b8de Translated on translate.pretix.eu (Catalan)
Currently translated at 40.9% (1596 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-16 12:49:37 +01:00
albert
bb6b8bd8bb Translated on translate.pretix.eu (Catalan)
Currently translated at 3.9% (5 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ca/

powered by weblate
2020-12-16 12:49:37 +01:00
albert
2aeceeed08 Translated on translate.pretix.eu (Catalan)
Currently translated at 40.9% (1595 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-16 12:49:37 +01:00
Raphael Michel
39223f0f65 Docker setup: Allow to configure number of worker processes 2020-12-16 12:49:13 +01:00
Raphael Michel
33ba4daadb Docker setup: Tune some nginx parameters 2020-12-16 12:42:49 +01:00
Raphael Michel
1f9adcce6e Always use "cleaned" LANGUAGE_CODE in templates 2020-12-16 10:46:47 +01:00
Raphael Michel
4d36676cf8 Allow to filter for partially paid orders 2020-12-15 16:06:59 +01:00
Raphael Michel
821cb54ad0 Backend order list: Show sales channel 2020-12-15 15:53:21 +01:00
Raphael Michel
a40951060f Backend order list: Show payment amount 2020-12-15 15:44:38 +01:00
Raphael Michel
c6a98fad5a Fix involunatery CSS change 2020-12-15 14:52:50 +01:00
Raphael Michel
d3a0405faa Fix another bug in phone_format template tag 2020-12-15 09:56:18 +01:00
Raphael Michel
664bb9a65b Frontend order details: Do not show empty "name" line 2020-12-15 09:56:18 +01:00
Raphael Michel
06d8464998 Merge pull request #1877 from pretix-translations/weblate-pretix-pretix 2020-12-15 09:53:41 +01:00
Raphael Michel
c9b20d2cf5 Translated on translate.pretix.eu (German (informal))
Currently translated at 100.0% (3900 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2020-12-15 09:51:37 +01:00
Raphael Michel
a198635865 Translated on translate.pretix.eu (German)
Currently translated at 100.0% (3900 of 3900 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2020-12-15 09:51:35 +01:00
Raphael Michel
4e26df5752 Fix phone number display issue 2020-12-15 09:38:26 +01:00
Raphael Michel
5caa874263 Merge pull request #1876 from pretix-translations/weblate-pretix-pretix
Co-authored-by: albert <albert.serra.monner@gmail.com>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2020-12-15 09:30:30 +01:00
Raphael Michel
05939537dd Update django.po 2020-12-15 09:28:23 +01:00
Raphael Michel
0d29f8624f Merge branch 'master' into weblate-pretix-pretix 2020-12-15 09:24:47 +01:00
Raphael Michel
0d8db8266d Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2020-12-15 09:22:57 +01:00
albert
09be2c1199 Translated on translate.pretix.eu (Catalan)
Currently translated at 3.1% (4 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ca/

powered by weblate
2020-12-15 09:20:50 +01:00
albert
da8ecb6e6e Translated on translate.pretix.eu (Catalan)
Currently translated at 36.2% (1397 of 3859 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-15 09:20:50 +01:00
Raphael Michel
4240ad43d0 Add order-level telephone field to core (#1872)
Co-authored-by: Martin Gross <gross@rami.io>
2020-12-15 09:20:44 +01:00
Raphael Michel
c47e41ac8a Merge pull request #1874 from pretix/perf2020 2020-12-14 17:02:01 +01:00
Raphael Michel
04bfa63a5e Add region setting to supplement localization (#1875) 2020-12-14 13:15:38 +01:00
Raphael Michel
e311341d01 Extend .dockerignore 2020-12-14 13:11:19 +01:00
Raphael Michel
1f21d1420c Fix import order 2020-12-14 11:45:23 +01:00
Raphael Michel
5c1d637637 Merge pull request #1873 from pretix-translations/weblate-pretix-pretix
Translations update from Weblate
2020-12-14 11:36:50 +01:00
Ondřej Sokol
ecc72d54ad Translated on translate.pretix.eu (Czech)
Currently translated at 28.9% (37 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/cs/

powered by weblate
2020-12-14 11:00:17 +01:00
Ondřej Sokol
ff8a3ea1c3 Translated on translate.pretix.eu (Czech)
Currently translated at 3.5% (134 of 3859 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/cs/

powered by weblate
2020-12-14 11:00:16 +01:00
albert
924bad3484 Translated on translate.pretix.eu (Catalan)
Currently translated at 32.2% (1241 of 3859 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-13 19:16:55 +01:00
albert
808df7a982 Translated on translate.pretix.eu (Catalan)
Currently translated at 2.3% (3 of 128 strings)

Translation: pretix/pretix (frontend)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ca/

powered by weblate
2020-12-13 19:16:55 +01:00
albert
7f196ef6fe Translated on translate.pretix.eu (Catalan)
Currently translated at 30.3% (1171 of 3859 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-13 19:16:55 +01:00
albert
44ef9b608a Translated on translate.pretix.eu (Catalan)
Currently translated at 27.5% (1062 of 3859 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ca/

powered by weblate
2020-12-13 19:16:55 +01:00
Raphael Michel
62b1aec3b0 Save a pointless query on non-series events 2020-12-13 16:31:17 +01:00
Raphael Michel
571fef4ed8 Re-structure some querying on cart and order pages to reduce load 2020-12-13 16:31:17 +01:00
Raphael Michel
5308099d84 Fix 5-second quota caching 2020-12-13 15:50:02 +01:00
177 changed files with 70016 additions and 60495 deletions

View File

@@ -1,3 +1,10 @@
doc/ doc/
env/ env/
res/ res/
local/
.git/
pretixeu/
src/data/
src/pretix/static.dist/
src/dist/

View File

@@ -1,10 +1,13 @@
user www-data www-data; user www-data www-data;
worker_processes 1; worker_processes auto;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
daemon off; daemon off;
worker_rlimit_nofile 262144;
events { events {
worker_connections 4096; worker_connections 16384;
multi_accept on;
use epoll;
} }
http { http {

View File

@@ -3,9 +3,10 @@ cd /pretix/src
export DJANGO_SETTINGS_MODULE=production_settings export DJANGO_SETTINGS_MODULE=production_settings
export DATA_DIR=/data/ export DATA_DIR=/data/
export HOME=/pretix export HOME=/pretix
export NUM_WORKERS=$((2 * $(nproc --all)))
AUTOMIGRATE=${AUTOMIGRATE:-yes} AUTOMIGRATE=${AUTOMIGRATE:-yes}
NUM_WORKERS_DEFAULT=$((2 * $(nproc --all)))
export NUM_WORKERS=${NUM_WORKERS:-$NUM_WORKERS_DEFAULT}
if [ ! -d /data/logs ]; then if [ ! -d /data/logs ]; then
mkdir /data/logs; mkdir /data/logs;

View File

@@ -105,7 +105,12 @@ Example::
``csp_log`` ``csp_log``
Log violations of the Content Security Policy (CSP). Defaults to ``on``. Log violations of the Content Security Policy (CSP). Defaults to ``on``.
``csp_additional_header``
Specifies a CSP header that will be **merged** with pretix's default header. For example, if you set this
to ``script-src https://mycdn.com``, pretix will add ``https://mycdn.com`` as an **additional** allowed source
to all CSP headers. Empty by default.
``loglevel`` ``loglevel``
Set console and file log level (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``. Set console and file log level (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``.

View File

@@ -135,7 +135,7 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
user=pretix user=pretix
; Replace with the password you chose above ; Replace with the password you chose above
password=********* password=*********
; In most docker setups, 172.17.0.1 is the address of the docker host. Adjuts ; In most docker setups, 172.17.0.1 is the address of the docker host. Adjust
; this to wherever your database is running, e.g. the name of a linked container ; this to wherever your database is running, e.g. the name of a linked container
; or of a mounted MySQL socket. ; or of a mounted MySQL socket.
host=172.17.0.1 host=172.17.0.1
@@ -295,7 +295,9 @@ on one machine after each upgrade manually, otherwise multiple containers might
database schema at the same time. database schema at the same time.
To run only the ``pretix-web`` component of pretix as well as a nginx server serving static files, you To run only the ``pretix-web`` component of pretix as well as a nginx server serving static files, you
can invoke the container with ``docker run … pretix/standalone:stable web`` (instead of ``all``). can invoke the container with ``docker run … pretix/standalone:stable web`` (instead of ``all``). You
can adjust the number of ``gunicorn`` processes with the ``NUM_WORKERS`` environment variable (defaults to
two times the number of CPUs detected).
To run only ``pretix-worker``, you can run ``docker run … pretix/standalone:stable taskworker``. You can To run only ``pretix-worker``, you can run ``docker run … pretix/standalone:stable taskworker``. You can
also pass arguments to limit the worker to specific queues or to change the number of concurrent task also pass arguments to limit the worker to specific queues or to change the number of concurrent task

View File

@@ -30,6 +30,7 @@ testmode boolean If ``true``, th
test mode. Only orders in test mode can be deleted. test mode. Only orders in test mode can be deleted.
secret string The secret contained in the link sent to the customer secret string The secret contained in the link sent to the customer
email string The customer email address email string The customer email address
phone string The customer phone number
locale string The locale used for communication with this customer locale string The locale used for communication with this customer
sales_channel string Channel this sale was created through, such as sales_channel string Channel this sale was created through, such as
``"web"``. ``"web"``.
@@ -167,6 +168,10 @@ last_modified datetime Last modificati
The ``subevent_before`` query parameter has been added. The ``subevent_before`` query parameter has been added.
.. versionchanged:: 3.14
The ``phone`` attribute has been added.
.. _order-position-resource: .. _order-position-resource:
@@ -372,6 +377,7 @@ List of all orders
"secret": "k24fiuwvu8kxz3y1", "secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/", "url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org", "email": "tester@example.org",
"phone": "+491234567",
"locale": "en", "locale": "en",
"sales_channel": "web", "sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z", "datetime": "2017-12-01T10:00:00Z",
@@ -539,6 +545,7 @@ Fetching individual orders
"secret": "k24fiuwvu8kxz3y1", "secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/", "url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org", "email": "tester@example.org",
"phone": "+491234567",
"locale": "en", "locale": "en",
"sales_channel": "web", "sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z", "datetime": "2017-12-01T10:00:00Z",
@@ -705,6 +712,8 @@ Updating order fields
* ``email`` * ``email``
* ``phone``
* ``checkin_attention`` * ``checkin_attention``
* ``locale`` * ``locale``

View File

@@ -14,7 +14,9 @@ Control panel views
------------------- -------------------
If you want to add a custom view to the control area of an event, just register an URL in your If you want to add a custom view to the control area of an event, just register an URL in your
``urls.py`` that lives in the ``/control/`` subpath:: ``urls.py`` that lives in the ``/control/`` subpath:
.. code-block:: python
from django.conf.urls import url from django.conf.urls import url
@@ -44,7 +46,9 @@ If only the ``organizer`` parameter is present, it will be ensured that:
* The user has permission to access view the current organizer * The user has permission to access view the current organizer
If you want to require specific permission types, we provide you with a decorator or a mixin for If you want to require specific permission types, we provide you with a decorator or a mixin for
your views:: your views:
.. code-block:: python
from pretix.control.permissions import ( from pretix.control.permissions import (
event_permission_required, EventPermissionRequiredMixin event_permission_required, EventPermissionRequiredMixin
@@ -61,8 +65,9 @@ your views::
... ...
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``. In case of Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``. In case of
event-related views, there is also a signal that allows you to add the view to the event navigation like this:: event-related views, there is also a signal that allows you to add the view to the event navigation like this:
.. code-block:: python
from django.urls import resolve, reverse from django.urls import resolve, reverse
from django.dispatch import receiver from django.dispatch import receiver
@@ -90,7 +95,9 @@ Event settings view
------------------- -------------------
A special case of a control panel view is a view hooked into the event settings page. For this case, there is a A special case of a control panel view is a view hooked into the event settings page. For this case, there is a
special navigation signal:: special navigation signal:
.. code-block:: python
@receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings') @receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings')
def navbar_settings(sender, request, **kwargs): def navbar_settings(sender, request, **kwargs):
@@ -105,7 +112,9 @@ special navigation signal::
}] }]
Also, your view should inherit from ``EventSettingsViewMixin`` and your template from ``pretixcontrol/event/settings_base.html`` Also, your view should inherit from ``EventSettingsViewMixin`` and your template from ``pretixcontrol/event/settings_base.html``
for good integration. If you just want to display a form, you could do it like the following:: for good integration. If you just want to display a form, you could do it like the following:
.. code-block:: python
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView): class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
model = Event model = Event
@@ -147,7 +156,9 @@ Including a custom view into the participant-facing frontend is a little bit dif
no path prefix like ``control/``. no path prefix like ``control/``.
First, define your URL in your ``urls.py``, but this time in the ``event_patterns`` section and wrapped by First, define your URL in your ``urls.py``, but this time in the ``event_patterns`` section and wrapped by
``event_url``:: ``event_url``:
.. code-block:: python
from pretix.multidomain import event_url from pretix.multidomain import event_url
@@ -182,8 +193,9 @@ standard Django request handling: There are `ViewSets`_ to group related views i
automatically build URL configurations from them. automatically build URL configurations from them.
To integrate a custom viewset with pretix' REST API, you can just register with one of our routers within the To integrate a custom viewset with pretix' REST API, you can just register with one of our routers within the
``urls.py`` module of your plugin:: ``urls.py`` module of your plugin:
.. code-block:: python
from pretix.api.urls import event_router, router, orga_router from pretix.api.urls import event_router, router, orga_router
@@ -200,7 +212,9 @@ in the control panel. However, you need to make sure on your own only to return
.event`` and ``request.organizer`` are available as usual. .event`` and ``request.organizer`` are available as usual.
To require a special permission like ``can_view_orders``, you do not need to inherit from a special ViewSet base To require a special permission like ``can_view_orders``, you do not need to inherit from a special ViewSet base
class, you can just set the ``permission`` attribute on your viewset:: class, you can just set the ``permission`` attribute on your viewset:
.. code-block:: python
class MyViewSet(ModelViewSet): class MyViewSet(ModelViewSet):
permission = 'can_view_orders' permission = 'can_view_orders'
@@ -208,8 +222,9 @@ class, you can just set the ``permission`` attribute on your viewset::
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
API authentications can be done via user sessions or API tokens and you should therefore check something like the API authentications can be done via user sessions or API tokens and you should therefore check something like the
following:: following:
.. code-block:: python
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user) perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'): if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'):

View File

@@ -15,7 +15,9 @@ Output registration
The email HTML renderer API does not make a lot of usage from signals, however, it The email HTML renderer API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available email renderers. Your plugin does use a signal to get a list of all available email renderers. Your plugin
should listen for this signal and return the subclass of ``pretix.base.email.BaseHTMLMailRenderer`` should listen for this signal and return the subclass of ``pretix.base.email.BaseHTMLMailRenderer``
that we'll provide in this plugin:: that we'll provide in this plugin:
.. code-block:: python
from django.dispatch import receiver from django.dispatch import receiver
@@ -72,7 +74,9 @@ class ``TemplateBasedMailRenderer`` that you can re-use to perform the following
* Call `inlinestyler`_ to convert all ``<style>`` style sheets to inline ``style=""`` * Call `inlinestyler`_ to convert all ``<style>`` style sheets to inline ``style=""``
attributes for better compatibility attributes for better compatibility
To use it, you just need to implement some variables:: To use it, you just need to implement some variables:
.. code-block:: python
class ClassicMailRenderer(TemplateBasedMailRenderer): class ClassicMailRenderer(TemplateBasedMailRenderer):
verbose_name = _('pretix default') verbose_name = _('pretix default')

View File

@@ -17,7 +17,9 @@ Exporter registration
The exporter API does not make a lot of usage from signals, however, it does use a signal to get a list of The exporter API does not make a lot of usage from signals, however, it does use a signal to get a list of
all available exporters. Your plugin should listen for this signal and return the subclass of all available exporters. Your plugin should listen for this signal and return the subclass of
``pretix.base.exporter.BaseExporter`` ``pretix.base.exporter.BaseExporter``
that we'll provide in this plugin:: that we'll provide in this plugin:
.. code-block:: python
from django.dispatch import receiver from django.dispatch import receiver
@@ -31,7 +33,9 @@ that we'll provide in this plugin::
Some exporters might also prove to be useful, when provided on an organizer-level. In order to declare your Some exporters might also prove to be useful, when provided on an organizer-level. In order to declare your
exporter as capable of providing exports spanning multiple events, your plugin should listen for this signal exporter as capable of providing exports spanning multiple events, your plugin should listen for this signal
and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin:: and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin:
.. code-block:: python
from django.dispatch import receiver from django.dispatch import receiver

View File

@@ -15,7 +15,9 @@ Output registration
The invoice renderer API does not make a lot of usage from signals, however, it The invoice renderer API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available invoice renderers. Your plugin does use a signal to get a list of all available invoice renderers. Your plugin
should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer`` should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer``
that we'll provide in this plugin:: that we'll provide in this plugin:
.. code-block:: python
from django.dispatch import receiver from django.dispatch import receiver

View File

@@ -19,7 +19,9 @@ Provider registration
The payment provider API does not make a lot of usage from signals, however, it The payment provider API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available payment providers. Your plugin does use a signal to get a list of all available payment providers. Your plugin
should listen for this signal and return the subclass of ``pretix.base.payment.BasePaymentProvider`` should listen for this signal and return the subclass of ``pretix.base.payment.BasePaymentProvider``
that the plugin will provide:: that the plugin will provide:
.. code-block:: python
from django.dispatch import receiver from django.dispatch import receiver
@@ -140,7 +142,9 @@ it is necessary to introduce additional views. One example is the PayPal
provider. It redirects the user to a PayPal website in the provider. It redirects the user to a PayPal website in the
:py:meth:`BasePaymentProvider.checkout_prepare` step of the checkout process :py:meth:`BasePaymentProvider.checkout_prepare` step of the checkout process
and provides PayPal with a URL to redirect back to. This URL points to a and provides PayPal with a URL to redirect back to. This URL points to a
view which looks roughly like this:: view which looks roughly like this:
.. code-block:: python
@login_required @login_required
def success(request): def success(request):

View File

@@ -13,7 +13,9 @@ Placeholder registration
The placeholder API does not make a lot of usage from signals, however, it The placeholder API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available email placeholders. Your plugin does use a signal to get a list of all available email placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:: should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:
.. code-block:: python
from django.dispatch import receiver from django.dispatch import receiver
@@ -71,7 +73,9 @@ Helper class for simple placeholders
------------------------------------ ------------------------------------
pretix ships with a helper class that makes it easy to provide placeholders based on simple pretix ships with a helper class that makes it easy to provide placeholders based on simple
functions:: functions:
.. code-block:: python
placeholder = SimpleFunctionalMailTextPlaceholder( placeholder = SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL' 'code', ['order'], lambda order: order.code, sample='F8VVL'

View File

@@ -55,7 +55,9 @@ restricted boolean (optional) ``False`` by default, restricts a plugin
compatibility string Specifier for compatible pretix versions. compatibility string Specifier for compatible pretix versions.
================== ==================== =========================================================== ================== ==================== ===========================================================
A working example would be:: A working example would be:
.. code-block:: python
try: try:
from pretix.base.plugins import PluginConfig from pretix.base.plugins import PluginConfig
@@ -81,7 +83,7 @@ A working example would be::
default_app_config = 'pretix_paypal.PaypalApp' default_app_config = 'pretix_paypal.PaypalApp'
The ``AppConfig`` class may implement a property ``compatiblity_errors``, that checks The ``AppConfig`` class may implement a property ``compatibility_errors``, that checks
whether the pretix installation meets all requirements of the plugin. If so, whether the pretix installation meets all requirements of the plugin. If so,
it should contain ``None`` or an empty list, otherwise a list of strings containing it should contain ``None`` or an empty list, otherwise a list of strings containing
human-readable error messages. We recommend using the ``django.utils.functional.cached_property`` human-readable error messages. We recommend using the ``django.utils.functional.cached_property``
@@ -96,7 +98,9 @@ Plugin registration
Somehow, pretix needs to know that your plugin exists at all. For this purpose, we Somehow, pretix needs to know that your plugin exists at all. For this purpose, we
make use of the `entry point`_ feature of setuptools. To register a plugin that lives make use of the `entry point`_ feature of setuptools. To register a plugin that lives
in a separate python package, your ``setup.py`` should contain something like this:: in a separate python package, your ``setup.py`` should contain something like this:
.. code-block:: python
setup( setup(
args..., args...,
@@ -118,7 +122,9 @@ The various components of pretix define a number of signals which your plugin ca
listen for. We will go into the details of the different signals in the following listen for. We will go into the details of the different signals in the following
pages. We suggest that you put your signal receivers into a ``signals`` submodule pages. We suggest that you put your signal receivers into a ``signals`` submodule
of your plugin. You should extend your ``AppConfig`` (see above) by the following of your plugin. You should extend your ``AppConfig`` (see above) by the following
method to make your receivers available:: method to make your receivers available:
.. code-block:: python
class PaypalApp(AppConfig): class PaypalApp(AppConfig):
@@ -127,7 +133,9 @@ method to make your receivers available::
from . import signals # NOQA from . import signals # NOQA
You can optionally specify code that is executed when your plugin is activated for an event You can optionally specify code that is executed when your plugin is activated for an event
in the ``installed`` method:: in the ``installed`` method:
.. code-block:: python
class PaypalApp(AppConfig): class PaypalApp(AppConfig):

View File

@@ -74,7 +74,7 @@ looks like this:
def generate_files(self) -> List[Tuple[str, str, str]]: def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'invoice-addresses.json', 'application/json', json.dumps({ yield 'invoice-addresses.json', 'application/json', json.dumps({
ia.order.code: InvoiceAdddressSerializer(ia).data ia.order.code: InvoiceAddressSerializer(ia).data
for ia in InvoiceAddress.objects.filter(order__event=self.event) for ia in InvoiceAddress.objects.filter(order__event=self.event)
}, indent=4) }, indent=4)

View File

@@ -17,7 +17,9 @@ Output registration
The ticket output API does not make a lot of usage from signals, however, it The ticket output API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available ticket outputs. Your plugin does use a signal to get a list of all available ticket outputs. Your plugin
should listen for this signal and return the subclass of ``pretix.base.ticketoutput.BaseTicketOutput`` should listen for this signal and return the subclass of ``pretix.base.ticketoutput.BaseTicketOutput``
that we'll provide in this plugin:: that we'll provide in this plugin:
.. code-block:: python
from django.dispatch import receiver from django.dispatch import receiver

View File

@@ -12,7 +12,9 @@ Implementing a task
------------------- -------------------
A common pattern for implementing asynchronous tasks can be seen a lot in ``pretix.base.services`` A common pattern for implementing asynchronous tasks can be seen a lot in ``pretix.base.services``
and looks like this:: and looks like this:
.. code-block:: python
from pretix.celery_app import app from pretix.celery_app import app
@@ -34,13 +36,15 @@ If your user needs to wait for the response of the asynchronous task, there are
that will probably move to ``pretix.base`` at some point. They consist of the view mixin ``AsyncAction`` that allows that will probably move to ``pretix.base`` at some point. They consist of the view mixin ``AsyncAction`` that allows
you to easily write a view that kicks off and waits for an asynchronous task. ``AsyncAction`` will determine whether you to easily write a view that kicks off and waits for an asynchronous task. ``AsyncAction`` will determine whether
to run the task asynchronously or not and will do some magic to look nice for users with and without JavaScript support. to run the task asynchronously or not and will do some magic to look nice for users with and without JavaScript support.
A usage example taken directly from the code is:: A usage example taken directly from the code is:
.. code-block:: python
class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View): class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
""" """
A view that executes a task asynchronously. A POST request will kick off the A view that executes a task asynchronously. A POST request will kick off the
task into the background or run it in the foreground if celery is not installed. task into the background or run it in the foreground if celery is not installed.
In the former case, subsequent GET calls can be used to determinine the current In the former case, subsequent GET calls can be used to determine the current
status of the task. status of the task.
""" """
@@ -79,7 +83,9 @@ A usage example taken directly from the code is::
return super().get_error_message(exception) return super().get_error_message(exception)
On the client side, this can be used by simply adding a ``data-asynctask`` attribute to an HTML form. This will enable On the client side, this can be used by simply adding a ``data-asynctask`` attribute to an HTML form. This will enable
AJAX sending of the form and display a loading indicator:: AJAX sending of the form and display a loading indicator:
.. code-block:: html
<form method="post" data-asynctask <form method="post" data-asynctask
action="{% eventurl request.event "presale:event.order.cancel.do" %}"> action="{% eventurl request.event "presale:event.order.cancel.do" %}">

View File

@@ -27,7 +27,9 @@ numbers and dates, ``LazyDate`` and ``LazyNumber``. There also is a ``LazyLocale
exceptions with gettext-localized exception messages. exceptions with gettext-localized exception messages.
Last, but definitely not least, we have the ``language`` context manager (``pretix.base.i18n.language``) that allows Last, but definitely not least, we have the ``language`` context manager (``pretix.base.i18n.language``) that allows
you to execute a piece of code with a different locale:: you to execute a piece of code with a different locale:
.. code-block:: python
with language('de'): with language('de'):
render_mail_template() render_mail_template()

View File

@@ -16,7 +16,9 @@ We recommend all relevant models to inherit from ``LoggedModel`` as it simplifie
.. autoclass:: pretix.base.models.LoggedModel .. autoclass:: pretix.base.models.LoggedModel
:members: log_action, all_logentries :members: log_action, all_logentries
To actually log an action, you can just call the ``log_action`` method on your object:: To actually log an action, you can just call the ``log_action`` method on your object:
.. code-block:: python
order.log_action('pretix.event.order.canceled', user=user, data={}) order.log_action('pretix.event.order.canceled', user=user, data={})
@@ -29,7 +31,9 @@ Logging form actions
"""""""""""""""""""" """"""""""""""""""""
A very common use case is to log the changes to a model that have been done in a ``ModelForm``. In this case, A very common use case is to log the changes to a model that have been done in a ``ModelForm``. In this case,
we generally use a custom ``form_valid`` method on our ``FormView`` that looks like this:: we generally use a custom ``form_valid`` method on our ``FormView`` that looks like this:
.. code-block:: python
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
@@ -40,7 +44,9 @@ we generally use a custom ``form_valid`` method on our ``FormView`` that looks l
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form) return super().form_valid(form)
It gets a little bit more complicated if your form allows file uploads:: It gets a little bit more complicated if your form allows file uploads:
.. code-block:: python
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
@@ -67,7 +73,9 @@ following ready-to-include template::
We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
implementation could look like:: implementation could look like:
.. code-block:: python
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from pretix.base.signals import logentry_display from pretix.base.signals import logentry_display
@@ -88,7 +96,9 @@ Sending notifications
If you think that the logged information might be important or urgent enough to send out a notification to interested If you think that the logged information might be important or urgent enough to send out a notification to interested
organizers. In this case, you should listen for the :py:attr:`pretix.base.signals.register_notification_types` signal organizers. In this case, you should listen for the :py:attr:`pretix.base.signals.register_notification_types` signal
to register a notification type:: to register a notification type:
.. code-block:: python
@receiver(register_notification_types) @receiver(register_notification_types)
def register_my_notification_types(sender, **kwargs): def register_my_notification_types(sender, **kwargs):
@@ -103,7 +113,9 @@ You should subclass the base ``NotificationType`` class and implement all its me
.. autoclass:: pretix.base.notifications.NotificationType .. autoclass:: pretix.base.notifications.NotificationType
:members: action_type, verbose_name, required_permission, build_notification :members: action_type, verbose_name, required_permission, build_notification
A simple implementation could look like this:: A simple implementation could look like this:
.. code-block:: python
class MyNotificationType(NotificationType): class MyNotificationType(NotificationType):
required_permission = "can_view_orders" required_permission = "can_view_orders"
@@ -143,7 +155,9 @@ Logging technical information
----------------------------- -----------------------------
If you just want to log technical information to a log file on disk that does not need to be parsed If you just want to log technical information to a log file on disk that does not need to be parsed
and displayed later, you can just use Python's ``logging`` module:: and displayed later, you can just use Python's ``logging`` module:
.. code-block:: python
import logging import logging
@@ -151,7 +165,9 @@ and displayed later, you can just use Python's ``logging`` module::
logger.info('Startup complete.') logger.info('Startup complete.')
This is also very useful to provide debugging information when an exception occurs:: This is also very useful to provide debugging information when an exception occurs:
.. code-block:: python
try: try:
foo() foo()

View File

@@ -15,7 +15,9 @@ Requiring permissions for a view
-------------------------------- --------------------------------
pretix provides a number of useful mixins and decorators that allow you to specify that a user needs a certain pretix provides a number of useful mixins and decorators that allow you to specify that a user needs a certain
permission level to access a view:: permission level to access a view:
.. code-block:: python
from pretix.control.permissions import ( from pretix.control.permissions import (
OrganizerPermissionRequiredMixin, organizer_permission_required OrganizerPermissionRequiredMixin, organizer_permission_required
@@ -44,7 +46,9 @@ permission level to access a view::
# Only users with *any* permission on this organizer can access this # Only users with *any* permission on this organizer can access this
Of course, the same is available on event level:: Of course, the same is available on event level:
.. code-block:: python
from pretix.control.permissions import ( from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required EventPermissionRequiredMixin, event_permission_required
@@ -73,7 +77,9 @@ Of course, the same is available on event level::
# Only users with *any* permission on this event can access this # Only users with *any* permission on this event can access this
You can also require that this view is only accessible by system administrators with an active "admin session" You can also require that this view is only accessible by system administrators with an active "admin session"
(see below for what this means):: (see below for what this means):
.. code-block:: python
from pretix.control.permissions import ( from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, administrator_permission_required AdministratorPermissionRequiredMixin, administrator_permission_required
@@ -89,7 +95,9 @@ You can also require that this view is only accessible by system administrators
# ... # ...
In rare cases it might also be useful to expose a feature only to people who have a staff account but do not In rare cases it might also be useful to expose a feature only to people who have a staff account but do not
necessarily have an active admin session:: necessarily have an active admin session:
.. code-block:: python
from pretix.control.permissions import ( from pretix.control.permissions import (
StaffMemberRequiredMixin, staff_member_required StaffMemberRequiredMixin, staff_member_required

View File

@@ -39,7 +39,9 @@ subclass that also adds support for internationalized fields:
.. autoclass:: pretix.base.forms.SettingsForm .. autoclass:: pretix.base.forms.SettingsForm
You can simply use it like this:: You can simply use it like this:
.. code-block:: python
class EventSettingsForm(SettingsForm): class EventSettingsForm(SettingsForm):
show_date_to = forms.BooleanField( show_date_to = forms.BooleanField(
@@ -56,7 +58,9 @@ You can simply use it like this::
Defaults in plugins Defaults in plugins
------------------- -------------------
Plugins can add custom hardcoded defaults in the following way:: Plugins can add custom hardcoded defaults in the following way:
.. code-block:: python
from pretix.base.settings import settings_hierarkey from pretix.base.settings import settings_hierarkey

View File

@@ -1 +1 @@
__version__ = "3.14.0.dev0" __version__ = "3.15.0.dev0"

View File

@@ -574,6 +574,7 @@ class EventSettingsSerializer(serializers.Serializer):
'presale_start_show_date', 'presale_start_show_date',
'locales', 'locales',
'locale', 'locale',
'region',
'last_order_modification_date', 'last_order_modification_date',
'show_quota_left', 'show_quota_left',
'waiting_list_enabled', 'waiting_list_enabled',
@@ -600,6 +601,9 @@ class EventSettingsSerializer(serializers.Serializer):
'attendee_data_explanation_text', 'attendee_data_explanation_text',
'confirm_texts', 'confirm_texts',
'order_email_asked_twice', 'order_email_asked_twice',
'order_phone_asked',
'order_phone_required',
'checkout_phone_helptext',
'payment_term_mode', 'payment_term_mode',
'payment_term_days', 'payment_term_days',
'payment_term_weekdays', 'payment_term_weekdays',

View File

@@ -180,7 +180,7 @@ class PdfDataSerializer(serializers.Field):
res = {} res = {}
ev = instance.subevent or instance.order.event ev = instance.subevent or instance.order.event
with language(instance.order.locale): with language(instance.order.locale, instance.order.event.settings.region):
# This needs to have some extra performance improvements to avoid creating hundreds of queries when # This needs to have some extra performance improvements to avoid creating hundreds of queries when
# we serialize a list. # we serialize a list.
@@ -361,7 +361,7 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = Order model = Order
fields = ( fields = (
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date', 'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads', 'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url' 'url'
@@ -393,7 +393,7 @@ class OrderSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
# Even though all fields that shouldn't be edited are marked as read_only in the serializer # Even though all fields that shouldn't be edited are marked as read_only in the serializer
# (hopefully), we'll be extra careful here and be explicit about the model fields we update. # (hopefully), we'll be extra careful here and be explicit about the model fields we update.
update_fields = ['comment', 'checkin_attention', 'email', 'locale'] update_fields = ['comment', 'checkin_attention', 'email', 'locale', 'phone']
if 'invoice_address' in validated_data: if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address') iadata = validated_data.pop('invoice_address')
@@ -691,7 +691,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = Order model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel', fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_email', 'simulate') 'force', 'send_email', 'simulate')

View File

@@ -1,7 +1,7 @@
from decimal import Decimal from decimal import Decimal
from django.db.models import Q from django.db.models import Q
from django.utils.translation import get_language, gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from hierarkey.proxy import HierarkeyProxy from hierarkey.proxy import HierarkeyProxy
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -9,6 +9,7 @@ from rest_framework.exceptions import ValidationError
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
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region
from pretix.base.models import ( from pretix.base.models import (
Device, GiftCard, GiftCardTransaction, Organizer, SeatingPlan, Team, Device, GiftCard, GiftCardTransaction, Organizer, SeatingPlan, Team,
TeamAPIToken, TeamInvite, User, TeamAPIToken, TeamInvite, User,
@@ -145,7 +146,7 @@ class TeamInviteSerializer(serializers.ModelSerializer):
}) })
}, },
event=None, event=None,
locale=get_language() # TODO: expose? locale=get_language_without_region() # TODO: expose?
) )
except SendMailException: except SendMailException:
pass # Already logged pass # Already logged
@@ -217,6 +218,7 @@ class OrganizerSettingsSerializer(serializers.Serializer):
'giftcard_length', 'giftcard_length',
'giftcard_expiry_years', 'giftcard_expiry_years',
'locales', 'locales',
'region',
'event_team_provisioning', 'event_team_provisioning',
'primary_color', 'primary_color',
'theme_color_success', 'theme_color_success',

View File

@@ -81,9 +81,9 @@ class ExportersMixin:
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs()) serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cf = CachedFile() cf = CachedFile(web_download=False)
cf.date = now() cf.date = now()
cf.expires = now() + timedelta(days=3) cf.expires = now() + timedelta(hours=24)
cf.save() cf.save()
d = serializer.data d = serializer.data
for k, v in d.items(): for k, v in d.items():

View File

@@ -582,7 +582,7 @@ class OrderViewSet(viewsets.ModelViewSet):
auth=request.auth, auth=request.auth,
) )
with language(order.locale): with language(order.locale, self.request.event.settings.region):
order_placed.send(self.request.event, order=order) order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID: if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order) order_paid.send(self.request.event, order=order)
@@ -674,6 +674,17 @@ class OrderViewSet(viewsets.ModelViewSet):
} }
) )
if 'phone' in self.request.data and serializer.instance.phone != self.request.data.get('phone'):
serializer.instance.log_action(
'pretix.event.order.phone.changed',
user=self.request.user,
auth=self.request.auth,
data={
'old_phone': serializer.instance.phone,
'new_phone': self.request.data.get('phone'),
}
)
if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'): if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'):
serializer.instance.log_action( serializer.instance.log_action(
'pretix.event.order.locale.changed', 'pretix.event.order.locale.changed',
@@ -886,7 +897,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
price = get_price(**kwargs) price = get_price(**kwargs)
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule) tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
with language(data.get('locale') or self.request.event.settings.locale): with language(data.get('locale') or self.request.event.settings.locale, self.request.event.settings.region):
return Response({ return Response({
'gross': price.gross, 'gross': price.gross,
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True), 'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),

View File

@@ -115,7 +115,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
'body': body_md, 'body': body_md,
'subject': str(subject), 'subject': str(subject),
'color': settings.PRETIX_PRIMARY_COLOR, 'color': settings.PRETIX_PRIMARY_COLOR,
'rtl': get_language() in settings.LANGUAGES_RTL 'rtl': get_language() in settings.LANGUAGES_RTL or get_language().split('-')[0] in settings.LANGUAGES_RTL,
} }
if self.event: if self.event:
htmlctx['event'] = self.event htmlctx['event'] = self.event

View File

@@ -139,7 +139,7 @@ class OrderListExporter(MultiSheetListExporter):
tax_rates = self._get_all_tax_rates(qs) tax_rates = self._get_all_tax_rates(qs)
headers = [ headers = [
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'), _('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'), _('Order date'),
_('Order time'), _('Company'), _('Name'), _('Order time'), _('Company'), _('Name'),
] ]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
@@ -215,6 +215,7 @@ class OrderListExporter(MultiSheetListExporter):
order.total, order.total,
order.get_status_display(), order.get_status_display(),
order.email, order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'), order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'), order.datetime.astimezone(tz).strftime('%H:%M:%S'),
] ]
@@ -303,6 +304,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Order code'), _('Order code'),
_('Status'), _('Status'),
_('Email'), _('Email'),
_('Phone number'),
_('Order date'), _('Order date'),
_('Order time'), _('Order time'),
_('Fee type'), _('Fee type'),
@@ -334,6 +336,7 @@ class OrderListExporter(MultiSheetListExporter):
order.code, order.code,
order.get_status_display(), order.get_status_display(),
order.email, order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'), order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'), order.datetime.astimezone(tz).strftime('%H:%M:%S'),
op.get_fee_type_display(), op.get_fee_type_display(),
@@ -402,6 +405,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Position ID'), _('Position ID'),
_('Status'), _('Status'),
_('Email'), _('Email'),
_('Phone number'),
_('Order date'), _('Order date'),
_('Order time'), _('Order time'),
] ]
@@ -481,6 +485,7 @@ class OrderListExporter(MultiSheetListExporter):
op.positionid, op.positionid,
order.get_status_display(), order.get_status_display(),
order.email, order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'), order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'), order.datetime.astimezone(tz).strftime('%H:%M:%S'),
] ]

View File

@@ -1,12 +1,17 @@
import hashlib
import ipaddress
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.password_validation import ( from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password, password_validators_help_texts, validate_password,
) )
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import User from pretix.base.models import User
from pretix.helpers.dicts import move_to_end from pretix.helpers.dicts import move_to_end
from pretix.helpers.http import get_client_ip
class LoginForm(forms.Form): class LoginForm(forms.Form):
@@ -18,6 +23,7 @@ class LoginForm(forms.Form):
error_messages = { error_messages = {
'invalid_login': _("This combination of credentials is not known to our system."), 'invalid_login': _("This combination of credentials is not known to our system."),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'inactive': _("This account is inactive.") 'inactive': _("This account is inactive.")
} }
@@ -39,10 +45,36 @@ class LoginForm(forms.Form):
else: else:
move_to_end(self.fields, 'keep_logged_in') move_to_end(self.fields, 'keep_logged_in')
@cached_property
def ratelimit_key(self):
if not settings.HAS_REDIS:
return None
client_ip = get_client_ip(self.request)
if not client_ip:
return None
try:
client_ip = ipaddress.ip_address(client_ip)
except ValueError:
# Web server not set up correctly
return None
if client_ip.is_private:
# This is the private IP of the server, web server not set up correctly
return None
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
def clean(self): def clean(self):
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required): if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
if self.ratelimit_key:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.get(self.ratelimit_key)
if cnt and int(cnt) > 10:
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data) self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
if self.user_cache is None: if self.user_cache is None:
if self.ratelimit_key:
rc.incr(self.ratelimit_key)
rc.expire(self.ratelimit_key, 300)
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['invalid_login'], self.error_messages['invalid_login'],
code='invalid_login' code='invalid_login'

View File

@@ -9,7 +9,6 @@ import pycountry
import pytz import pytz
import vat_moss.errors import vat_moss.errors
import vat_moss.id import vat_moss.id
from babel import localedata
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -20,22 +19,24 @@ 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
from django.utils.timezone import get_current_timezone from django.utils.timezone import get_current_timezone
from django.utils.translation import ( from django.utils.translation import gettext_lazy as _, pgettext_lazy
get_language, gettext_lazy as _, pgettext_lazy,
)
from django_countries import countries from django_countries import countries
from django_countries.fields import Country, CountryField from django_countries.fields import Country, CountryField
from phonenumber_field.formfields import PhoneNumberField from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber from phonenumber_field.phonenumber import PhoneNumber
from phonenumber_field.widgets import PhoneNumberPrefixWidget from phonenumber_field.widgets import (
from phonenumbers import NumberParseException PhoneNumberPrefixWidget, PhonePrefixSelect,
)
from phonenumbers import NumberParseException, national_significant_number
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
from pretix.base.forms.widgets import ( from pretix.base.forms.widgets import (
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget, BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
TimePickerWidget, UploadedFileWidget, TimePickerWidget, UploadedFileWidget,
) )
from pretix.base.i18n import language from pretix.base.i18n import (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.models import InvoiceAddress, Question, QuestionOption from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import ( from pretix.base.models.tax import (
EU_COUNTRIES, cc_to_vat_prefix, is_eu_country, EU_COUNTRIES, cc_to_vat_prefix, is_eu_country,
@@ -204,7 +205,18 @@ class NamePartsFormField(forms.MultiValueField):
return value return value
class WrappedPhonePrefixSelect(PhonePrefixSelect):
def __init__(self, *args, **kwargs):
with language(get_babel_locale()):
super().__init__(*args, **kwargs)
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget): class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def __init__(self, attrs=None, initial=None):
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput())
super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs)
def render(self, name, value, attrs=None, renderer=None): def render(self, name, value, attrs=None, renderer=None):
output = super().render(name, value, attrs, renderer) output = super().render(name, value, attrs, renderer)
return mark_safe(self.format_output(output)) return mark_safe(self.format_output(output))
@@ -212,12 +224,44 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def format_output(self, rendered_widgets) -> str: def format_output(self, rendered_widgets) -> str:
return '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets) return '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets)
def decompress(self, value):
"""
If an incomplete phone number (e.g. without country prefix) is currently entered,
the default implementation just discards the value and shows nothing at all.
Let's rather show something invalid, so the user is prompted to fix it, instead of
silently deleting data.
"""
if value:
if type(value) == PhoneNumber:
if value.country_code and value.national_number:
return [
"+%d" % value.country_code,
national_significant_number(value),
]
return [
None,
str(value)
]
elif "." in value:
return value.split(".")
else:
return [None, value]
return [None, ""]
def value_from_datadict(self, data, files, name):
# In contrast to defualt implementation, do not silently fail if a number without
# country prefix is entered
values = super(PhoneNumberPrefixWidget, self).value_from_datadict(data, files, name)
if values[1]:
return "%s.%s" % tuple(values)
return ""
def guess_country(event): def guess_country(event):
# Try to guess the initial country from either the country of the merchant # Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :) # or the locale. This will hopefully save at least some users some scrolling :)
locale = get_language() locale = get_language_without_region()
country = event.settings.invoice_address_from_country country = event.settings.region or event.settings.invoice_address_from_country
if not country: if not country:
valid_countries = countries.countries valid_countries = countries.countries
if '-' in locale: if '-' in locale:
@@ -532,13 +576,7 @@ class BaseQuestionsForm(forms.Form):
if q.valid_datetime_max: if q.valid_datetime_max:
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max)) field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
elif q.type == Question.TYPE_PHONENUMBER: elif q.type == Question.TYPE_PHONENUMBER:
babel_locale = 'en' with language(get_babel_locale()):
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
if localedata.exists(get_language()):
babel_locale = get_language()
elif localedata.exists(get_language()[:2]):
babel_locale = get_language()[:2]
with language(babel_locale):
default_country = guess_country(event) default_country = guess_country(event)
default_prefix = None default_prefix = None
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items(): for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():

View File

@@ -1,4 +1,5 @@
from django import forms from django import forms
from django.conf import settings
from django.contrib.auth.hashers import check_password from django.contrib.auth.hashers import check_password
from django.contrib.auth.password_validation import ( from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password, password_validators_help_texts, validate_password,
@@ -19,6 +20,7 @@ class UserSettingsForm(forms.ModelForm):
"address or password."), "address 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."),
} }
old_pw = forms.CharField(max_length=255, old_pw = forms.CharField(max_length=255,
@@ -64,6 +66,18 @@ class UserSettingsForm(forms.ModelForm):
def clean_old_pw(self): def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw') old_pw = self.cleaned_data.get('old_pw')
if old_pw and settings.HAS_REDIS:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
if cnt > 10:
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if old_pw and not check_password(old_pw, self.user.password): if old_pw and not check_password(old_pw, self.user.password):
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['pw_current_wrong'], self.error_messages['pw_current_wrong'],

View File

@@ -1,5 +1,6 @@
from contextlib import contextmanager from contextlib import contextmanager
from babel import localedata
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import translation
from django.utils.formats import date_format, number_format from django.utils.formats import date_format, number_format
@@ -66,10 +67,52 @@ class LazyNumber:
return number_format(self.value, decimal_pos=self.decimal_pos) return number_format(self.value, decimal_pos=self.decimal_pos)
ALLOWED_LANGUAGES = dict(settings.LANGUAGES)
def get_babel_locale():
babel_locale = 'en'
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
if localedata.exists(translation.get_language()):
babel_locale = translation.get_language()
elif localedata.exists(translation.get_language()[:2]):
babel_locale = translation.get_language()[:2]
return babel_locale
def get_language_without_region(lng=None):
"""
Returns the currently active language, but strips what pretix calls a ``region``. For example,
if the currently active language is ``en-us``, you will be returned ``en`` since pretix does not
ship with separate language files for ``en-us``. If the currently active language is ``pt-br``,
you will be returned ``pt-br`` since there are separate language files for ``pt-br``.
tl;dr: You will be always passed a language that is defined in settings.LANGUAGES.
"""
lng = lng or translation.get_language() or settings.LANGUAGE_CODE
if lng not in ALLOWED_LANGUAGES:
lng = lng.split('-')[0]
if lng not in ALLOWED_LANGUAGES:
lng = settings.LANGUAGE_CODE
return lng
@contextmanager @contextmanager
def language(lng): def language(lng, region=None):
"""
Temporarily change the active language to ``lng``. Will automatically be rolled back when the
context manager returns.
You can optionally pass a "region". For example, if you pass ``en`` as ``lng`` and ``US`` as
``region``, the active language will be ``en-us``, which will mostly affect date/time
formatting. If you pass a ``lng`` that already contains a region, e.g. ``pt-br``, the ``region``
attribute will be ignored.
"""
_lng = translation.get_language() _lng = translation.get_language()
translation.activate(lng or settings.LANGUAGE_CODE) lng = lng or settings.LANGUAGE_CODE
if '-' not in lng and region:
lng += '-' + region.lower()
translation.activate(lng)
try: try:
yield yield
finally: finally:

View File

@@ -144,7 +144,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
def _upper(self, val): def _upper(self, val):
# We uppercase labels, but not in every language # We uppercase labels, but not in every language
if get_language() == 'el': if get_language().startswith('el'):
return val return val
return val.upper() return val.upper()

View File

@@ -15,7 +15,8 @@ from django.utils.translation.trans_real import (
parse_accept_lang_header, parse_accept_lang_header,
) )
from pretix.base.settings import GlobalSettingsObject from pretix.base.i18n import get_language_without_region
from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import ( from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain, get_event_domain, get_organizer_domain,
) )
@@ -35,19 +36,30 @@ class LocaleMiddleware(MiddlewareMixin):
# Normally, this middleware runs *before* the event is set. However, on event frontend pages it # Normally, this middleware runs *before* the event is set. However, on event frontend pages it
# might be run a second time by pretix.presale.EventMiddleware and in this case the event is already # might be run a second time by pretix.presale.EventMiddleware and in this case the event is already
# set and can be taken into account for the decision. # set and can be taken into account for the decision.
if hasattr(request, 'event') and not request.path.startswith(get_script_prefix() + 'control'): if not request.path.startswith(get_script_prefix() + 'control'):
if language not in request.event.settings.locales: if hasattr(request, 'event'):
firstpart = language.split('-')[0] if language not in request.event.settings.locales:
if firstpart in request.event.settings.locales: firstpart = language.split('-')[0]
language = firstpart if firstpart in request.event.settings.locales:
else: language = firstpart
language = request.event.settings.locale else:
for lang in request.event.settings.locales: language = request.event.settings.locale
if lang.startswith(firstpart + '-'): for lang in request.event.settings.locales:
language = lang if lang.startswith(firstpart + '-'):
break language = lang
break
if '-' not in language and request.event.settings.region:
language += '-' + request.event.settings.region
elif hasattr(request, 'organizer'):
if '-' not in language and request.organizer.settings.region:
language += '-' + request.organizer.settings.region
else:
gs = global_settings_object(request)
if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region
translation.activate(language) translation.activate(language)
request.LANGUAGE_CODE = translation.get_language() request.LANGUAGE_CODE = get_language_without_region()
tzname = None tzname = None
if hasattr(request, 'event'): if hasattr(request, 'event'):
@@ -192,7 +204,7 @@ class SecurityMiddleware(MiddlewareMixin):
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"' resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
img_src = [] img_src = []
gs = GlobalSettingsObject() gs = global_settings_object(request)
if gs.settings.leaflet_tiles: if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*")) img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
@@ -216,6 +228,8 @@ class SecurityMiddleware(MiddlewareMixin):
h['report-uri'] = ["/csp_report/"] h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp: if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy'])) _merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
if settings.CSP_ADDITIONAL_HEADER:
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
staticdomain = "'self'" staticdomain = "'self'"
dynamicdomain = "'self'" dynamicdomain = "'self'"

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.0.11 on 2020-12-18 18:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0162_remove_seat_name'),
]
operations = [
migrations.AddField(
model_name='cachedfile',
name='session_key',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='cachedfile',
name='web_download',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 3.0.11 on 2020-12-11 16:48
import json
import phonenumber_field.modelfields
from django.db import migrations
import pretix.base.models.fields
def migrate_settings(apps, schema_editor):
Order = apps.get_model('pretixbase', 'Order')
Event = apps.get_model('pretixbase', 'Event')
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
Event_SettingsStore.objects.filter(key='telephone_field_required').update(key='order_phone_required')
Event_SettingsStore.objects.filter(key='telephone_field_help_text').update(key='checkout_phone_helptext')
for e in Event.objects.filter(plugins__icontains="pretix_telephone"):
plugins = e.plugins.split(",")
plugins.remove("pretix_telephone")
e.plugins = ",".join(plugins)
e.save()
Event_SettingsStore.objects.create(object=e, key='order_phone_asked', value='True')
for o in Order.objects.filter(meta_info__icontains='"telephone"'):
mi = json.loads(o.meta_info)
if 'telephone' in mi.get('contact_form_data', {}):
mi['phone'] = mi['contact_form_data'].pop('telephone')
o.phone = mi['phone']
o.meta_info = json.dumps(mi)
o.save(update_fields=['meta_info', 'phone'])
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0172_event_sales_channels'),
]
operations = [
migrations.AddField(
model_name='order',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None),
),
migrations.AlterField(
model_name='event',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=['web']),
),
migrations.RunPython(
migrate_settings, migrations.RunPython.noop,
)
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.0.11 on 2020-12-22 10:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0173_auto_20201211_1648'),
('pretixbase', '0162b_auto_20201218_1810'),
]
operations = [
]

View File

@@ -28,6 +28,8 @@ class CachedFile(models.Model):
filename = models.CharField(max_length=255) filename = models.CharField(max_length=255)
type = models.CharField(max_length=255) type = models.CharField(max_length=255)
file = models.FileField(null=True, blank=True, upload_to=cachedfile_name, max_length=255) file = models.FileField(null=True, blank=True, upload_to=cachedfile_name, max_length=255)
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
session_key = models.TextField(null=True, blank=True) # only allow download in this session
@receiver(post_delete, sender=CachedFile) @receiver(post_delete, sender=CachedFile)

View File

@@ -528,7 +528,7 @@ class Event(EventMixin, LoggedModel):
return locking.LockManager(self) return locking.LockManager(self)
def get_mail_backend(self, force_custom=False): def get_mail_backend(self, timeout=None, force_custom=False):
""" """
Returns an email server connection, either by using the system-wide connection Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the event's settings. or by returning a custom one based on the event's settings.
@@ -542,7 +542,7 @@ class Event(EventMixin, LoggedModel):
password=self.settings.smtp_password, password=self.settings.smtp_password,
use_tls=self.settings.smtp_use_tls, use_tls=self.settings.smtp_use_tls,
use_ssl=self.settings.smtp_use_ssl, use_ssl=self.settings.smtp_use_ssl,
fail_silently=False) fail_silently=False, timeout=timeout)
else: else:
return get_connection(fail_silently=False) return get_connection(fail_silently=False)

View File

@@ -31,6 +31,7 @@ from django_countries.fields import Country
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField from jsonfallback.fields import FallbackJSONField
from phonenumber_field.modelfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber from phonenumber_field.phonenumber import PhoneNumber
from phonenumbers import NumberParseException from phonenumbers import NumberParseException
@@ -86,6 +87,8 @@ class Order(LockModel, LoggedModel):
:type event: Event :type event: Event
:param email: The email of the person who ordered this :param email: The email of the person who ordered this
:type email: str :type email: str
:param phone: The phone number of the person who ordered this
:type phone: str
:param testmode: Whether this is a test mode order :param testmode: Whether this is a test mode order
:type testmode: bool :type testmode: bool
:param locale: The locale of this order :param locale: The locale of this order
@@ -144,6 +147,10 @@ class Order(LockModel, LoggedModel):
null=True, blank=True, null=True, blank=True,
verbose_name=_('E-mail') verbose_name=_('E-mail')
) )
phone = PhoneNumberField(
null=True, blank=True,
verbose_name=_('Phone number'),
)
locale = models.CharField( locale = models.CharField(
null=True, blank=True, max_length=32, null=True, blank=True, max_length=32,
verbose_name=_('Locale') verbose_name=_('Locale')
@@ -326,6 +333,9 @@ class Order(LockModel, LoggedModel):
payment_sum=payment_sum_sq, payment_sum=payment_sum_sq,
refund_sum=refund_sum_sq, refund_sum=refund_sum_sq,
) )
qs = qs.annotate(
computed_payment_refund_sum=Coalesce(payment_sum_sq, 0) - Coalesce(refund_sum_sq, 0),
)
qs = qs.annotate( qs = qs.annotate(
pending_sum_t=F('total') - Coalesce(payment_sum_sq, 0) + Coalesce(refund_sum_sq, 0), pending_sum_t=F('total') - Coalesce(payment_sum_sq, 0) + Coalesce(refund_sum_sq, 0),
@@ -857,7 +867,7 @@ class Order(LockModel, LoggedModel):
for k, v in self.event.meta_data.items(): for k, v in self.event.meta_data.items():
context['meta_' + k] = v context['meta_' + k] = v
with language(self.locale): with language(self.locale, self.event.settings.region):
recipient = self.email recipient = self.email
if position and position.attendee_email: if position and position.attendee_email:
recipient = position.attendee_email recipient = position.attendee_email
@@ -890,7 +900,7 @@ class Order(LockModel, LoggedModel):
) )
def resend_link(self, user=None, auth=None): def resend_link(self, user=None, auth=None):
with language(self.locale): with language(self.locale, self.event.settings.region):
email_template = self.event.settings.mail_text_resend_link email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.event, order=self) email_context = get_email_context(event=self.event, order=self)
email_subject = _('Your order: %(code)s') % {'code': self.code} email_subject = _('Your order: %(code)s') % {'code': self.code}
@@ -902,7 +912,7 @@ class Order(LockModel, LoggedModel):
@property @property
def positions_with_tickets(self): def positions_with_tickets(self):
for op in self.positions.all(): for op in self.positions.select_related('item'):
if not op.generate_ticket: if not op.generate_ticket:
continue continue
yield op yield op
@@ -1155,7 +1165,7 @@ class AbstractPosition(models.Model):
(2) questions: a list of Question objects, extended by an 'answer' property (2) questions: a list of Question objects, extended by an 'answer' property
""" """
self.answ = {} self.answ = {}
for a in self.answers.all(): for a in getattr(self, 'answerlist', self.answers.all()): # use prefetch_related cache from get_cart
self.answ[a.question_id] = a self.answ[a.question_id] = a
# We need to clone our question objects, otherwise we will override the cached # We need to clone our question objects, otherwise we will override the cached
@@ -1514,7 +1524,7 @@ class OrderPayment(models.Model):
def _send_paid_mail_attendee(self, position, user): def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
with language(self.order.locale): with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid_attendee email_template = self.order.event.settings.mail_text_order_paid_attendee
email_context = get_email_context(event=self.order.event, order=self.order, position=position) email_context = get_email_context(event=self.order.event, order=self.order, position=position)
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code} email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
@@ -1532,7 +1542,7 @@ class OrderPayment(models.Model):
def _send_paid_mail(self, invoice, user, mail_text): def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
with language(self.order.locale): with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid email_template = self.order.event.settings.mail_text_order_paid
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text) email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code} email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
@@ -2104,7 +2114,7 @@ class OrderPosition(AbstractPosition):
for k, v in self.event.meta_data.items(): for k, v in self.event.meta_data.items():
context['meta_' + k] = v context['meta_' + k] = v
with language(self.order.locale): with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email recipient = self.attendee_email
try: try:
email_content = render_mail(template, context) email_content = render_mail(template, context)
@@ -2132,7 +2142,7 @@ class OrderPosition(AbstractPosition):
def resend_link(self, user=None, auth=None): def resend_link(self, user=None, auth=None):
with language(self.order.locale): with language(self.order.locale, self.order.event.settings.region):
email_template = self.event.settings.mail_text_resend_link email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.order.event, order=self.order, position=self) email_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = _('Your event registration: %(code)s') % {'code': self.order.code} email_subject = _('Your event registration: %(code)s') % {'code': self.order.code}

View File

@@ -125,7 +125,7 @@ class WaitingListEntry(LoggedModel):
self.voucher = v self.voucher = v
self.save() self.save()
with language(self.locale): with language(self.locale, self.event.settings.region):
mail( mail(
self.email, self.email,
_('You have been selected from the waitinglist for {event}').format(event=str(self.event)), _('You have been selected from the waitinglist for {event}').format(event=str(self.event)),

View File

@@ -39,6 +39,7 @@ from pretix.base.models import Order, OrderPosition
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_text_variables from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -229,6 +230,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Random City"), "editor_sample": _("Random City"),
"evaluate": lambda op, order, ev: str(ev.location) "evaluate": lambda op, order, ev: str(ev.location)
}), }),
("telephone", {
"label": _("Phone number"),
"editor_sample": "+01 1234 567890",
"evaluate": lambda op, order, ev: phone_format(order.phone)
}),
("invoice_name", { ("invoice_name", {
"label": _("Invoice address name"), "label": _("Invoice address name"),
"editor_sample": _("John Doe"), "editor_sample": _("John Doe"),
@@ -421,6 +427,7 @@ class Renderer:
self.layout = layout self.layout = layout
self.background_file = background_file self.background_file = background_file
self.variables = get_variables(event) self.variables = get_variables(event)
self.event = event
if self.background_file: if self.background_file:
self.bg_bytes = self.background_file.read() self.bg_bytes = self.background_file.read()
self.bg_pdf = PdfFileReader(BytesIO(self.bg_bytes), strict=False) self.bg_pdf = PdfFileReader(BytesIO(self.bg_bytes), strict=False)
@@ -487,7 +494,7 @@ class Renderer:
def _get_text_content(self, op: OrderPosition, order: Order, o: dict, inner=False): def _get_text_content(self, op: OrderPosition, order: Order, o: dict, inner=False):
if o.get('locale', None) and not inner: if o.get('locale', None) and not inner:
with language(o['locale']): with language(o['locale'], self.event.settings.region):
return self._get_text_content(op, order, o, True) return self._get_text_content(op, order, o, True)
ev = self._get_ev(op, order) ev = self._get_ev(op, order)

View File

@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent): def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
with language(wle.locale): with language(wle.locale, wle.event.settings.region):
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event) email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
try: try:
mail( mail(
@@ -41,7 +41,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent, def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent,
refund_amount: Decimal, user: User, positions: list): refund_amount: Decimal, user: User, positions: list):
with language(order.locale): with language(order.locale, order.event.settings.region):
try: try:
ia = order.invoice_address ia = order.invoice_address
except InvoiceAddress.DoesNotExist: except InvoiceAddress.DoesNotExist:

View File

@@ -32,7 +32,7 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
) )
file = CachedFile.objects.get(id=fileid) file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale), override(event.settings.timezone): with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
responses = register_data_exporters.send(event) responses = register_data_exporters.send(event)
for receiver, response in responses: for receiver, response in responses:
ex = response(event, set_progress) ex = response(event, set_progress)
@@ -67,15 +67,18 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
if user: if user:
locale = user.locale locale = user.locale
timezone = user.timezone timezone = user.timezone
region = None # todo: add to user?
else: else:
e = allowed_events.first() e = allowed_events.first()
if e: if e:
locale = e.settings.locale locale = e.settings.locale
timezone = e.settings.timezone timezone = e.settings.timezone
region = e.settings.region
else: else:
locale = settings.LANGUAGE_CODE locale = settings.LANGUAGE_CODE
timezone = settings.TIME_ZONE timezone = settings.TIME_ZONE
with language(locale), override(timezone): region = None
with language(locale, region), override(timezone):
if isinstance(form_data['events'][0], str): if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer) events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else: else:

View File

@@ -43,7 +43,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
lp = invoice.order.payments.last() lp = invoice.order.payments.last()
with language(invoice.locale): with language(invoice.locale, invoice.event.settings.region):
invoice.invoice_from = invoice.event.settings.get('invoice_address_from') invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name') invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode') invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
@@ -244,7 +244,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.date = timezone.now().date() cancellation.date = timezone.now().date()
cancellation.payment_provider_text = '' cancellation.payment_provider_text = ''
cancellation.file = None cancellation.file = None
with language(invoice.locale): with language(invoice.locale, invoice.event.settings.region):
cancellation.invoice_from = invoice.event.settings.get('invoice_address_from') cancellation.invoice_from = invoice.event.settings.get('invoice_address_from')
cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name') cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
cancellation.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode') cancellation.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
@@ -297,7 +297,7 @@ def invoice_pdf_task(invoice: int):
return None return None
if i.file: if i.file:
i.file.delete() i.file.delete()
with language(i.locale): with language(i.locale, i.event.settings.region):
fname, ftype, fcontent = i.event.invoice_renderer.generate(i) fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
i.file.save(fname, ContentFile(fcontent)) i.file.save(fname, ContentFile(fcontent))
i.save() i.save()
@@ -328,7 +328,7 @@ def build_preview_invoice_pdf(event):
if not locale or locale == '__user__': if not locale or locale == '__user__':
locale = event.settings.locale locale = event.settings.locale
with rolledback_transaction(), language(locale): with rolledback_transaction(), language(locale, event.settings.region):
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(), order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count()) expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
invoice = Invoice( invoice = Invoice(

View File

@@ -290,7 +290,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
except Order.DoesNotExist: except Order.DoesNotExist:
order = None order = None
else: else:
with language(order.locale): with language(order.locale, event.settings.region):
if position: if position:
try: try:
position = order.positions.get(pk=position) position = order.positions.get(pk=position)

View File

@@ -65,7 +65,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
# TODO: quotacheck? # TODO: quotacheck?
cf = CachedFile.objects.get(id=fileid) cf = CachedFile.objects.get(id=fileid)
user = User.objects.get(pk=user) user = User.objects.get(pk=user)
with language(locale): with language(locale, event.settings.region):
cols = get_all_columns(event) cols = get_all_columns(event)
parsed = parse_csv(cf.file) parsed = parse_csv(cf.file)
orders = [] orders = []
@@ -163,7 +163,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
) )
for o in orders: for o in orders:
with language(o.locale): with language(o.locale, event.settings.region):
order_placed.send(event, order=o) order_placed.send(event, order=o)
if o.status == Order.STATUS_PAID: if o.status == Order.STATUS_PAID:
order_paid.send(event, order=o) order_paid.send(event, order=o)

View File

@@ -23,7 +23,9 @@ from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication from pretix.api.models import OAuthApplication
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import LazyLocaleException, language from pretix.base.i18n import (
LazyLocaleException, get_language_without_region, language,
)
from pretix.base.models import ( from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Order, CartPosition, Device, Event, GiftCard, Item, ItemVariation, Order,
OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
@@ -260,7 +262,7 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
# send_mail will trigger PDF generation later # send_mail will trigger PDF generation later
if send_mail: if send_mail:
with language(order.locale): with language(order.locale, order.event.settings.region):
if order.total == Decimal('0.00'): if order.total == Decimal('0.00'):
email_template = order.event.settings.mail_text_order_approved_free email_template = order.event.settings.mail_text_order_approved_free
email_subject = _('Order approved and confirmed: %(code)s') % {'code': order.code} email_subject = _('Order approved and confirmed: %(code)s') % {'code': order.code}
@@ -311,7 +313,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
if send_mail: if send_mail:
email_template = order.event.settings.mail_text_order_denied email_template = order.event.settings.mail_text_order_denied
email_context = get_email_context(event=order.event, order=order, comment=comment) email_context = get_email_context(event=order.event, order=order, comment=comment)
with language(order.locale): with language(order.locale, order.event.settings.region):
email_subject = _('Order denied: %(code)s') % {'code': order.code} email_subject = _('Order denied: %(code)s') % {'code': order.code}
try: try:
order.send_mail( order.send_mail(
@@ -422,7 +424,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if send_mail: if send_mail:
email_template = order.event.settings.mail_text_order_canceled email_template = order.event.settings.mail_text_order_canceled
with language(order.locale): with language(order.locale, order.event.settings.region):
email_context = get_email_context(event=order.event, order=order) email_context = get_email_context(event=order.event, order=order)
email_subject = _('Order canceled: %(code)s') % {'code': order.code} email_subject = _('Order canceled: %(code)s') % {'code': order.code}
try: try:
@@ -776,8 +778,9 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
status=Order.STATUS_PENDING, status=Order.STATUS_PENDING,
event=event, event=event,
email=email, email=email,
phone=(meta_info or {}).get('contact_form_data', {}).get('phone'),
datetime=now_dt, datetime=now_dt,
locale=locale, locale=get_language_without_region(locale),
total=total, total=total,
testmode=True if sales_channel.testmode_supported and event.testmode else False, testmode=True if sales_channel.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}), meta_info=json.dumps(meta_info or {}),
@@ -1033,7 +1036,7 @@ def send_expiry_warnings(sender, **kwargs):
# Race condition # Race condition
continue continue
with language(o.locale): with language(o.locale, settings.region):
o.expiry_reminder_sent = True o.expiry_reminder_sent = True
o.save(update_fields=['expiry_reminder_sent']) o.save(update_fields=['expiry_reminder_sent'])
email_template = settings.mail_text_order_expire_warning email_template = settings.mail_text_order_expire_warning
@@ -1110,7 +1113,7 @@ def send_download_reminders(sender, **kwargs):
if not send: if not send:
continue continue
with language(o.locale): with language(o.locale, o.event.settings.region):
o.download_reminder_sent = True o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent']) o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder email_template = event.settings.mail_text_download_reminder
@@ -1150,7 +1153,7 @@ def send_download_reminders(sender, **kwargs):
def notify_user_changed_order(order, user=None, auth=None, invoices=[]): def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
with language(order.locale): with language(order.locale, order.event.settings.region):
email_template = order.event.settings.mail_text_order_changed email_template = order.event.settings.mail_text_order_changed
email_context = get_email_context(event=order.event, order=order) email_context = get_email_context(event=order.event, order=order)
email_subject = _('Your order has been changed: %(code)s') % {'code': order.code} email_subject = _('Your order has been changed: %(code)s') % {'code': order.code}

View File

@@ -113,10 +113,11 @@ class QuotaAvailability:
raise e raise e
def _write_cache(self, quotas, now_dt): def _write_cache(self, quotas, now_dt):
events = {q.event for q in quotas} # We used to also delete item_quota_cache:* from the event cache here, but as the cache
# gets more complex, this does not seem worth it. The cache is only present for up to
# 5 seconds to prevent high peaks, and a 5-second delay in availability is usually
# tolerable
update = [] update = []
for e in events:
e.cache.delete('item_quota_cache')
for q in quotas: for q in quotas:
rewrite_cache = self._count_waitinglist and ( rewrite_cache = self._count_waitinglist and (
not q.cache_is_hot(now_dt) or self.results[q][0] > q.cached_availability_state not q.cache_is_hot(now_dt) or self.results[q][0] > q.cached_availability_state

View File

@@ -17,7 +17,7 @@ from pretix.celery_app import app
@app.task(base=ProfiledEventTask) @app.task(base=ProfiledEventTask)
def export(event: Event, shredders: List[str]) -> None: def export(event: Event, shredders: List[str], session_key=None) -> None:
known_shredders = event.get_data_shredders() known_shredders = event.get_data_shredders()
with NamedTemporaryFile() as rawfile: with NamedTemporaryFile() as rawfile:
@@ -55,6 +55,8 @@ def export(event: Event, shredders: List[str]) -> None:
cf.date = now() cf.date = now()
cf.filename = event.slug + '.zip' cf.filename = event.slug + '.zip'
cf.type = 'application/zip' cf.type = 'application/zip'
cf.session_key = session_key
cf.web_download = True
cf.expires = now() + timedelta(hours=1) cf.expires = now() + timedelta(hours=1)
cf.save() cf.save()
cf.file.save(cachedfile_name(cf, cf.filename), rawfile) cf.file.save(cachedfile_name(cf, cf.filename), rawfile)

View File

@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
def generate_orderposition(order_position: int, provider: str): def generate_orderposition(order_position: int, provider: str):
order_position = OrderPosition.objects.select_related('order', 'order__event').get(id=order_position) order_position = OrderPosition.objects.select_related('order', 'order__event').get(id=order_position)
with language(order_position.order.locale): with language(order_position.order.locale, order_position.order.event.settings.region):
responses = register_ticket_outputs.send(order_position.order.event) responses = register_ticket_outputs.send(order_position.order.event)
for receiver, response in responses: for receiver, response in responses:
prov = response(order_position.order.event) prov = response(order_position.order.event)
@@ -41,7 +41,7 @@ def generate_orderposition(order_position: int, provider: str):
def generate_order(order: int, provider: str): def generate_order(order: int, provider: str):
order = Order.objects.select_related('event').get(id=order) order = Order.objects.select_related('event').get(id=order)
with language(order.locale): with language(order.locale, order.event.settings.region):
responses = register_ticket_outputs.send(order.event) responses = register_ticket_outputs.send(order.event)
for receiver, response in responses: for receiver, response in responses:
prov = response(order.event) prov = response(order.event)
@@ -75,7 +75,7 @@ class DummyRollbackException(Exception):
def preview(event: int, provider: str): def preview(event: int, provider: str):
event = Event.objects.get(id=event) event = Event.objects.get(id=event)
with rolledback_transaction(), language(event.settings.locale): with rolledback_transaction(), language(event.settings.locale, event.settings.region):
item = event.items.create(name=_("Sample product"), default_price=42.23, item = event.items.create(name=_("Sample product"), default_price=42.23,
description=_("Sample product description")) description=_("Sample product description"))
item2 = event.items.create(name=_("Sample workshop"), default_price=23.40) item2 = event.items.create(name=_("Sample workshop"), default_price=23.40)

View File

@@ -193,6 +193,25 @@ DEFAULTS = {
help_text=_("Require customers to fill in the primary email address twice to avoid errors."), help_text=_("Require customers to fill in the primary email address twice to avoid errors."),
) )
}, },
'order_phone_asked': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for a phone number per order"),
)
},
'order_phone_required': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require a phone number per order"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-order_phone_asked'}),
)
},
'invoice_address_asked': { 'invoice_address_asked': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool,
@@ -832,6 +851,20 @@ DEFAULTS = {
label=_("Default language"), label=_("Default language"),
) )
}, },
'region': {
'default': None,
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
'serializer_kwargs': lambda: dict(**country_choice_kwargs()),
'form_kwargs': lambda: dict(
label=_('Region'),
help_text=_('Will be used to determine date and time formatting as well as default country for customer '
'addresses and phone numbers. For formatting, this takes less priority than the language and '
'is therefore mostly relevant for languages used in different regions globally (like English).'),
**country_choice_kwargs()
),
},
'show_dates_on_frontpage': { 'show_dates_on_frontpage': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool,
@@ -1845,6 +1878,17 @@ Your {event} team"""))
"why you need information from them.") "why you need information from them.")
) )
}, },
'checkout_phone_helptext': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Help text of the phone number field"),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
)
},
'checkout_email_helptext': { 'checkout_email_helptext': {
'default': LazyI18nString.from_gettext(gettext_noop( 'default': LazyI18nString.from_gettext(gettext_noop(
'Make sure to enter a valid email address. We will send you an order ' 'Make sure to enter a valid email address. We will send you an order '
@@ -2403,3 +2447,9 @@ def validate_organizer_settings(organizer, settings_dict):
# #
# N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered. # N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered.
pass pass
def global_settings_object(holder):
if not hasattr(holder, '_global_settings_object'):
holder._global_settings_object = GlobalSettingsObject()
return holder._global_settings_object

View File

@@ -20,6 +20,7 @@ from pretix.base.models import (
) )
from pretix.base.services.invoices import invoice_pdf_task from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.signals import register_data_shredders from pretix.base.signals import register_data_shredders
from pretix.helpers.json import CustomJSONEncoder
class ShredError(LazyLocaleException): class ShredError(LazyLocaleException):
@@ -121,6 +122,31 @@ def shred_log_fields(logentry, banlist=None, whitelist=None):
logentry.save(update_fields=['data', 'shredded']) logentry.save(update_fields=['data', 'shredded'])
class PhoneNumberShredder(BaseDataShredder):
verbose_name = _('Phone numbers')
identifier = 'phone_numbers'
description = _('This will remove all phone numbers from orders.')
def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'phone-by-order.json', 'application/json', json.dumps({
o.code: o.phone for o in self.event.orders.filter(phone__isnull=False)
}, cls=CustomJSONEncoder, indent=4)
@transaction.atomic
def shred_data(self):
for o in self.event.orders.all():
o.phone = None
d = o.meta_info_data
if d:
if 'contact_form_data' in d and 'phone' in d['contact_form_data']:
del d['contact_form_data']['phone']
o.meta_info = json.dumps(d)
o.save(update_fields=['meta_info', 'phone'])
for le in self.event.logentry_set.filter(action_type="pretix.event.order.phone.changed"):
shred_log_fields(le, banlist=['old_phone', 'new_phone'])
class EmailAddressShredder(BaseDataShredder): class EmailAddressShredder(BaseDataShredder):
verbose_name = _('E-mails') verbose_name = _('E-mails')
identifier = 'order_emails' identifier = 'order_emails'
@@ -372,9 +398,10 @@ class PaymentInfoShredder(BaseDataShredder):
@receiver(register_data_shredders, dispatch_uid="shredders_builtin") @receiver(register_data_shredders, dispatch_uid="shredders_builtin")
def register_payment_provider(sender, **kwargs): def register_core_shredders(sender, **kwargs):
return [ return [
EmailAddressShredder, EmailAddressShredder,
PhoneNumberShredder,
AttendeeInfoShredder, AttendeeInfoShredder,
InvoiceAddressShredder, InvoiceAddressShredder,
QuestionAnswerShredder, QuestionAnswerShredder,

View File

@@ -0,0 +1,22 @@
from django import template
from phonenumber_field.phonenumber import PhoneNumber
from phonenumbers import NumberParseException
register = template.Library()
@register.filter("phone_format")
def phone_format(value: str):
if not value:
return ""
if isinstance(value, str):
try:
return PhoneNumber.from_string(value).as_international
except NumberParseException:
return value
if isinstance(value, PhoneNumber) and value.national_number:
return value.as_international
return str(value)

View File

@@ -1,3 +1,4 @@
import re
import urllib.parse import urllib.parse
import bleach import bleach
@@ -71,6 +72,10 @@ EMAIL_RE = build_email_re(tlds=sorted(tld_set, key=len, reverse=True))
def safelink_callback(attrs, new=False): def safelink_callback(attrs, new=False):
"""
Makes sure that all links to a different domain are passed through a redirection handler
to ensure there's no passing of referers with secrets inside them.
"""
url = attrs.get((None, 'href'), '/') url = attrs.get((None, 'href'), '/')
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'): if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect') signer = signing.Signer(salt='safe-redirect')
@@ -80,7 +85,42 @@ def safelink_callback(attrs, new=False):
return attrs return attrs
def truelink_callback(attrs, new=False):
"""
Tries to prevent "phishing" attacks in which a link looks like it points to a safe place but instead
points somewhere else, e.g.
<a href="https://evilsite.com">https://google.com</a>
At the same time, custom texts are still allowed:
<a href="https://maps.google.com">Get to the event</a>
Suffixes are also allowed:
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
"""
text = re.sub('[^a-zA-Z0-9.-/_]', '', attrs.get('_text')) # clean up link text
if URL_RE.match(text):
# link text looks like a url
if text.startswith('//'):
text = 'https:' + text
elif not text.startswith('http'):
text = 'https://' + text
text_url = urllib.parse.urlparse(text)
href_url = urllib.parse.urlparse(attrs[None, 'href'])
if text_url.netloc != href_url.netloc or not href_url.path.startswith(href_url.path):
# link text contains an URL that has a different base than the actual URL
attrs['_text'] = attrs[None, 'href']
return attrs
def abslink_callback(attrs, new=False): def abslink_callback(attrs, new=False):
"""
Makes sure that all links will be absolute links and will be opened in a new page with no
window.opener attribute.
"""
url = attrs.get((None, 'href'), '/') url = attrs.get((None, 'href'), '/')
if not url.startswith('mailto:') and not url.startswith('tel:'): if not url.startswith('mailto:') and not url.startswith('tel:'):
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, url) attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, url)
@@ -93,6 +133,7 @@ def markdown_compile_email(source):
linker = bleach.Linker( linker = bleach.Linker(
url_re=URL_RE, url_re=URL_RE,
email_re=EMAIL_RE, email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True parse_email=True
) )
return linker.linkify(bleach.clean( return linker.linkify(bleach.clean(
@@ -145,7 +186,7 @@ def rich_text(text: str, **kwargs):
linker = bleach.Linker( linker = bleach.Linker(
url_re=URL_RE, url_re=URL_RE,
email_re=EMAIL_RE, email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]), callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
parse_email=True parse_email=True
) )
body_md = linker.linkify(markdown_compile(text)) body_md = linker.linkify(markdown_compile(text))
@@ -161,7 +202,7 @@ def rich_text_snippet(text: str, **kwargs):
linker = bleach.Linker( linker = bleach.Linker(
url_re=URL_RE, url_re=URL_RE,
email_re=EMAIL_RE, email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]), callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
parse_email=True parse_email=True
) )
body_md = linker.linkify(markdown_compile(text, snippet=True)) body_md = linker.linkify(markdown_compile(text, snippet=True))

View File

@@ -13,7 +13,11 @@ class DownloadView(TemplateView):
@cached_property @cached_property
def object(self) -> CachedFile: def object(self) -> CachedFile:
try: try:
return get_object_or_404(CachedFile, id=self.kwargs['id']) o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
if o.session_key:
if o.session_key != self.request.session.session_key:
raise Http404()
return o
except ValueError: # Invalid URLs except ValueError: # Invalid URLs
raise Http404() raise Http404()

View File

@@ -203,6 +203,7 @@ class CachedFileField(ExtFileField):
cf = CachedFile.objects.create( cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1), expires=now() + datetime.timedelta(days=1),
date=now(), date=now(),
web_download=True,
filename=data.name, filename=data.name,
type=data.content_type, type=data.content_type,
) )
@@ -218,6 +219,7 @@ class CachedFileField(ExtFileField):
if isinstance(data, File): if isinstance(data, File):
cf = CachedFile.objects.create( cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1), expires=now() + datetime.timedelta(days=1),
web_download=True,
date=now(), date=now(),
filename=data.name, filename=data.name,
type=data.content_type, type=data.content_type,

View File

@@ -19,9 +19,7 @@ from pytz import common_timezones, timezone
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
from pretix.base.forms import ( from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
I18nModelForm, PlaceholderValidator, SettingsForm,
)
from pretix.base.models import Event, Organizer, TaxRule, Team from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventMetaValue, SubEvent from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
@@ -459,6 +457,7 @@ class EventSettingsForm(SettingsForm):
'presale_start_show_date', 'presale_start_show_date',
'locales', 'locales',
'locale', 'locale',
'region',
'show_quota_left', 'show_quota_left',
'waiting_list_enabled', 'waiting_list_enabled',
'waiting_list_hours', 'waiting_list_hours',
@@ -482,6 +481,9 @@ class EventSettingsForm(SettingsForm):
'attendee_addresses_asked', 'attendee_addresses_asked',
'attendee_addresses_required', 'attendee_addresses_required',
'attendee_data_explanation_text', 'attendee_data_explanation_text',
'order_phone_asked',
'order_phone_required',
'checkout_phone_helptext',
'banner_text', 'banner_text',
'banner_text_bottom', 'banner_text_bottom',
'order_email_asked_twice', 'order_email_asked_twice',
@@ -499,6 +501,31 @@ class EventSettingsForm(SettingsForm):
data = super().clean() data = super().clean()
settings_dict = self.event.settings.freeze() settings_dict = self.event.settings.freeze()
settings_dict.update(data) settings_dict.update(data)
# set all dependants of virtual_keys and
# delete all virtual_fields to prevent them from being saved
for virtual_key in self.virtual_keys:
if virtual_key not in data:
continue
base_key = virtual_key.rsplit('_', 2)[0]
asked_key = base_key + '_asked'
required_key = base_key + '_required'
if data[virtual_key] == 'optional':
data[asked_key] = True
data[required_key] = False
elif data[virtual_key] == 'required':
data[asked_key] = True
data[required_key] = True
# Explicitly check for 'do_not_ask'.
# Do not overwrite as default-behaviour when no value for virtual field is transmitted!
elif data[virtual_key] == 'do_not_ask':
data[asked_key] = False
data[required_key] = False
# hierarkey.forms cannot handle non-existent keys in cleaned_data => do not delete, but set to None
data[virtual_key] = None
validate_event_settings(self.event, data) validate_event_settings(self.event, data)
return data return data
@@ -526,6 +553,39 @@ class EventSettingsForm(SettingsForm):
(a, {"title": a, "data": v}) for a, v in get_fonts().items() (a, {"title": a, "data": v}) for a, v in get_fonts().items()
] ]
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
self.virtual_keys = []
for asked_key in [key for key in self.fields.keys() if key.endswith('_asked')]:
required_key = asked_key.rsplit('_', 1)[0] + '_required'
virtual_key = asked_key + '_required'
if required_key not in self.fields or virtual_key in self.fields:
# either no matching required key or
# there already is a field with virtual_key defined manually, so do not overwrite
continue
asked_field = self.fields[asked_key]
self.fields[virtual_key] = forms.ChoiceField(
label=asked_field.label,
help_text=asked_field.help_text,
required=True,
widget=forms.RadioSelect,
choices=[
# default key needs a value other than '' because with '' it would also overwrite even if combi-field is not transmitted
('do_not_ask', _('Do not ask')),
('optional', _('Ask, but do not require input')),
('required', _('Ask and require input'))
]
)
self.virtual_keys.append(virtual_key)
if self.initial[required_key]:
self.initial[virtual_key] = 'required'
elif self.initial[asked_key]:
self.initial[virtual_key] = 'optional'
else:
self.initial[virtual_key] = 'do_not_ask'
class CancelSettingsForm(SettingsForm): class CancelSettingsForm(SettingsForm):
auto_fields = [ auto_fields = [

View File

@@ -150,8 +150,8 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')), (Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
)), )),
(_('Cancellations'), ( (_('Cancellations'), (
(Order.STATUS_CANCELED, _('Canceled')), (Order.STATUS_CANCELED, _('Canceled (fully)')),
('cp', _('Canceled (or with paid fee)')), ('cp', _('Canceled (fully or with paid fee)')),
('rc', _('Cancellation requested')), ('rc', _('Cancellation requested')),
)), )),
(_('Payment process'), ( (_('Payment process'), (
@@ -159,7 +159,8 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')), (Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
('o', _('Pending (overdue)')), ('o', _('Pending (overdue)')),
('overpaid', _('Overpaid')), ('overpaid', _('Overpaid')),
('underpaid', _('Underpaid')), ('partially_paid', _('Partially paid')),
('underpaid', _('Underpaid (but confirmed)')),
('pendingpaid', _('Pending (but fully paid)')), ('pendingpaid', _('Pending (but fully paid)')),
)), )),
(_('Approval process'), ( (_('Approval process'), (
@@ -245,6 +246,14 @@ class OrderFilterForm(FilterForm):
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0) Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0)
& Q(require_approval=False) & Q(require_approval=False)
) )
elif s == 'partially_paid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
computed_payment_refund_sum__lt=F('total'),
computed_payment_refund_sum__gt=Decimal('0.00')
).exclude(
status=Order.STATUS_CANCELED
)
elif s == 'underpaid': elif s == 'underpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True) qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter( qs = qs.filter(

View File

@@ -10,11 +10,15 @@ from pretix.base.signals import register_global_settings
class GlobalSettingsForm(SettingsForm): class GlobalSettingsForm(SettingsForm):
auto_fields = [
'region'
]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.obj = GlobalSettingsObject() self.obj = GlobalSettingsObject()
super().__init__(*args, obj=self.obj, **kwargs) super().__init__(*args, obj=self.obj, **kwargs)
self.fields = OrderedDict([ self.fields = OrderedDict(list(self.fields.items()) + [
('footer_text', I18nFormField( ('footer_text', I18nFormField(
widget=I18nTextInput, widget=I18nTextInput,
required=False, required=False,

View File

@@ -16,6 +16,7 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.questions import WrappedPhoneNumberPrefixWidget
from pretix.base.forms.widgets import ( from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget, DatePickerWidget, SplitDateTimePickerWidget,
) )
@@ -460,7 +461,15 @@ class OrderContactForm(forms.ModelForm):
class Meta: class Meta:
model = Order model = Order
fields = ['email', 'email_known_to_work'] fields = ['email', 'email_known_to_work', 'phone']
widgets = {
'phone': WrappedPhoneNumberPrefixWidget()
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.event.settings.order_phone_asked and not self.instance.phone:
del self.fields['phone']
class OrderLocaleForm(forms.ModelForm): class OrderLocaleForm(forms.ModelForm):

View File

@@ -223,6 +223,7 @@ class OrganizerSettingsForm(SettingsForm):
'giftcard_length', 'giftcard_length',
'giftcard_expiry_years', 'giftcard_expiry_years',
'locales', 'locales',
'region',
'event_team_provisioning', 'event_team_provisioning',
'primary_color', 'primary_color',
'theme_color_success', 'theme_color_success',

View File

@@ -292,6 +292,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.denied': _('The order has been denied.'), 'pretix.event.order.denied': _('The order has been denied.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" ' 'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'), 'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _('The email address has been confirmed to be working (the user clicked on a link '
'in the email for the first time).'),
'pretix.event.order.phone.changed': _('The phone number has been changed from "{old_phone}" '
'to "{new_phone}".'),
'pretix.event.order.locale.changed': _('The order locale has been changed.'), 'pretix.event.order.locale.changed': _('The order locale has been changed.'),
'pretix.event.order.invoice.generated': _('The invoice has been generated.'), 'pretix.event.order.invoice.generated': _('The invoice has been generated.'),
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'), 'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),

View File

@@ -18,7 +18,7 @@
<script type="text/javascript" src="{% url 'javascript-catalog' lang=request.LANGUAGE_CODE %}" <script type="text/javascript" src="{% url 'javascript-catalog' lang=request.LANGUAGE_CODE %}"
defer></script> defer></script>
{% else %} {% else %}
<script src="{% statici18n LANGUAGE_CODE %}" async></script> <script src="{% statici18n request.LANGUAGE_CODE %}" async></script>
{% endif %} {% endif %}
{% compress js %} {% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script> <script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>

View File

@@ -83,22 +83,61 @@
{% bootstrap_field sform.locales layout="control" %} {% bootstrap_field sform.locales layout="control" %}
{% bootstrap_field sform.locale layout="control" %} {% bootstrap_field sform.locale layout="control" %}
{% bootstrap_field sform.timezone layout="control" %} {% bootstrap_field sform.timezone layout="control" %}
{% bootstrap_field sform.region layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Attendee data" %}</legend> <legend>{% trans "Customer and attendee data" %}</legend>
{% bootstrap_field sform.attendee_names_asked layout="control" %} <h4>{% trans "Customer data (once per order)" %}</h4>
{% bootstrap_field sform.attendee_names_required layout="control" %} <div class="form-group">
<label class="control-label col-md-3">
{% trans "E-mail" %}
</label>
<div class="col-md-9">
<div class="checkbox">
<label><input type="checkbox" checked="checked" disabled="disabled"> {% trans "Ask and require input" %}</label>
</div>
</div>
</div>
{% bootstrap_field sform.order_email_asked_twice layout="control" %}
{% bootstrap_field sform.order_phone_asked_required layout="control" %}
<div class="form-group">
<label class="control-label col-md-3">
{% trans "Name and address" %}
</label>
<div class="col-md-9 static-form-row">
<p>
<a href="{% url "control:event.settings.invoice" event=request.event.slug organizer=request.organizer.slug %}#tab-0-1-open" target="_blank">
{% trans "See invoice settings" %}
</a>
</p>
</div>
</div>
<h4>{% trans "Attendee data (once per admission ticket)" %}</h4>
{% bootstrap_field sform.attendee_names_asked_required layout="control" %}
{% bootstrap_field sform.attendee_emails_asked_required layout="control" %}
{% bootstrap_field sform.attendee_company_asked_required layout="control" %}
{% bootstrap_field sform.attendee_addresses_asked_required layout="control" %}
<div class="form-group">
<label class="control-label col-md-3">
{% trans "Custom fields" %}
</label>
<div class="col-md-9 static-form-row">
<p>
<a href="{% url "control:event.items.questions" event=request.event.slug organizer=request.organizer.slug %}" target="_blank">
{% trans "Manage questions" %}
</a>
</p>
</div>
</div>
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
<h4>{% trans "Other settings" %}</h4>
{% bootstrap_field sform.name_scheme layout="control" %} {% bootstrap_field sform.name_scheme layout="control" %}
{% bootstrap_field sform.name_scheme_titles layout="control" %} {% bootstrap_field sform.name_scheme_titles layout="control" %}
{% bootstrap_field sform.order_email_asked_twice layout="control" %}
{% bootstrap_field sform.attendee_emails_asked layout="control" %}
{% bootstrap_field sform.attendee_emails_required layout="control" %}
{% bootstrap_field sform.attendee_company_asked layout="control" %}
{% bootstrap_field sform.attendee_company_required layout="control" %}
{% bootstrap_field sform.attendee_addresses_asked layout="control" %}
{% bootstrap_field sform.attendee_addresses_required layout="control" %}
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %} {% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Texts" %}</legend> <legend>{% trans "Texts" %}</legend>
@@ -177,6 +216,7 @@
</div> </div>
{% bootstrap_field sform.checkout_email_helptext layout="control" %} {% bootstrap_field sform.checkout_email_helptext layout="control" %}
{% bootstrap_field sform.checkout_phone_helptext layout="control" %}
{% bootstrap_field sform.banner_text layout="control" %} {% bootstrap_field sform.banner_text layout="control" %}
{% bootstrap_field sform.banner_text_bottom layout="control" %} {% bootstrap_field sform.banner_text_bottom layout="control" %}
</fieldset> </fieldset>

View File

@@ -6,6 +6,7 @@
{% load rich_text %} {% load rich_text %}
{% load safelink %} {% load safelink %}
{% load eventsignal %} {% load eventsignal %}
{% load phone_format %}
{% block title %} {% block title %}
{% blocktrans trimmed with code=order.code %} {% blocktrans trimmed with code=order.code %}
Order details: {{ code }} Order details: {{ code }}
@@ -201,6 +202,15 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</dd> </dd>
{% if order.phone or request.event.settings.order_phone_asked %}
<dt>{% trans "Phone number" %}</dt>
<dd>
{{ order.phone|default_if_none:""|phone_format }}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
</dd>
{% endif %}
{% if invoices %} {% if invoices %}
<dt>{% trans "Invoices" %}</dt> <dt>{% trans "Invoices" %}</dt>
<dd> <dd>
@@ -560,6 +570,26 @@
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
{% if order.status != "c" and order.total != payment_refund_sum %}
<div class="row-fluid product-row cart-row">
<div class="col-md-4 col-xs-6">
{% trans "Successful payments" %}
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
{{ payment_refund_sum|money:event.currency }}
</div>
<div class="clearfix"></div>
</div>
<div class="row-fluid product-row total text-danger">
<div class="col-md-4 col-xs-6">
<strong>{% trans "Pending total" %}</strong>
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
<strong>{{ order.pending_sum|money:event.currency }}</strong>
</div>
<div class="clearfix"></div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% eventsignal event "pretix.control.signals.order_info" order=order request=request %} {% eventsignal event "pretix.control.signals.order_info" order=order request=request %}

View File

@@ -113,7 +113,7 @@
<a href="?{% url_replace request 'ordering' '-datetime' %}"><i class="fa fa-caret-down"></i></a> <a href="?{% url_replace request 'ordering' '-datetime' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'datetime' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'ordering' 'datetime' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
<th class="text-right flip">{% trans "Order total" %} <th class="text-right flip">{% trans "Order paid / total" %}
<a href="?{% url_replace request 'ordering' '-total' %}"><i class="fa fa-caret-down"></i></a> <a href="?{% url_replace request 'ordering' '-total' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a></th> <a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a></th>
<th class="text-right flip">{% trans "Positions" %}</th> <th class="text-right flip">{% trans "Positions" %}</th>
@@ -141,7 +141,11 @@
<br>{{ o.invoice_address.name }} <br>{{ o.invoice_address.name }}
{% endif %} {% endif %}
</td> </td>
<td>{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}</td> <td>
<span class="fa fa-{{ o.sales_channel_obj.icon }} text-muted"
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
</td>
<td class="text-right flip"> <td class="text-right flip">
{% if o.has_cancellation_request %} {% if o.has_cancellation_request %}
<span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span> <span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span>
@@ -158,6 +162,13 @@
{% elif o.is_pending_with_full_payment %} {% elif o.is_pending_with_full_payment %}
<span class="label label-danger">{% trans "FULLY PAID" %}</span> <span class="label label-danger">{% trans "FULLY PAID" %}</span>
{% endif %} {% endif %}
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
<span class="text-muted">
{% endif %}
{{ o.computed_payment_refund_sum|money:request.event.currency }} /
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
</span>
{% endif %}
{{ o.total|money:request.event.currency }} {{ o.total|money:request.event.currency }}
</td> </td>
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td> <td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>

View File

@@ -45,6 +45,7 @@
<fieldset> <fieldset>
<legend>{% trans "Localization" %}</legend> <legend>{% trans "Localization" %}</legend>
{% bootstrap_field sform.locales layout="control" %} {% bootstrap_field sform.locales layout="control" %}
{% bootstrap_field sform.region layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Shop design" %}</legend> <legend>{% trans "Shop design" %}</legend>

View File

@@ -32,7 +32,7 @@
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label> <label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" name="url" <input type="text" name="url"
value="{% abseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code }}&subevent={{ voucher.subevent_id }}" value="{% abseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code }}{% if voucher.subevent_id %}&subevent={{ voucher.subevent_id }}{% endif %}"
class="form-control" class="form-control"
id="id_url" readonly> id="id_url" readonly>
</div> </div>

View File

@@ -1,4 +1,5 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from django.views.generic.base import RedirectView
from pretix.control.views import ( from pretix.control.views import (
auth, checkin, dashboards, event, geo, global_settings, item, main, oauth, auth, checkin, dashboards, event, geo, global_settings, item, main, oauth,
@@ -303,4 +304,5 @@ urlpatterns = [
url(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(), url(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(),
name='event.orders.checkinlists.delete'), name='event.orders.checkinlists.delete'),
])), ])),
url(r'^event/(?P<organizer>[^/]+)/$', RedirectView.as_view(pattern_name='control:organizer'), name='event.organizerredirect'),
] ]

View File

@@ -74,13 +74,15 @@ def login(request):
backend = [b for b in backends if b.visible][0] backend = [b for b in backends if b.visible][0]
if request.user.is_authenticated: if request.user.is_authenticated:
next_url = backend.get_next_url(request) or 'control:index' next_url = backend.get_next_url(request) or 'control:index'
return redirect(next_url) if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
return redirect(next_url)
return redirect(reverse('control:index'))
if request.method == 'POST': if request.method == 'POST':
form = LoginForm(backend=backend, data=request.POST) form = LoginForm(backend=backend, data=request.POST, request=request)
if form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier: if form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier:
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False)) return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
else: else:
form = LoginForm(backend=backend) form = LoginForm(backend=backend, request=request)
ctx['form'] = form ctx['form'] = form
ctx['can_register'] = settings.PRETIX_REGISTRATION ctx['can_register'] = settings.PRETIX_REGISTRATION
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET

View File

@@ -19,7 +19,6 @@ from django.http import (
) )
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils import translation
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
@@ -55,6 +54,7 @@ from pretix.multidomain.urlreverse import get_event_domain
from pretix.plugins.stripe.payment import StripeSettingsHolder from pretix.plugins.stripe.payment import StripeSettingsHolder
from pretix.presale.style import regenerate_css from pretix.presale.style import regenerate_css
from ...base.i18n import language
from ...base.models.items import ItemMetaProperty from ...base.models.items import ItemMetaProperty
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
from ..logdisplay import OVERVIEW_BANLIST from ..logdisplay import OVERVIEW_BANLIST
@@ -594,7 +594,7 @@ class MailSettings(EventSettingsViewMixin, EventSettingsFormView):
) )
if request.POST.get('test', '0').strip() == '1': if request.POST.get('test', '0').strip() == '1':
backend = self.request.event.get_mail_backend(force_custom=True) backend = self.request.event.get_mail_backend(force_custom=True, timeout=10)
try: try:
backend.test(self.request.event.settings.mail_from) backend.test(self.request.event.settings.mail_from)
except Exception as e: except Exception as e:
@@ -659,7 +659,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
if matched is not None: if matched is not None:
idx = matched.group('idx') idx = matched.group('idx')
if idx in self.supported_locale: if idx in self.supported_locale:
with translation.override(self.supported_locale[idx]): with language(self.supported_locale[idx], self.request.event.settings.region):
msgs[self.supported_locale[idx]] = markdown_compile_email( msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item)) v.format_map(self.placeholders(preview_item))
) )

View File

@@ -71,7 +71,7 @@ class GeoCodeView(LoginRequiredMixin, View):
try: try:
r = requests.get( r = requests.get(
'http://www.mapquestapi.com/geocoding/v1/address?location={}&key={}'.format( 'https://www.mapquestapi.com/geocoding/v1/address?location={}&key={}'.format(
quote(q), gs.settings.mapquest_apikey quote(q), gs.settings.mapquest_apikey
) )
) )

View File

@@ -153,17 +153,18 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
annotated = { annotated = {
o['pk']: o o['pk']: o
for o in for o in
Order.annotate_overpayments(Order.objects).filter( Order.annotate_overpayments(Order.objects, sums=True).filter(
pk__in=[o.pk for o in ctx['orders']] pk__in=[o.pk for o in ctx['orders']]
).annotate( ).annotate(
pcnt=Subquery(s, output_field=IntegerField()), pcnt=Subquery(s, output_field=IntegerField()),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk'))) has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
).values( ).values(
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund', 'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
'has_pending_refund', 'has_cancellation_request' 'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum'
) )
} }
scs = get_all_sales_channels()
for o in ctx['orders']: for o in ctx['orders']:
if o.pk not in annotated: if o.pk not in annotated:
continue continue
@@ -174,6 +175,8 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
o.has_external_refund = annotated.get(o.pk)['has_external_refund'] o.has_external_refund = annotated.get(o.pk)['has_external_refund']
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund'] o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request'] o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
o.sales_channel_obj = scs[o.sales_channel]
if ctx['page_obj'].paginator.count < 1000: if ctx['page_obj'].paginator.count < 1000:
# Performance safeguard: Only count positions if the data set is small # Performance safeguard: Only count positions if the data set is small
@@ -262,6 +265,8 @@ class OrderDetail(OrderView):
ctx['overpaid'] = self.order.pending_sum * -1 ctx['overpaid'] = self.order.pending_sum * -1
ctx['sales_channel'] = get_all_sales_channels().get(self.order.sales_channel) ctx['sales_channel'] = get_all_sales_channels().get(self.order.sales_channel)
ctx['download_buttons'] = self.download_buttons ctx['download_buttons'] = self.download_buttons
ctx['payment_refund_sum'] = self.order.payment_refund_sum
ctx['pending_sum'] = self.order.pending_sum
return ctx return ctx
@cached_property @cached_property
@@ -666,7 +671,7 @@ class OrderCancellationRequestDelete(OrderView):
}, user=self.request.user) }, user=self.request.user)
messages.success(self.request, _('The request has been removed. If you want, you can now inform the user.')) messages.success(self.request, _('The request has been removed. If you want, you can now inform the user.'))
with language(self.order.locale): with language(self.order.locale, self.request.event.settings.region):
return redirect(reverse('control:event.order.sendmail', kwargs={ return redirect(reverse('control:event.order.sendmail', kwargs={
'event': self.request.event.slug, 'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
@@ -937,7 +942,7 @@ class OrderRefundView(OrderView):
if giftcard_value and self.order.email: if giftcard_value and self.order.email:
messages.success(self.request, _('A new gift card was created. You can now send the user their ' messages.success(self.request, _('A new gift card was created. You can now send the user their '
'gift card code.')) 'gift card code.'))
with language(self.order.locale): with language(self.order.locale, self.request.event.settings.region):
return redirect(reverse('control:event.order.sendmail', kwargs={ return redirect(reverse('control:event.order.sendmail', kwargs={
'event': self.request.event.slug, 'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
@@ -1663,6 +1668,7 @@ class OrderContactChange(OrderView):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
old_email = self.order.email old_email = self.order.email
old_phone = self.order.phone
changed = False changed = False
if self.form.is_valid(): if self.form.is_valid():
new_email = self.form.cleaned_data['email'] new_email = self.form.cleaned_data['email']
@@ -1677,6 +1683,18 @@ class OrderContactChange(OrderView):
user=self.request.user, user=self.request.user,
) )
new_phone = self.form.cleaned_data.get('phone')
if new_phone != old_phone:
changed = True
self.order.log_action(
'pretix.event.order.phone.changed',
data={
'old_phone': old_phone,
'new_phone': self.form.cleaned_data['phone'],
},
user=self.request.user,
)
if self.form.cleaned_data['regenerate_secrets']: if self.form.cleaned_data['regenerate_secrets']:
changed = True changed = True
self.order.secret = generate_secret() self.order.secret = generate_secret()
@@ -1779,7 +1797,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
code=self.kwargs['code'].upper() code=self.kwargs['code'].upper()
) )
self.preview_output = {} self.preview_output = {}
with language(order.locale): with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order) email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message']) email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context)) email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
@@ -1842,7 +1860,7 @@ class OrderPositionSendMail(OrderSendMail):
attendee_email__isnull=False attendee_email__isnull=False
) )
self.preview_output = {} self.preview_output = {}
with language(position.order.locale): with language(position.order.locale, self.request.event.settings.region):
email_context = get_email_context(event=position.order.event, order=position.order, position=position) email_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message']) email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context)) email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
@@ -2046,9 +2064,9 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
messages.error(self.request, _('There was a problem processing your input. See below for error details.')) messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
cf = CachedFile() cf = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now() cf.date = now()
cf.expires = now() + timedelta(days=3) cf.expires = now() + timedelta(hours=24)
cf.save() cf.save()
return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data) return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data)

View File

@@ -1242,9 +1242,9 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
messages.error(self.request, _('There was a problem processing your input. See below for error details.')) messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
cf = CachedFile() cf = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now() cf.date = now()
cf.expires = now() + timedelta(days=3) cf.expires = now() + timedelta(hours=24)
cf.save() cf.save()
return self.do( return self.do(
organizer=self.request.organizer.id, organizer=self.request.organizer.id,

View File

@@ -137,7 +137,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
buffer = BytesIO() buffer = BytesIO()
p.write(buffer) p.write(buffer)
buffer.seek(0) buffer.seek(0)
c = CachedFile() c = CachedFile(web_download=True)
c.expires = now() + timedelta(days=7) c.expires = now() + timedelta(days=7)
c.date = now() c.date = now()
c.filename = 'background_preview.pdf' c.filename = 'background_preview.pdf'
@@ -162,7 +162,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
"status": "error", "status": "error",
"error": error "error": error
}) })
c = CachedFile() c = CachedFile(web_download=True)
c.expires = now() + timedelta(days=7) c.expires = now() + timedelta(days=7)
c.date = now() c.date = now()
c.filename = 'background_preview.pdf' c.filename = 'background_preview.pdf'
@@ -188,7 +188,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
pass pass
if "preview" in request.POST: if "preview" in request.POST:
with rolledback_transaction(), language(request.event.settings.locale): with rolledback_transaction(), language(request.event.settings.locale, request.event.settings.region):
p = self._get_preview_position() p = self._get_preview_position()
fname, mimet, data = self.generate( fname, mimet, data = self.generate(
p, p,

View File

@@ -75,7 +75,7 @@ class ShredExportView(RecentAuthenticationRequiredMixin, EventPermissionRequired
if constr: if constr:
return self.error(ShredError(self.get_error_url())) return self.error(ShredError(self.get_error_url()))
return self.do(self.request.event.id, request.POST.getlist("shredder")) return self.do(self.request.event.id, request.POST.getlist("shredder"), self.request.session.session_key)
class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View): class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):

View File

@@ -1,8 +1,9 @@
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import get_language
from django_countries import Countries from django_countries import Countries
from django_countries.fields import CountryField from django_countries.fields import CountryField
from pretix.base.i18n import get_language_without_region
class CachedCountries(Countries): class CachedCountries(Countries):
_cached_lists = {} _cached_lists = {}
@@ -14,7 +15,7 @@ class CachedCountries(Countries):
django-countries performs a unicode-aware sorting based on pyuca which is incredibly django-countries performs a unicode-aware sorting based on pyuca which is incredibly
slow. slow.
""" """
cache_key = "countries:all:{}".format(get_language()) cache_key = "countries:all:{}".format(get_language_without_region())
if self.cache_subkey: if self.cache_subkey:
cache_key += ":" + self.cache_subkey cache_key += ":" + self.cache_subkey
if cache_key in self._cached_lists: if cache_key in self._cached_lists:

View File

@@ -0,0 +1,18 @@
# Date according to https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
SHORT_DATE_FORMAT = 'm/d/Y'
SHORT_DATETIME_FORMAT = 'm/d/Y P'
TIME_FORMAT = 'P'
WEEK_FORMAT = '\\W W, o'
WEEK_DAY_FORMAT = 'D, M jS'
DATE_INPUT_FORMATS = [
'%m/%d/%Y',
'%Y-%m-%d',
'%m/%d/%y',
]
TIME_INPUT_FORMATS = [
'%I:%M %p',
'%H:%M:%S', # '14:30:59'
'%H:%M:%S.%f', # '14:30:59.000200'
'%H:%M', # '14:30'
]

View File

@@ -126,6 +126,7 @@ def get_javascript_format_without_seconds(format_name):
def get_moment_locale(locale=None): def get_moment_locale(locale=None):
cur_lang = locale or translation.get_language() cur_lang = locale or translation.get_language()
cur_lang = cur_lang.lower()
if cur_lang in moment_locales: if cur_lang in moment_locales:
return cur_lang return cur_lang
if '-' in cur_lang or '_' in cur_lang: if '-' in cur_lang or '_' in cur_lang:

File diff suppressed because it is too large Load Diff

View File

@@ -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: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2020-07-30 19:00+0000\n" "PO-Revision-Date: 2020-07-30 19:00+0000\n"
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n" "Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -302,15 +302,15 @@ msgstr "الكل"
msgid "None" msgid "None"
msgstr "لا شيء" msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "استخدام اسم مختلف داخليا" msgstr "استخدام اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "انقر لقريب" msgstr "انقر لقريب"
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,15 +7,17 @@ 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: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: Automatically generated\n" "Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: none\n" "Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ca/>\n"
"Language: ca\n" "Language: ca\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.10.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -25,7 +27,7 @@ msgstr ""
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:" msgid "Comment:"
msgstr "" msgstr "Comentari:"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
@@ -104,7 +106,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:189 #: pretix/static/pretixbase/js/asynctask.js:189
msgid "We are processing your request …" msgid "We are processing your request …"
msgstr "" msgstr "Estem processant la vostra sol·licitud …"
#: pretix/static/pretixbase/js/asynctask.js:197 #: pretix/static/pretixbase/js/asynctask.js:197
msgid "" msgid ""
@@ -230,7 +232,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:477 #: pretix/static/pretixcontrol/js/ui/editor.js:477
msgid "Ticket design" msgid "Ticket design"
msgstr "" msgstr "Disseny del tiquet"
#: pretix/static/pretixcontrol/js/ui/editor.js:734 #: pretix/static/pretixcontrol/js/ui/editor.js:734
msgid "Saving failed." msgid "Saving failed."
@@ -279,15 +281,15 @@ msgstr ""
msgid "None" msgid "None"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""
@@ -301,7 +303,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:82 #: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count" msgid "Count"
msgstr "" msgstr "Quantitat"
#: pretix/static/pretixcontrol/js/ui/question.js:135 #: pretix/static/pretixcontrol/js/ui/question.js:135
msgid "Yes" msgid "Yes"
@@ -319,11 +321,11 @@ msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39 #: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you." msgid "The items in your cart are no longer reserved for you."
msgstr "" msgstr "El contingut de la cistella ja no el teniu reservat."
#: pretix/static/pretixpresale/js/ui/cart.js:41 #: pretix/static/pretixpresale/js/ui/cart.js:41
msgid "Cart expired" msgid "Cart expired"
msgstr "" msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/cart.js:46 #: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute." msgid "The items in your cart are reserved for you for one minute."

File diff suppressed because it is too large Load Diff

View File

@@ -7,15 +7,17 @@ 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: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2020-12-14 10:00+0000\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
"Language-Team: none\n" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
"cs/>\n"
"Language: cs\n" "Language: cs\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 3.10.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -279,15 +281,15 @@ msgstr ""
msgid "None" msgid "None"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""
@@ -297,19 +299,19 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42 #: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others" msgid "Others"
msgstr "" msgstr "Další"
#: pretix/static/pretixcontrol/js/ui/question.js:82 #: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count" msgid "Count"
msgstr "" msgstr "Počet"
#: pretix/static/pretixcontrol/js/ui/question.js:135 #: pretix/static/pretixcontrol/js/ui/question.js:135
msgid "Yes" msgid "Yes"
msgstr "" msgstr "Ano"
#: pretix/static/pretixcontrol/js/ui/question.js:136 #: pretix/static/pretixcontrol/js/ui/question.js:136
msgid "No" msgid "No"
msgstr "" msgstr "Ne"
#: pretix/static/pretixcontrol/js/ui/subevent.js:111 #: pretix/static/pretixcontrol/js/ui/subevent.js:111
msgid "(one more date)" msgid "(one more date)"
@@ -361,12 +363,12 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17 #: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget" msgctxt "widget"
msgid "Sold out" msgid "Sold out"
msgstr "" msgstr "Vyprodáno"
#: pretix/static/pretixpresale/js/widget/widget.js:18 #: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget" msgctxt "widget"
msgid "Buy" msgid "Buy"
msgstr "" msgstr "Koupit"
#: pretix/static/pretixpresale/js/widget/widget.js:19 #: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget" msgctxt "widget"
@@ -455,62 +457,62 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36 #: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget" msgctxt "widget"
msgid "Resume checkout" msgid "Resume checkout"
msgstr "" msgstr "Obnovit checkout"
#: pretix/static/pretixpresale/js/widget/widget.js:37 #: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget" msgctxt "widget"
msgid "Redeem a voucher" msgid "Redeem a voucher"
msgstr "" msgstr "Uplatnit poukázku"
#: pretix/static/pretixpresale/js/widget/widget.js:38 #: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget" msgctxt "widget"
msgid "Redeem" msgid "Redeem"
msgstr "" msgstr "Uplatnit"
#: pretix/static/pretixpresale/js/widget/widget.js:39 #: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget" msgctxt "widget"
msgid "Voucher code" msgid "Voucher code"
msgstr "" msgstr "Kód poukázky"
#: pretix/static/pretixpresale/js/widget/widget.js:40 #: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget" msgctxt "widget"
msgid "Close" msgid "Close"
msgstr "" msgstr "Zavřít"
#: pretix/static/pretixpresale/js/widget/widget.js:41 #: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget" msgctxt "widget"
msgid "Continue" msgid "Continue"
msgstr "" msgstr "Pokračovat"
#: pretix/static/pretixpresale/js/widget/widget.js:42 #: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget" msgctxt "widget"
msgid "See variations" msgid "See variations"
msgstr "" msgstr "Zobrazit možnosti"
#: pretix/static/pretixpresale/js/widget/widget.js:43 #: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget" msgctxt "widget"
msgid "Choose a different event" msgid "Choose a different event"
msgstr "" msgstr "Vybrat jinou událost"
#: pretix/static/pretixpresale/js/widget/widget.js:44 #: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget" msgctxt "widget"
msgid "Choose a different date" msgid "Choose a different date"
msgstr "" msgstr "Vybrat jiný datum"
#: pretix/static/pretixpresale/js/widget/widget.js:45 #: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget" msgctxt "widget"
msgid "Back" msgid "Back"
msgstr "" msgstr "Zpět"
#: pretix/static/pretixpresale/js/widget/widget.js:46 #: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget" msgctxt "widget"
msgid "Next month" msgid "Next month"
msgstr "" msgstr "Následující měsíc"
#: pretix/static/pretixpresale/js/widget/widget.js:47 #: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget" msgctxt "widget"
msgid "Previous month" msgid "Previous month"
msgstr "" msgstr "Předchozí měsíc"
#: pretix/static/pretixpresale/js/widget/widget.js:48 #: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget" msgctxt "widget"
@@ -529,76 +531,76 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52 #: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Mo" msgid "Mo"
msgstr "" msgstr "Po"
#: pretix/static/pretixpresale/js/widget/widget.js:53 #: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Tu" msgid "Tu"
msgstr "" msgstr "Út"
#: pretix/static/pretixpresale/js/widget/widget.js:54 #: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "We" msgid "We"
msgstr "" msgstr "St"
#: pretix/static/pretixpresale/js/widget/widget.js:55 #: pretix/static/pretixpresale/js/widget/widget.js:55
msgid "Th" msgid "Th"
msgstr "" msgstr "Čt"
#: pretix/static/pretixpresale/js/widget/widget.js:56 #: pretix/static/pretixpresale/js/widget/widget.js:56
msgid "Fr" msgid "Fr"
msgstr "" msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57 #: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "Sa" msgid "Sa"
msgstr "" msgstr "So"
#: pretix/static/pretixpresale/js/widget/widget.js:58 #: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Su" msgid "Su"
msgstr "" msgstr "Ne"
#: pretix/static/pretixpresale/js/widget/widget.js:61 #: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "January" msgid "January"
msgstr "" msgstr "Leden"
#: pretix/static/pretixpresale/js/widget/widget.js:62 #: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "February" msgid "February"
msgstr "" msgstr "Únor"
#: pretix/static/pretixpresale/js/widget/widget.js:63 #: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "March" msgid "March"
msgstr "" msgstr "Březen"
#: pretix/static/pretixpresale/js/widget/widget.js:64 #: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "April" msgid "April"
msgstr "" msgstr "Duben"
#: pretix/static/pretixpresale/js/widget/widget.js:65 #: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "May" msgid "May"
msgstr "" msgstr "Květen"
#: pretix/static/pretixpresale/js/widget/widget.js:66 #: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "June" msgid "June"
msgstr "" msgstr "červen"
#: pretix/static/pretixpresale/js/widget/widget.js:67 #: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "July" msgid "July"
msgstr "" msgstr "Červenec"
#: pretix/static/pretixpresale/js/widget/widget.js:68 #: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "August" msgid "August"
msgstr "" msgstr "Srpen"
#: pretix/static/pretixpresale/js/widget/widget.js:69 #: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "September" msgid "September"
msgstr "" msgstr "Září"
#: pretix/static/pretixpresale/js/widget/widget.js:70 #: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "October" msgid "October"
msgstr "" msgstr "Říjen"
#: pretix/static/pretixpresale/js/widget/widget.js:71 #: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "November" msgid "November"
msgstr "" msgstr "Listopad"
#: pretix/static/pretixpresale/js/widget/widget.js:72 #: pretix/static/pretixpresale/js/widget/widget.js:72
msgid "December" msgid "December"
msgstr "" msgstr "Prosinec"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2020-09-15 02:00+0000\n" "PO-Revision-Date: 2020-09-15 02:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n" "Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -306,15 +306,15 @@ msgstr "Alle"
msgid "None" msgid "None"
msgstr "Ingen" msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "Klik for at lukke" msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!" msgstr "Du har ændringer, der ikke er gemt!"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2020-08-25 02:00+0000\n" "PO-Revision-Date: 2020-08-25 02:00+0000\n"
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n" "Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -301,15 +301,15 @@ msgstr "Alle"
msgid "None" msgid "None"
msgstr "Keine" msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden" msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "Klicken zum Schließen" msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "Sie haben ungespeicherte Änderungen!" msgstr "Sie haben ungespeicherte Änderungen!"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2020-08-25 02:00+0000\n" "PO-Revision-Date: 2020-08-25 02:00+0000\n"
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n" "Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/" "Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -300,15 +300,15 @@ msgstr "Alle"
msgid "None" msgid "None"
msgstr "Keine" msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden" msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "Klicken zum Schließen" msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "Du hast ungespeicherte Änderungen!" msgstr "Du hast ungespeicherte Änderungen!"

File diff suppressed because it is too large Load Diff

View File

@@ -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: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+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"
@@ -280,15 +280,15 @@ msgstr ""
msgid "None" msgid "None"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "" msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -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: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n" "PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n" "Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -320,15 +320,15 @@ msgstr "Όλα"
msgid "None" msgid "None"
msgstr "Κανένας" msgstr "Κανένας"
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά" msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "Κάντε κλικ για να κλείσετε" msgstr "Κάντε κλικ για να κλείσετε"
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "" msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -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: 2020-11-27 17:32+0000\n" "POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2020-04-27 20:00+0000\n" "PO-Revision-Date: 2020-04-27 20:00+0000\n"
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n" "Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -315,15 +315,15 @@ msgstr "Todos"
msgid "None" msgid "None"
msgstr "Ninguno" msgstr "Ninguno"
#: pretix/static/pretixcontrol/js/ui/main.js:706 #: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally" msgid "Use a different name internally"
msgstr "Usar un nombre diferente internamente" msgstr "Usar un nombre diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:763 #: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close" msgid "Click to close"
msgstr "Click para cerrar" msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:778 #: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!" msgid "You have unsaved changes!"
msgstr "¡Tienes cambios sin guardar!" msgstr "¡Tienes cambios sin guardar!"

File diff suppressed because it is too large Load Diff

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