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
181 changed files with 70020 additions and 60511 deletions

View File

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

View File

@@ -1,10 +1,13 @@
user www-data www-data;
worker_processes 1;
worker_processes auto;
pid /var/run/nginx.pid;
daemon off;
worker_rlimit_nofile 262144;
events {
worker_connections 4096;
worker_connections 16384;
multi_accept on;
use epoll;
}
http {
@@ -24,8 +27,8 @@ http {
default_type application/octet-stream;
add_header X-Content-Type-Options nosniff;
access_log /dev/stdout private;
error_log /dev/stderr;
access_log /var/log/nginx/access.log private;
error_log /var/log/nginx/error.log;
add_header Referrer-Policy same-origin;
gzip on;

View File

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

View File

@@ -2,8 +2,8 @@
file=/tmp/supervisor.sock
[supervisord]
logfile=/dev/stderr
logfile_maxbytes=0
logfile=/tmp/supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
loglevel=info
pidfile=/tmp/supervisord.pid

View File

@@ -5,7 +5,3 @@ autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -4,7 +4,3 @@ autostart=true
autorestart=true
priority=5
user=pretixuser
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -5,7 +5,3 @@ autorestart=true
priority=5
user=pretixuser
environment=HOME=/pretix
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -105,7 +105,12 @@ Example::
``csp_log``
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``
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
; Replace with the password you chose above
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
; or of a mounted MySQL socket.
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.
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
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.
secret string The secret contained in the link sent to the customer
email string The customer email address
phone string The customer phone number
locale string The locale used for communication with this customer
sales_channel string Channel this sale was created through, such as
``"web"``.
@@ -167,6 +168,10 @@ last_modified datetime Last modificati
The ``subevent_before`` query parameter has been added.
.. versionchanged:: 3.14
The ``phone`` attribute has been added.
.. _order-position-resource:
@@ -372,6 +377,7 @@ List of all orders
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"phone": "+491234567",
"locale": "en",
"sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z",
@@ -539,6 +545,7 @@ Fetching individual orders
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"phone": "+491234567",
"locale": "en",
"sales_channel": "web",
"datetime": "2017-12-01T10:00:00Z",
@@ -705,6 +712,8 @@ Updating order fields
* ``email``
* ``phone``
* ``checkin_attention``
* ``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
``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
@@ -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
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 (
event_permission_required, EventPermissionRequiredMixin
@@ -61,8 +65,9 @@ your views::
...
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.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
special navigation signal::
special navigation signal:
.. code-block:: python
@receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings')
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``
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):
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/``.
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
@@ -182,8 +193,9 @@ standard Django request handling: There are `ViewSets`_ to group related views i
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
``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
@@ -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.
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):
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
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)
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
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``
that we'll provide in this plugin::
that we'll provide in this plugin:
.. code-block:: python
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=""``
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):
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
all available exporters. Your plugin should listen for this signal and return the subclass of
``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
@@ -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
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

View File

@@ -15,7 +15,9 @@ Output registration
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
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

View File

@@ -19,7 +19,9 @@ Provider registration
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
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
@@ -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
: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
view which looks roughly like this::
view which looks roughly like this:
.. code-block:: python
@login_required
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
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
@@ -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
functions::
functions:
.. code-block:: python
placeholder = SimpleFunctionalMailTextPlaceholder(
'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.
================== ==================== ===========================================================
A working example would be::
A working example would be:
.. code-block:: python
try:
from pretix.base.plugins import PluginConfig
@@ -81,7 +83,7 @@ A working example would be::
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,
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``
@@ -96,7 +98,9 @@ Plugin registration
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
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(
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
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
method to make your receivers available::
method to make your receivers available:
.. code-block:: python
class PaypalApp(AppConfig):
@@ -127,7 +133,9 @@ method to make your receivers available::
from . import signals # NOQA
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):

View File

@@ -74,7 +74,7 @@ looks like this:
def generate_files(self) -> List[Tuple[str, str, str]]:
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)
}, 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
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``
that we'll provide in this plugin::
that we'll provide in this plugin:
.. code-block:: python
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``
and looks like this::
and looks like this:
.. code-block:: python
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
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.
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):
"""
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.
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.
"""
@@ -79,7 +83,9 @@ A usage example taken directly from the code is::
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
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
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.
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'):
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
: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={})
@@ -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,
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
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.'))
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
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
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 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
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)
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
: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):
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
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
@@ -151,7 +165,9 @@ and displayed later, you can just use Python's ``logging`` module::
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:
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
permission level to access a view::
permission level to access a view:
.. code-block:: python
from pretix.control.permissions import (
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
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 (
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
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 (
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
necessarily have an active admin session::
necessarily have an active admin session:
.. code-block:: python
from pretix.control.permissions import (
StaffMemberRequiredMixin, staff_member_required

View File

@@ -39,7 +39,9 @@ subclass that also adds support for internationalized fields:
.. 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):
show_date_to = forms.BooleanField(
@@ -56,7 +58,9 @@ You can simply use it like this::
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

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

View File

@@ -180,7 +180,7 @@ class PdfDataSerializer(serializers.Field):
res = {}
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
# we serialize a list.
@@ -361,7 +361,7 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
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',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url'
@@ -393,7 +393,7 @@ class OrderSerializer(I18nAwareModelSerializer):
def update(self, instance, validated_data):
# 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.
update_fields = ['comment', 'checkin_attention', 'email', 'locale']
update_fields = ['comment', 'checkin_attention', 'email', 'locale', 'phone']
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -691,7 +691,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta:
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',
'force', 'send_email', 'simulate')

View File

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

View File

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

View File

@@ -582,7 +582,7 @@ class OrderViewSet(viewsets.ModelViewSet):
auth=request.auth,
)
with language(order.locale):
with language(order.locale, self.request.event.settings.region):
order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID:
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'):
serializer.instance.log_action(
'pretix.event.order.locale.changed',
@@ -886,7 +897,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
price = get_price(**kwargs)
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({
'gross': price.gross,
'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,
'subject': str(subject),
'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:
htmlctx['event'] = self.event

View File

@@ -139,7 +139,7 @@ class OrderListExporter(MultiSheetListExporter):
tax_rates = self._get_all_tax_rates(qs)
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'),
]
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.get_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'),
]
@@ -303,6 +304,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Order code'),
_('Status'),
_('Email'),
_('Phone number'),
_('Order date'),
_('Order time'),
_('Fee type'),
@@ -334,6 +336,7 @@ class OrderListExporter(MultiSheetListExporter):
order.code,
order.get_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'),
op.get_fee_type_display(),
@@ -402,6 +405,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Position ID'),
_('Status'),
_('Email'),
_('Phone number'),
_('Order date'),
_('Order time'),
]
@@ -481,6 +485,7 @@ class OrderListExporter(MultiSheetListExporter):
op.positionid,
order.get_status_display(),
order.email,
str(order.phone) if order.phone else '',
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
order.datetime.astimezone(tz).strftime('%H:%M:%S'),
]

View File

@@ -1,12 +1,17 @@
import hashlib
import ipaddress
from django import forms
from django.conf import settings
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pretix.base.models import User
from pretix.helpers.dicts import move_to_end
from pretix.helpers.http import get_client_ip
class LoginForm(forms.Form):
@@ -18,6 +23,7 @@ class LoginForm(forms.Form):
error_messages = {
'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.")
}
@@ -39,10 +45,36 @@ class LoginForm(forms.Form):
else:
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):
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)
if self.user_cache is None:
if self.ratelimit_key:
rc.incr(self.ratelimit_key)
rc.expire(self.ratelimit_key, 300)
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login'

View File

@@ -9,7 +9,6 @@ import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
from babel import localedata
from django import forms
from django.contrib import messages
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.safestring import mark_safe
from django.utils.timezone import get_current_timezone
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries
from django_countries.fields import Country, CountryField
from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
from phonenumber_field.widgets import PhoneNumberPrefixWidget
from phonenumbers import NumberParseException
from phonenumber_field.widgets import (
PhoneNumberPrefixWidget, PhonePrefixSelect,
)
from phonenumbers import NumberParseException, national_significant_number
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
from pretix.base.forms.widgets import (
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
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.tax import (
EU_COUNTRIES, cc_to_vat_prefix, is_eu_country,
@@ -204,7 +205,18 @@ class NamePartsFormField(forms.MultiValueField):
return value
class WrappedPhonePrefixSelect(PhonePrefixSelect):
def __init__(self, *args, **kwargs):
with language(get_babel_locale()):
super().__init__(*args, **kwargs)
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):
output = super().render(name, value, attrs, renderer)
return mark_safe(self.format_output(output))
@@ -212,12 +224,44 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def format_output(self, rendered_widgets) -> str:
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):
# 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 :)
locale = get_language()
country = event.settings.invoice_address_from_country
locale = get_language_without_region()
country = event.settings.region or event.settings.invoice_address_from_country
if not country:
valid_countries = countries.countries
if '-' in locale:
@@ -532,13 +576,7 @@ class BaseQuestionsForm(forms.Form):
if q.valid_datetime_max:
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
elif q.type == Question.TYPE_PHONENUMBER:
babel_locale = 'en'
# 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):
with language(get_babel_locale()):
default_country = guess_country(event)
default_prefix = None
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():

View File

@@ -1,4 +1,5 @@
from django import forms
from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
@@ -19,6 +20,7 @@ class UserSettingsForm(forms.ModelForm):
"address or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'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,
@@ -64,6 +66,18 @@ class UserSettingsForm(forms.ModelForm):
def clean_old_pw(self):
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):
raise forms.ValidationError(
self.error_messages['pw_current_wrong'],

View File

@@ -1,5 +1,6 @@
from contextlib import contextmanager
from babel import localedata
from django.conf import settings
from django.utils import translation
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)
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
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()
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:
yield
finally:

View File

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

View File

@@ -15,7 +15,8 @@ from django.utils.translation.trans_real import (
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 (
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
# 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.
if hasattr(request, 'event') and not request.path.startswith(get_script_prefix() + 'control'):
if language not in request.event.settings.locales:
firstpart = language.split('-')[0]
if firstpart in request.event.settings.locales:
language = firstpart
else:
language = request.event.settings.locale
for lang in request.event.settings.locales:
if lang.startswith(firstpart + '-'):
language = lang
break
if not request.path.startswith(get_script_prefix() + 'control'):
if hasattr(request, 'event'):
if language not in request.event.settings.locales:
firstpart = language.split('-')[0]
if firstpart in request.event.settings.locales:
language = firstpart
else:
language = request.event.settings.locale
for lang in request.event.settings.locales:
if lang.startswith(firstpart + '-'):
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)
request.LANGUAGE_CODE = translation.get_language()
request.LANGUAGE_CODE = get_language_without_region()
tzname = None
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\"'
img_src = []
gs = GlobalSettingsObject()
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
@@ -216,6 +228,8 @@ class SecurityMiddleware(MiddlewareMixin):
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
if settings.CSP_ADDITIONAL_HEADER:
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
staticdomain = "'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)
type = models.CharField(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)

View File

@@ -528,7 +528,7 @@ class Event(EventMixin, LoggedModel):
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
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,
use_tls=self.settings.smtp_use_tls,
use_ssl=self.settings.smtp_use_ssl,
fail_silently=False)
fail_silently=False, timeout=timeout)
else:
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 i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
from phonenumber_field.modelfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
from phonenumbers import NumberParseException
@@ -86,6 +87,8 @@ class Order(LockModel, LoggedModel):
:type event: Event
:param email: The email of the person who ordered this
: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
:type testmode: bool
:param locale: The locale of this order
@@ -144,6 +147,10 @@ class Order(LockModel, LoggedModel):
null=True, blank=True,
verbose_name=_('E-mail')
)
phone = PhoneNumberField(
null=True, blank=True,
verbose_name=_('Phone number'),
)
locale = models.CharField(
null=True, blank=True, max_length=32,
verbose_name=_('Locale')
@@ -326,6 +333,9 @@ class Order(LockModel, LoggedModel):
payment_sum=payment_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(
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():
context['meta_' + k] = v
with language(self.locale):
with language(self.locale, self.event.settings.region):
recipient = self.email
if position and position.attendee_email:
recipient = position.attendee_email
@@ -890,7 +900,7 @@ class Order(LockModel, LoggedModel):
)
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_context = get_email_context(event=self.event, order=self)
email_subject = _('Your order: %(code)s') % {'code': self.code}
@@ -902,7 +912,7 @@ class Order(LockModel, LoggedModel):
@property
def positions_with_tickets(self):
for op in self.positions.all():
for op in self.positions.select_related('item'):
if not op.generate_ticket:
continue
yield op
@@ -1155,7 +1165,7 @@ class AbstractPosition(models.Model):
(2) questions: a list of Question objects, extended by an 'answer' property
"""
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
# 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):
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_context = get_email_context(event=self.order.event, order=self.order, position=position)
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):
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_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}
@@ -2104,7 +2114,7 @@ class OrderPosition(AbstractPosition):
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.order.locale):
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
@@ -2132,7 +2142,7 @@ class OrderPosition(AbstractPosition):
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_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = _('Your event registration: %(code)s') % {'code': self.order.code}

View File

@@ -125,7 +125,7 @@ class WaitingListEntry(LoggedModel):
self.voucher = v
self.save()
with language(self.locale):
with language(self.locale, self.event.settings.region):
mail(
self.email,
_('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.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
@@ -229,6 +230,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Random City"),
"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", {
"label": _("Invoice address name"),
"editor_sample": _("John Doe"),
@@ -421,6 +427,7 @@ class Renderer:
self.layout = layout
self.background_file = background_file
self.variables = get_variables(event)
self.event = event
if self.background_file:
self.bg_bytes = self.background_file.read()
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):
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)
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):
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)
try:
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,
refund_amount: Decimal, user: User, positions: list):
with language(order.locale):
with language(order.locale, order.event.settings.region):
try:
ia = order.invoice_address
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)
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)
for receiver, response in responses:
ex = response(event, set_progress)
@@ -67,15 +67,18 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
if user:
locale = user.locale
timezone = user.timezone
region = None # todo: add to user?
else:
e = allowed_events.first()
if e:
locale = e.settings.locale
timezone = e.settings.timezone
region = e.settings.region
else:
locale = settings.LANGUAGE_CODE
timezone = settings.TIME_ZONE
with language(locale), override(timezone):
region = None
with language(locale, region), override(timezone):
if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else:

View File

@@ -43,7 +43,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
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_name = invoice.event.settings.get('invoice_address_from_name')
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.payment_provider_text = ''
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_name = invoice.event.settings.get('invoice_address_from_name')
cancellation.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode')
@@ -297,7 +297,7 @@ def invoice_pdf_task(invoice: int):
return None
if i.file:
i.file.delete()
with language(i.locale):
with language(i.locale, i.event.settings.region):
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
i.file.save(fname, ContentFile(fcontent))
i.save()
@@ -328,7 +328,7 @@ def build_preview_invoice_pdf(event):
if not locale or locale == '__user__':
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(),
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
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:
order = None
else:
with language(order.locale):
with language(order.locale, event.settings.region):
if position:
try:
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?
cf = CachedFile.objects.get(id=fileid)
user = User.objects.get(pk=user)
with language(locale):
with language(locale, event.settings.region):
cols = get_all_columns(event)
parsed = parse_csv(cf.file)
orders = []
@@ -163,7 +163,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
)
for o in orders:
with language(o.locale):
with language(o.locale, event.settings.region):
order_placed.send(event, order=o)
if o.status == Order.STATUS_PAID:
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.base.channels import get_all_sales_channels
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 (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Order,
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
if send_mail:
with language(order.locale):
with language(order.locale, order.event.settings.region):
if order.total == Decimal('0.00'):
email_template = order.event.settings.mail_text_order_approved_free
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:
email_template = order.event.settings.mail_text_order_denied
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}
try:
order.send_mail(
@@ -422,7 +424,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if send_mail:
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_subject = _('Order canceled: %(code)s') % {'code': order.code}
try:
@@ -776,8 +778,9 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
status=Order.STATUS_PENDING,
event=event,
email=email,
phone=(meta_info or {}).get('contact_form_data', {}).get('phone'),
datetime=now_dt,
locale=locale,
locale=get_language_without_region(locale),
total=total,
testmode=True if sales_channel.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}),
@@ -1033,7 +1036,7 @@ def send_expiry_warnings(sender, **kwargs):
# Race condition
continue
with language(o.locale):
with language(o.locale, settings.region):
o.expiry_reminder_sent = True
o.save(update_fields=['expiry_reminder_sent'])
email_template = settings.mail_text_order_expire_warning
@@ -1110,7 +1113,7 @@ def send_download_reminders(sender, **kwargs):
if not send:
continue
with language(o.locale):
with language(o.locale, o.event.settings.region):
o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder
@@ -1150,7 +1153,7 @@ def send_download_reminders(sender, **kwargs):
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_context = get_email_context(event=order.event, order=order)
email_subject = _('Your order has been changed: %(code)s') % {'code': order.code}

View File

@@ -113,10 +113,11 @@ class QuotaAvailability:
raise e
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 = []
for e in events:
e.cache.delete('item_quota_cache')
for q in quotas:
rewrite_cache = self._count_waitinglist and (
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)
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()
with NamedTemporaryFile() as rawfile:
@@ -55,6 +55,8 @@ def export(event: Event, shredders: List[str]) -> None:
cf.date = now()
cf.filename = event.slug + '.zip'
cf.type = 'application/zip'
cf.session_key = session_key
cf.web_download = True
cf.expires = now() + timedelta(hours=1)
cf.save()
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):
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)
for receiver, response in responses:
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):
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)
for receiver, response in responses:
prov = response(order.event)
@@ -75,7 +75,7 @@ class DummyRollbackException(Exception):
def preview(event: int, provider: str):
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,
description=_("Sample product description"))
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."),
)
},
'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': {
'default': 'True',
'type': bool,
@@ -832,6 +851,20 @@ DEFAULTS = {
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': {
'default': 'True',
'type': bool,
@@ -1845,6 +1878,17 @@ Your {event} team"""))
"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': {
'default': LazyI18nString.from_gettext(gettext_noop(
'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.
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.signals import register_data_shredders
from pretix.helpers.json import CustomJSONEncoder
class ShredError(LazyLocaleException):
@@ -121,6 +122,31 @@ def shred_log_fields(logentry, banlist=None, whitelist=None):
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):
verbose_name = _('E-mails')
identifier = 'order_emails'
@@ -372,9 +398,10 @@ class PaymentInfoShredder(BaseDataShredder):
@receiver(register_data_shredders, dispatch_uid="shredders_builtin")
def register_payment_provider(sender, **kwargs):
def register_core_shredders(sender, **kwargs):
return [
EmailAddressShredder,
PhoneNumberShredder,
AttendeeInfoShredder,
InvoiceAddressShredder,
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 bleach
@@ -71,6 +72,10 @@ EMAIL_RE = build_email_re(tlds=sorted(tld_set, key=len, reverse=True))
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'), '/')
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')
@@ -80,7 +85,42 @@ def safelink_callback(attrs, new=False):
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):
"""
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'), '/')
if not url.startswith('mailto:') and not url.startswith('tel:'):
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, url)
@@ -93,6 +133,7 @@ def markdown_compile_email(source):
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
return linker.linkify(bleach.clean(
@@ -145,7 +186,7 @@ def rich_text(text: str, **kwargs):
linker = bleach.Linker(
url_re=URL_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
)
body_md = linker.linkify(markdown_compile(text))
@@ -161,7 +202,7 @@ def rich_text_snippet(text: str, **kwargs):
linker = bleach.Linker(
url_re=URL_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
)
body_md = linker.linkify(markdown_compile(text, snippet=True))

View File

@@ -13,7 +13,11 @@ class DownloadView(TemplateView):
@cached_property
def object(self) -> CachedFile:
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
raise Http404()

View File

@@ -203,6 +203,7 @@ class CachedFileField(ExtFileField):
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
web_download=True,
filename=data.name,
type=data.content_type,
)
@@ -218,6 +219,7 @@ class CachedFileField(ExtFileField):
if isinstance(data, File):
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
web_download=True,
date=now(),
filename=data.name,
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.email import get_available_placeholders
from pretix.base.forms import (
I18nModelForm, PlaceholderValidator, SettingsForm,
)
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
@@ -459,6 +457,7 @@ class EventSettingsForm(SettingsForm):
'presale_start_show_date',
'locales',
'locale',
'region',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_hours',
@@ -482,6 +481,9 @@ class EventSettingsForm(SettingsForm):
'attendee_addresses_asked',
'attendee_addresses_required',
'attendee_data_explanation_text',
'order_phone_asked',
'order_phone_required',
'checkout_phone_helptext',
'banner_text',
'banner_text_bottom',
'order_email_asked_twice',
@@ -499,6 +501,31 @@ class EventSettingsForm(SettingsForm):
data = super().clean()
settings_dict = self.event.settings.freeze()
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)
return data
@@ -526,6 +553,39 @@ class EventSettingsForm(SettingsForm):
(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):
auto_fields = [

View File

@@ -150,8 +150,8 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
)),
(_('Cancellations'), (
(Order.STATUS_CANCELED, _('Canceled')),
('cp', _('Canceled (or with paid fee)')),
(Order.STATUS_CANCELED, _('Canceled (fully)')),
('cp', _('Canceled (fully or with paid fee)')),
('rc', _('Cancellation requested')),
)),
(_('Payment process'), (
@@ -159,7 +159,8 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
('o', _('Pending (overdue)')),
('overpaid', _('Overpaid')),
('underpaid', _('Underpaid')),
('partially_paid', _('Partially paid')),
('underpaid', _('Underpaid (but confirmed)')),
('pendingpaid', _('Pending (but fully paid)')),
)),
(_('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(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':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(

View File

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

View File

@@ -16,6 +16,7 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.questions import WrappedPhoneNumberPrefixWidget
from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget,
)
@@ -460,7 +461,15 @@ class OrderContactForm(forms.ModelForm):
class Meta:
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):

View File

@@ -223,6 +223,7 @@ class OrganizerSettingsForm(SettingsForm):
'giftcard_length',
'giftcard_expiry_years',
'locales',
'region',
'event_team_provisioning',
'primary_color',
'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.contact.changed': _('The email address has been changed from "{old_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.invoice.generated': _('The invoice has been generated.'),
'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 %}"
defer></script>
{% else %}
<script src="{% statici18n LANGUAGE_CODE %}" async></script>
<script src="{% statici18n request.LANGUAGE_CODE %}" async></script>
{% endif %}
{% compress js %}
<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.locale layout="control" %}
{% bootstrap_field sform.timezone layout="control" %}
{% bootstrap_field sform.region layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Attendee data" %}</legend>
{% bootstrap_field sform.attendee_names_asked layout="control" %}
{% bootstrap_field sform.attendee_names_required layout="control" %}
<legend>{% trans "Customer and attendee data" %}</legend>
<h4>{% trans "Customer data (once per order)" %}</h4>
<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_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.attendee_data_explanation_text layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Texts" %}</legend>
@@ -177,6 +216,7 @@
</div>
{% 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_bottom layout="control" %}
</fieldset>

View File

@@ -6,6 +6,7 @@
{% load rich_text %}
{% load safelink %}
{% load eventsignal %}
{% load phone_format %}
{% block title %}
{% blocktrans trimmed with code=order.code %}
Order details: {{ code }}
@@ -201,6 +202,15 @@
{% endif %}
{% endif %}
</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 %}
<dt>{% trans "Invoices" %}</dt>
<dd>
@@ -560,6 +570,26 @@
</div>
<div class="clearfix"></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>
{% 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-up"></i></a>
</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-up"></i></a></th>
<th class="text-right flip">{% trans "Positions" %}</th>
@@ -141,7 +141,11 @@
<br>{{ o.invoice_address.name }}
{% endif %}
</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">
{% if o.has_cancellation_request %}
<span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span>
@@ -158,6 +162,13 @@
{% elif o.is_pending_with_full_payment %}
<span class="label label-danger">{% trans "FULLY PAID" %}</span>
{% 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 }}
</td>
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>

View File

@@ -45,6 +45,7 @@
<fieldset>
<legend>{% trans "Localization" %}</legend>
{% bootstrap_field sform.locales layout="control" %}
{% bootstrap_field sform.region layout="control" %}
</fieldset>
<fieldset>
<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>
<div class="col-md-9">
<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"
id="id_url" readonly>
</div>

View File

@@ -1,4 +1,5 @@
from django.conf.urls import include, url
from django.views.generic.base import RedirectView
from pretix.control.views import (
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(),
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]
if request.user.is_authenticated:
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':
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:
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
else:
form = LoginForm(backend=backend)
form = LoginForm(backend=backend, request=request)
ctx['form'] = form
ctx['can_register'] = settings.PRETIX_REGISTRATION
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.urls import reverse
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.timezone import now
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.presale.style import regenerate_css
from ...base.i18n import language
from ...base.models.items import ItemMetaProperty
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
from ..logdisplay import OVERVIEW_BANLIST
@@ -594,7 +594,7 @@ class MailSettings(EventSettingsViewMixin, EventSettingsFormView):
)
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:
backend.test(self.request.event.settings.mail_from)
except Exception as e:
@@ -659,7 +659,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
if matched is not None:
idx = matched.group('idx')
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(
v.format_map(self.placeholders(preview_item))
)

View File

@@ -71,7 +71,7 @@ class GeoCodeView(LoginRequiredMixin, View):
try:
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
)
)

View File

@@ -153,17 +153,18 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
annotated = {
o['pk']: o
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']]
).annotate(
pcnt=Subquery(s, output_field=IntegerField()),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
).values(
'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']:
if o.pk not in annotated:
continue
@@ -174,6 +175,8 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
o.has_external_refund = annotated.get(o.pk)['has_external_refund']
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
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:
# 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['sales_channel'] = get_all_sales_channels().get(self.order.sales_channel)
ctx['download_buttons'] = self.download_buttons
ctx['payment_refund_sum'] = self.order.payment_refund_sum
ctx['pending_sum'] = self.order.pending_sum
return ctx
@cached_property
@@ -666,7 +671,7 @@ class OrderCancellationRequestDelete(OrderView):
}, user=self.request.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={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
@@ -937,7 +942,7 @@ class OrderRefundView(OrderView):
if giftcard_value and self.order.email:
messages.success(self.request, _('A new gift card was created. You can now send the user their '
'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={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
@@ -1663,6 +1668,7 @@ class OrderContactChange(OrderView):
def post(self, *args, **kwargs):
old_email = self.order.email
old_phone = self.order.phone
changed = False
if self.form.is_valid():
new_email = self.form.cleaned_data['email']
@@ -1677,6 +1683,18 @@ class OrderContactChange(OrderView):
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']:
changed = True
self.order.secret = generate_secret()
@@ -1779,7 +1797,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
code=self.kwargs['code'].upper()
)
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_template = LazyI18nString(form.cleaned_data['message'])
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
@@ -1842,7 +1860,7 @@ class OrderPositionSendMail(OrderSendMail):
attendee_email__isnull=False
)
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_template = LazyI18nString(form.cleaned_data['message'])
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.'))
return self.get(request, *args, **kwargs)
cf = CachedFile()
cf = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now()
cf.expires = now() + timedelta(days=3)
cf.expires = now() + timedelta(hours=24)
cf.save()
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.'))
return self.get(request, *args, **kwargs)
cf = CachedFile()
cf = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now()
cf.expires = now() + timedelta(days=3)
cf.expires = now() + timedelta(hours=24)
cf.save()
return self.do(
organizer=self.request.organizer.id,

View File

@@ -137,7 +137,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
buffer = BytesIO()
p.write(buffer)
buffer.seek(0)
c = CachedFile()
c = CachedFile(web_download=True)
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
@@ -162,7 +162,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
"status": "error",
"error": error
})
c = CachedFile()
c = CachedFile(web_download=True)
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
@@ -188,7 +188,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
pass
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()
fname, mimet, data = self.generate(
p,

View File

@@ -75,7 +75,7 @@ class ShredExportView(RecentAuthenticationRequiredMixin, EventPermissionRequired
if constr:
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):

View File

@@ -1,8 +1,9 @@
from django.core.cache import cache
from django.utils.translation import get_language
from django_countries import Countries
from django_countries.fields import CountryField
from pretix.base.i18n import get_language_without_region
class CachedCountries(Countries):
_cached_lists = {}
@@ -14,7 +15,7 @@ class CachedCountries(Countries):
django-countries performs a unicode-aware sorting based on pyuca which is incredibly
slow.
"""
cache_key = "countries:all:{}".format(get_language())
cache_key = "countries:all:{}".format(get_language_without_region())
if self.cache_subkey:
cache_key += ":" + self.cache_subkey
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):
cur_lang = locale or translation.get_language()
cur_lang = cur_lang.lower()
if cur_lang in moment_locales:
return 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 ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -302,15 +302,15 @@ msgstr "الكل"
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:706
#: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally"
msgstr "استخدام اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:763
#: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close"
msgstr "انقر لقريب"
#: pretix/static/pretixcontrol/js/ui/main.js:778
#: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,15 +7,17 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-27 17:32+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ca/>\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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:62
@@ -25,7 +27,7 @@ msgstr ""
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr ""
msgstr "Comentari:"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
@@ -104,7 +106,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:189
msgid "We are processing your request …"
msgstr ""
msgstr "Estem processant la vostra sol·licitud …"
#: pretix/static/pretixbase/js/asynctask.js:197
msgid ""
@@ -230,7 +232,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:477
msgid "Ticket design"
msgstr ""
msgstr "Disseny del tiquet"
#: pretix/static/pretixcontrol/js/ui/editor.js:734
msgid "Saving failed."
@@ -279,15 +281,15 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:706
#: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763
#: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:778
#: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!"
msgstr ""
@@ -301,7 +303,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count"
msgstr ""
msgstr "Quantitat"
#: pretix/static/pretixcontrol/js/ui/question.js:135
msgid "Yes"
@@ -319,11 +321,11 @@ msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39
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
msgid "Cart expired"
msgstr ""
msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/cart.js:46
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 ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-27 17:32+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"POT-Creation-Date: 2020-12-22 11:06+0000\n"
"PO-Revision-Date: 2020-12-14 10:00+0000\n"
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
"cs/>\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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:62
@@ -279,15 +281,15 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:706
#: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763
#: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:778
#: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!"
msgstr ""
@@ -297,19 +299,19 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
msgstr "Další"
#: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count"
msgstr ""
msgstr "Počet"
#: pretix/static/pretixcontrol/js/ui/question.js:135
msgid "Yes"
msgstr ""
msgstr "Ano"
#: pretix/static/pretixcontrol/js/ui/question.js:136
msgid "No"
msgstr ""
msgstr "Ne"
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
msgid "(one more date)"
@@ -361,12 +363,12 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Sold out"
msgstr ""
msgstr "Vyprodáno"
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Buy"
msgstr ""
msgstr "Koupit"
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
@@ -455,62 +457,62 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
msgstr "Obnovit checkout"
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
msgstr "Uplatnit poukázku"
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Redeem"
msgstr ""
msgstr "Uplatnit"
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Voucher code"
msgstr ""
msgstr "Kód poukázky"
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "Close"
msgstr ""
msgstr "Zavřít"
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Continue"
msgstr ""
msgstr "Pokračovat"
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "See variations"
msgstr ""
msgstr "Zobrazit možnosti"
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
msgstr "Vybrat jinou událost"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Choose a different date"
msgstr ""
msgstr "Vybrat jiný datum"
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Back"
msgstr ""
msgstr "Zpět"
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "Next month"
msgstr ""
msgstr "Následující měsíc"
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
msgid "Previous month"
msgstr ""
msgstr "Předchozí měsíc"
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget"
@@ -529,76 +531,76 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgid "Mo"
msgstr ""
msgstr "Po"
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgid "Tu"
msgstr ""
msgstr "Út"
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgid "We"
msgstr ""
msgstr "St"
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgid "Th"
msgstr ""
msgstr "Čt"
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgid "Fr"
msgstr ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgid "Sa"
msgstr ""
msgstr "So"
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Su"
msgstr ""
msgstr "Ne"
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "January"
msgstr ""
msgstr "Leden"
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "February"
msgstr ""
msgstr "Únor"
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "March"
msgstr ""
msgstr "Březen"
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "April"
msgstr ""
msgstr "Duben"
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgid "May"
msgstr ""
msgstr "Květen"
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgid "June"
msgstr ""
msgstr "červen"
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "July"
msgstr ""
msgstr "Červenec"
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "August"
msgstr ""
msgstr "Srpen"
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "September"
msgstr ""
msgstr "Září"
#: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "October"
msgstr ""
msgstr "Říjen"
#: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "November"
msgstr ""
msgstr "Listopad"
#: pretix/static/pretixpresale/js/widget/widget.js:72
msgid "December"
msgstr ""
msgstr "Prosinec"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \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"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -306,15 +306,15 @@ msgstr "Alle"
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:706
#: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763
#: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close"
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!"
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 ""
"Project-Id-Version: \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"
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -301,15 +301,15 @@ msgstr "Alle"
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:706
#: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally"
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"
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!"
msgstr "Sie haben ungespeicherte Änderungen!"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \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"
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -300,15 +300,15 @@ msgstr "Alle"
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:706
#: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally"
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"
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!"
msgstr "Du hast ungespeicherte Änderungen!"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -280,15 +280,15 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:706
#: pretix/static/pretixcontrol/js/ui/main.js:709
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:763
#: pretix/static/pretixcontrol/js/ui/main.js:766
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:778
#: pretix/static/pretixcontrol/js/ui/main.js:781
msgid "You have unsaved changes!"
msgstr ""

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