forked from CGM_Public/pretix_original
Compare commits
86 Commits
docker-ngi
...
validate-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5a2286a4 | ||
|
|
3d66bfee7f | ||
|
|
0b495a4070 | ||
|
|
23aba9b5ef | ||
|
|
454f0f6fc8 | ||
|
|
002ff38fba | ||
|
|
dc8bd59715 | ||
|
|
56a2da08df | ||
|
|
4762d6818f | ||
|
|
e99e91d20f | ||
|
|
9fee2d0fbc | ||
|
|
3f30ddc9ab | ||
|
|
641a848f30 | ||
|
|
a582322847 | ||
|
|
a7ec7491ec | ||
|
|
90ae8860dd | ||
|
|
00ca75e119 | ||
|
|
455fb2e560 | ||
|
|
1ec4c524f8 | ||
|
|
75b9b04c65 | ||
|
|
bf0a9675f4 | ||
|
|
853877f2da | ||
|
|
2e44900c43 | ||
|
|
c5085bb46e | ||
|
|
da859b9980 | ||
|
|
b6f30f6996 | ||
|
|
9fde378eac | ||
|
|
52e9525f64 | ||
|
|
80aeeed855 | ||
|
|
d207514c9a | ||
|
|
1286e53b85 | ||
|
|
7c0df5b755 | ||
|
|
8889d8441e | ||
|
|
c60a25f2bc | ||
|
|
a3dd015c23 | ||
|
|
736ecbd7b6 | ||
|
|
8ed41a1276 | ||
|
|
06643232cf | ||
|
|
90399d2567 | ||
|
|
609203196b | ||
|
|
070b871254 | ||
|
|
cbadb2c395 | ||
|
|
0e9951f964 | ||
|
|
6afb954b93 | ||
|
|
bdf1fc2c23 | ||
|
|
9c0c8a95fa | ||
|
|
356a2dc9c5 | ||
|
|
4f5a9284ca | ||
|
|
130b06d26b | ||
|
|
ab4dd9b8de | ||
|
|
bb6b8bd8bb | ||
|
|
2aeceeed08 | ||
|
|
39223f0f65 | ||
|
|
33ba4daadb | ||
|
|
1f9adcce6e | ||
|
|
4d36676cf8 | ||
|
|
821cb54ad0 | ||
|
|
a40951060f | ||
|
|
c6a98fad5a | ||
|
|
d3a0405faa | ||
|
|
664bb9a65b | ||
|
|
06d8464998 | ||
|
|
c9b20d2cf5 | ||
|
|
a198635865 | ||
|
|
4e26df5752 | ||
|
|
5caa874263 | ||
|
|
05939537dd | ||
|
|
0d29f8624f | ||
|
|
0d8db8266d | ||
|
|
09be2c1199 | ||
|
|
da8ecb6e6e | ||
|
|
4240ad43d0 | ||
|
|
c47e41ac8a | ||
|
|
04bfa63a5e | ||
|
|
e311341d01 | ||
|
|
1f21d1420c | ||
|
|
5c1d637637 | ||
|
|
ecc72d54ad | ||
|
|
ff8a3ea1c3 | ||
|
|
924bad3484 | ||
|
|
808df7a982 | ||
|
|
7f196ef6fe | ||
|
|
44ef9b608a | ||
|
|
62b1aec3b0 | ||
|
|
571fef4ed8 | ||
|
|
5308099d84 |
@@ -1,3 +1,10 @@
|
||||
doc/
|
||||
env/
|
||||
res/
|
||||
local/
|
||||
.git/
|
||||
pretixeu/
|
||||
src/data/
|
||||
src/pretix/static.dist/
|
||||
src/dist/
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
…
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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" … %}">
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.14.0.dev0"
|
||||
__version__ = "3.15.0.dev0"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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'"
|
||||
|
||||
23
src/pretix/base/migrations/0162b_auto_20201218_1810.py
Normal file
23
src/pretix/base/migrations/0162b_auto_20201218_1810.py
Normal 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),
|
||||
),
|
||||
]
|
||||
51
src/pretix/base/migrations/0173_auto_20201211_1648.py
Normal file
51
src/pretix/base/migrations/0173_auto_20201211_1648.py
Normal 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,
|
||||
)
|
||||
]
|
||||
14
src/pretix/base/migrations/0174_merge_20201222_1031.py
Normal file
14
src/pretix/base/migrations/0174_merge_20201222_1031.py
Normal 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 = [
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
22
src/pretix/base/templatetags/phone_format.py
Normal file
22
src/pretix/base/templatetags/phone_format.py
Normal 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)
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -223,6 +223,7 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
'giftcard_length',
|
||||
'giftcard_expiry_years',
|
||||
'locales',
|
||||
'region',
|
||||
'event_team_provisioning',
|
||||
'primary_color',
|
||||
'theme_color_success',
|
||||
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
0
src/pretix/helpers/formats/en_US/__init__.py
Normal file
0
src/pretix/helpers/formats/en_US/__init__.py
Normal file
18
src/pretix/helpers/formats/en_US/formats.py
Normal file
18
src/pretix/helpers/formats/en_US/formats.py
Normal 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'
|
||||
]
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 "Pá"
|
||||
|
||||
#: 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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user