Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
37f48ffd49 Expose logs of all running processes to docker 2020-12-14 13:10:44 +01:00
270 changed files with 80526 additions and 98480 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -135,7 +135,7 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
user=pretix
; Replace with the password you chose above
password=*********
; In most docker setups, 172.17.0.1 is the address of the docker host. Adjust
; In most docker setups, 172.17.0.1 is the address of the docker host. Adjuts
; 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,9 +295,7 @@ 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``). You
can adjust the number of ``gunicorn`` processes with the ``NUM_WORKERS`` environment variable (defaults to
two times the number of CPUs detected).
can invoke the container with ``docker run … pretix/standalone:stable web`` (instead of ``all``).
To run only ``pretix-worker``, you can run ``docker run … pretix/standalone:stable taskworker``. You can
also pass arguments to limit the worker to specific queues or to change the number of concurrent task

View File

@@ -95,12 +95,6 @@ pretix_model_instances
the ``model`` name. Starting with pretix 3.11, these numbers might only be approximate for
most tables when running on PostgreSQL to mitigate performance impact.
pretix_celery_tasks_queued_count
The number of background tasks in the worker queue, labeled with ``queue``.
pretix_celery_tasks_queued_age_seconds
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
.. _Prometheus: https://prometheus.io/
.. _cProfile: https://docs.python.org/3/library/profile.html

View File

@@ -183,9 +183,6 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
constructed from a number of
days before the base point
and the base point.
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
specifiers in requests
(see below).
===================== ============================ ===================================
Query parameters
@@ -230,48 +227,4 @@ We store idempotency keys for 24 hours, so you should never retry a request afte
All ``POST``, ``PUT``, ``PATCH``, or ``DELETE`` api calls support idempotency keys. Adding an idempotency key to a
``GET``, ``HEAD``, or ``OPTIONS`` request has no effect.
File upload
-----------
In some places, the API supports working with files, for example when setting the picture of a product. In this case,
you will first need to make a separate request to our file upload endpoint:
.. sourcecode:: http
POST /api/v1/upload HTTP/1.1
Host: pretix.eu
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
Content-Type: image/png
Content-Disposition: attachment; filename="logo.png"
Content-Length: 1234
<raw file content>
Note that the ``Content-Type`` and ``Content-Disposition`` headers are required. If the upload was successful, you will
receive a JSON response with the ID of the file:
.. sourcecode:: http
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
}
You can then use this file ID in the request you want to use it in. File IDs are currently valid for 24 hours and can only
be used using the same authorization method and user that was used to upload them.
.. sourcecode:: http
PATCH /api/v1/organizers/test/events/test/items/3/ HTTP/1.1
Host: pretix.eu
Content-Type: application/json
{
"picture": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
}
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax

View File

@@ -36,8 +36,8 @@ admission boolean ``true`` for it
(such as primary tickets) and ``false`` for others
(such as add-ons or merchandise).
position integer An integer, used for sorting
picture file A product picture to be displayed in the shop
(can be ``null``).
picture string A product picture to be displayed in the shop
(read-only, can be ``null``).
sales_channels list of strings Sales channels this product is available on, such as
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
available_from datetime The first date time at which this item can be bought

View File

@@ -30,7 +30,6 @@ 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"``.
@@ -168,10 +167,6 @@ 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:
@@ -325,8 +320,7 @@ state string Payment state,
source string How this refund has been created, one of ``buyer``, ``admin``, or ``external``
amount money (string) Payment amount
created datetime Date and time of creation of this payment
comment string Reason for refund (shown to the customer in some cases, can be ``null``).
execution_date datetime Date and time of completion of this refund (or ``null``)
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
===================================== ========================== =======================================================
@@ -378,7 +372,6 @@ 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",
@@ -546,7 +539,6 @@ 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",
@@ -713,8 +705,6 @@ Updating order fields
* ``email``
* ``phone``
* ``checkin_attention``
* ``locale``
@@ -1707,67 +1697,6 @@ Order position ticket download
Manipulating individual positions
---------------------------------
.. versionchanged:: 3.15
The ``PATCH`` method has been added for individual positions.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Updates specific fields on an order position. Currently, only the following fields are supported:
* ``attendee_email``
* ``attendee_name_parts`` or ``attendee_name``
* ``company``
* ``street``
* ``zipcode``
* ``city``
* ``country``
* ``state``
* ``answers``: If specified, you will need to provide **all** answers for this order position.
Validation is handled the same way as when creating orders through the API. You are therefore
expected to provide ``question``, ``answer``, and possibly ``options``. ``question_identifier``
and ``option_identifiers`` will be ignored.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"attendee_email": "other@example.org"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
(Full order resource, see above.)
:param organizer: The ``slug`` field of the organizer of the event
:param event: The ``slug`` field of the event
:param id: The ``id`` field of the order position to update
:statuscode 200: no error
:statuscode 400: The order could not be updated due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Deletes an order position, identified by its internal ID.
@@ -2120,7 +2049,6 @@ Order refund endpoints
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": "2017-12-04T12:13:12Z",
"comment": "Cancellation",
"provider": "banktransfer"
}
]
@@ -2163,7 +2091,6 @@ Order refund endpoints
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": "2017-12-04T12:13:12Z",
"comment": "Cancellation",
"provider": "banktransfer"
}
@@ -2198,7 +2125,6 @@ Order refund endpoints
"amount": "23.00",
"payment": 1,
"execution_date": null,
"comment": "Cancellation",
"provider": "manual",
"mark_canceled": false,
"mark_pending": true
@@ -2220,7 +2146,6 @@ Order refund endpoints
"payment": 1,
"created": "2017-12-01T10:00:00Z",
"execution_date": null,
"comment": "Cancellation",
"provider": "manual"
}

View File

@@ -14,9 +14,7 @@ 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:
.. code-block:: python
``urls.py`` that lives in the ``/control/`` subpath::
from django.conf.urls import url
@@ -46,9 +44,7 @@ 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:
.. code-block:: python
your views::
from pretix.control.permissions import (
event_permission_required, EventPermissionRequiredMixin
@@ -65,9 +61,8 @@ 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
@@ -95,9 +90,7 @@ 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:
.. code-block:: python
special navigation signal::
@receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings')
def navbar_settings(sender, request, **kwargs):
@@ -112,9 +105,7 @@ 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:
.. code-block:: python
for good integration. If you just want to display a form, you could do it like the following::
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
model = Event
@@ -156,9 +147,7 @@ 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``:
.. code-block:: python
``event_url``::
from pretix.multidomain import event_url
@@ -193,9 +182,8 @@ 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
@@ -212,9 +200,7 @@ 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:
.. code-block:: python
class, you can just set the ``permission`` attribute on your viewset::
class MyViewSet(ModelViewSet):
permission = 'can_view_orders'
@@ -222,9 +208,8 @@ class, you can just set the ``permission`` attribute on your viewset:
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
API authentications can be done via user sessions or API tokens and you should therefore check something like the
following:
following::
.. code-block:: python
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'):

View File

@@ -15,9 +15,7 @@ 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:
.. code-block:: python
that we'll provide in this plugin::
from django.dispatch import receiver
@@ -74,9 +72,7 @@ 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:
.. code-block:: python
To use it, you just need to implement some variables::
class ClassicMailRenderer(TemplateBasedMailRenderer):
verbose_name = _('pretix default')

View File

@@ -17,9 +17,7 @@ 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:
.. code-block:: python
that we'll provide in this plugin::
from django.dispatch import receiver
@@ -33,9 +31,7 @@ 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:
.. code-block:: python
and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin::
from django.dispatch import receiver

View File

@@ -15,9 +15,7 @@ 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:
.. code-block:: python
that we'll provide in this plugin::
from django.dispatch import receiver

View File

@@ -19,9 +19,7 @@ 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:
.. code-block:: python
that the plugin will provide::
from django.dispatch import receiver
@@ -106,22 +104,14 @@ The provider class
.. automethod:: payment_control_render
.. automethod:: payment_control_render_short
.. automethod:: payment_refund_supported
.. automethod:: payment_partial_refund_supported
.. automethod:: payment_presale_render
.. automethod:: execute_refund
.. automethod:: refund_control_render
.. automethod:: new_refund_control_form_render
.. automethod:: new_refund_control_form_process
.. automethod:: api_payment_details
.. automethod:: matching_id
@@ -150,9 +140,7 @@ 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:
.. code-block:: python
view which looks roughly like this::
@login_required
def success(request):

View File

@@ -13,9 +13,7 @@ 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``:
.. code-block:: python
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``::
from django.dispatch import receiver
@@ -73,9 +71,7 @@ Helper class for simple placeholders
------------------------------------
pretix ships with a helper class that makes it easy to provide placeholders based on simple
functions:
.. code-block:: python
functions::
placeholder = SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL'

View File

@@ -55,9 +55,7 @@ restricted boolean (optional) ``False`` by default, restricts a plugin
compatibility string Specifier for compatible pretix versions.
================== ==================== ===========================================================
A working example would be:
.. code-block:: python
A working example would be::
try:
from pretix.base.plugins import PluginConfig
@@ -83,7 +81,7 @@ A working example would be:
default_app_config = 'pretix_paypal.PaypalApp'
The ``AppConfig`` class may implement a property ``compatibility_errors``, that checks
The ``AppConfig`` class may implement a property ``compatiblity_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``
@@ -98,9 +96,7 @@ 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:
.. code-block:: python
in a separate python package, your ``setup.py`` should contain something like this::
setup(
args...,
@@ -122,9 +118,7 @@ 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:
.. code-block:: python
method to make your receivers available::
class PaypalApp(AppConfig):
@@ -133,9 +127,7 @@ 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:
.. code-block:: python
in the ``installed`` method::
class PaypalApp(AppConfig):

View File

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

View File

@@ -17,9 +17,7 @@ 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:
.. code-block:: python
that we'll provide in this plugin::
from django.dispatch import receiver

View File

@@ -12,9 +12,7 @@ Implementing a task
-------------------
A common pattern for implementing asynchronous tasks can be seen a lot in ``pretix.base.services``
and looks like this:
.. code-block:: python
and looks like this::
from pretix.celery_app import app
@@ -36,15 +34,13 @@ 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:
.. code-block:: python
A usage example taken directly from the code is::
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 determine the current
In the former case, subsequent GET calls can be used to determinine the current
status of the task.
"""
@@ -83,9 +79,7 @@ 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:
.. code-block:: html
AJAX sending of the form and display a loading indicator::
<form method="post" data-asynctask
action="{% eventurl request.event "presale:event.order.cancel.do" … %}">

View File

@@ -27,9 +27,7 @@ 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:
.. code-block:: python
you to execute a piece of code with a different locale::
with language('de'):
render_mail_template()

View File

@@ -16,9 +16,7 @@ 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:
.. code-block:: python
To actually log an action, you can just call the ``log_action`` method on your object::
order.log_action('pretix.event.order.canceled', user=user, data={})
@@ -31,9 +29,7 @@ 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:
.. code-block:: python
we generally use a custom ``form_valid`` method on our ``FormView`` that looks like this::
@transaction.atomic
def form_valid(self, form):
@@ -44,9 +40,7 @@ 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:
.. code-block:: python
It gets a little bit more complicated if your form allows file uploads::
@transaction.atomic
def form_valid(self, form):
@@ -73,9 +67,7 @@ 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:
.. code-block:: python
implementation could look like::
from django.utils.translation import gettext as _
from pretix.base.signals import logentry_display
@@ -96,9 +88,7 @@ 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:
.. code-block:: python
to register a notification type::
@receiver(register_notification_types)
def register_my_notification_types(sender, **kwargs):
@@ -113,9 +103,7 @@ 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:
.. code-block:: python
A simple implementation could look like this::
class MyNotificationType(NotificationType):
required_permission = "can_view_orders"
@@ -155,9 +143,7 @@ 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:
.. code-block:: python
and displayed later, you can just use Python's ``logging`` module::
import logging
@@ -165,9 +151,7 @@ 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:
.. code-block:: python
This is also very useful to provide debugging information when an exception occurs::
try:
foo()

View File

@@ -15,9 +15,7 @@ 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:
.. code-block:: python
permission level to access a view::
from pretix.control.permissions import (
OrganizerPermissionRequiredMixin, organizer_permission_required
@@ -46,9 +44,7 @@ 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:
.. code-block:: python
Of course, the same is available on event level::
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required
@@ -77,9 +73,7 @@ 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):
.. code-block:: python
(see below for what this means)::
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, administrator_permission_required
@@ -95,9 +89,7 @@ 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:
.. code-block:: python
necessarily have an active admin session::
from pretix.control.permissions import (
StaffMemberRequiredMixin, staff_member_required

View File

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

View File

@@ -64,35 +64,20 @@ is valid in every text):
Placeholder Description
============================== ===============================================================================
event The event name
event_slug The event's short form
code In case of the waiting list, the voucher code to redeem
currency The currency used for the event (three-letter code)
total The order's total value
total_with_currency The order's total value with a localized currency sign
refund_amount (For cancellation emails) The amount of money that will be refunded, including
the currency
currency The currency used for the event (three-letter code)
payment_info Information text specific to the payment method (e.g. banking details)
url An URL pointing to the download/status page of the order
url_info_change An URL pointing to the page of the order that can be used to change ticket
information
url_products_change An URL pointing to the page of the order that can be used to change the products
in the order
url_cancel An URL pointing to the page of the order that can be used to cancel the order
name, name_* Any name that can be used to address the recipient (e.g. name from invoice address,
name from first ticket, …)
invoice_name, invoice_name_* The name field of the invoice address
invoice_name The name field of the invoice address
invoice_company The company field of the invoice address
attendee_name, attendee_name_* The name of the attendee represented by the ticket
expire_date The order's expiration date
comment When rejecting an order, this will contain the reason for the rejection
date The same as ``expire_date``, but in a different e-mail (for backwards
compatibility)
orders A list of orders including links to their status pages, specific to the "resend
link (requested by user)" e-mail
code In case of the waiting list, the voucher code to redeem
hours In case of the waiting list, the number of hours the voucher code is valid
product In case of the waiting list, the product that has become available
voucher_list When sending out vouchers in bulk, this will be replaced with the list of
vouchers
============================== ===============================================================================
The different e-mails are explained in the following:

View File

@@ -304,92 +304,8 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
made through this widget will be counted towards this campaign.
* If you use the tracking plugin, you can enable cross-domain tracking. To do so, you need to initialize the
pretix-widget manually. Use the html code to embed the widget and add one the following code snippets. Make sure to
replace all occurrences of <MEASUREMENT_ID> with your Google Analytics MEASUREMENT_ID (UA-XXXXXXX-X or G-XXXXXXXX)
Please also make sure to add the embedding website to your `Referral exclusions
<https://support.google.com/analytics/answer/2795830>`_ in your Google Analytics settings.
If you use Google Analytics 4 (GA4 G-XXXXXXXX)::
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
<script type="text/javascript">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<MEASUREMENT_ID>');
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if (!window['google_tag_manager']) {
window.PretixWidget.buildWidgets();
return;
}
var clientId;
var sessionId;
var loadingTimeout;
function build() {
// use loadingTimeout to make sure build() is only called once
if (!loadingTimeout) return;
window.clearTimeout(loadingTimeout);
loadingTimeout = null;
if (clientId) window.PretixWidget.widget_data["tracking-ga-id"] = clientId;
if (sessionId) window.PretixWidget.widget_data["tracking-ga-sessid"] = sessionId;
window.PretixWidget.buildWidgets();
};
// make sure to build pretix-widgets if gtag fails to load either client_id or session_id
loadingTimeout = window.setTimeout(build, 2000);
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
clientId = id;
if (sessionId !== undefined) build();
});
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
sessionId = id;
if (clientId !== undefined) build();
});
});
};
</script>
If you use Universal Analytics with ``gtag.js`` (UA-XXXXXXX-X)::
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
<script type="text/javascript">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<MEASUREMENT_ID>');
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if (!window['google_tag_manager']) {
window.PretixWidget.buildWidgets();
return;
}
// make sure to build pretix-widgets if gtag fails to load client_id
var loadingTimeout = window.setTimeout(function() {
loadingTimeout = null;
window.PretixWidget.buildWidgets();
}, 1000);
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
if (loadingTimeout) {
window.clearTimeout(loadingTimeout);
window.PretixWidget.widget_data["tracking-ga-id"] = id;
window.PretixWidget.buildWidgets();
}
});
});
};
</script>
If you use ```analytics.js` (Universal Analytics)::
* If you use the tracking plugin, you can pass a Google Analytics User ID to enable cross-domain tracking. This will
require you to dynamically load the widget, like this::
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
@@ -397,33 +313,27 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '<MEASUREMENT_ID>', 'auto');
ga('create', 'UA-XXXXXX-1', 'auto');
ga('send', 'pageview');
window.pretixWidgetCallback = function () {
window.PretixWidget.build_widgets = false;
window.addEventListener('load', function() { // Wait for GA to be loaded
if (!window['ga'] || !ga.create) {
// Tracking is probably blocked
window.PretixWidget.buildWidgets()
return;
}
var loadingTimeout = window.setTimeout(function() {
loadingTimeout = null;
window.PretixWidget.buildWidgets();
}, 1000);
ga(function(tracker) {
if (loadingTimeout) {
window.clearTimeout(loadingTimeout);
if(window.ga && ga.create) {
ga(function(tracker) {
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
window.PretixWidget.buildWidgets();
}
});
window.PretixWidget.buildWidgets()
});
} else { // Tracking is probably blocked
window.PretixWidget.buildWidgets()
}
});
};
</script>
In some combinations with Google Tag Manager, the widget does not load this way. In this case, try replacing
``tracker.get('clientId')`` with ``ga.getAll()[0].get('clientId')``.
.. versionchanged:: 2.3

View File

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

View File

@@ -42,7 +42,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:order-list'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
@@ -69,7 +68,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
@@ -115,7 +113,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'plugins:pretix_seating:event.event.subevent'),
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
('POST', 'api-v1:upload'),
)

View File

@@ -89,38 +89,10 @@ class EventCRUDPermission(EventPermission):
class ProfilePermission(BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
if not request.user.is_authenticated:
return False
if request.user.is_authenticated:
try:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
assert_session_valid(request)
except SessionInvalid:
return False
except SessionReauthRequired:
return False
if isinstance(request.auth, OAuthAccessToken):
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
return False
return True
class AnyAuthenticatedClientPermission(BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
return False
if request.user.is_authenticated:
try:
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
assert_session_valid(request)
except SessionInvalid:
return False
except SessionReauthRequired:
return False
return True

View File

@@ -1,6 +1,5 @@
from datetime import timedelta
from django.core.files import File
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
@@ -101,15 +100,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
for answ_data in answers_data:
options = answ_data.pop('options')
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = cp.answers.create(**answ_data, answer='')
answ.file.save(an.name, an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
else:
answ = cp.answers.create(**answ_data)
answ.options.add(*options)
answ = cp.answers.create(**answ_data)
answ.options.add(*options)
return cp
def validate_cart_id(self, cid):

View File

@@ -1,29 +1,25 @@
import logging
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from django_countries.serializers import CountryFieldMixin
from hierarkey.proxy import HierarkeyProxy
from pytz import common_timezones
from rest_framework import serializers
from rest_framework.fields import ChoiceField, Field
from rest_framework.relations import SlugRelatedField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Event, TaxRule
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
SeatProtected, generate_seats, validate_plan_change,
)
from pretix.base.settings import validate_event_settings
from pretix.base.settings import DEFAULTS, validate_event_settings
from pretix.base.signals import api_event_settings_fields
logger = logging.getLogger(__name__)
class MetaDataField(Field):
@@ -562,7 +558,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
class EventSettingsSerializer(SettingsSerializer):
class EventSettingsSerializer(serializers.Serializer):
default_fields = [
'imprint_url',
'checkout_email_helptext',
@@ -578,7 +574,6 @@ class EventSettingsSerializer(SettingsSerializer):
'presale_start_show_date',
'locales',
'locale',
'region',
'last_order_modification_date',
'show_quota_left',
'waiting_list_enabled',
@@ -605,9 +600,6 @@ class EventSettingsSerializer(SettingsSerializer):
'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',
@@ -658,7 +650,6 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_additional_text',
'invoice_footer_text',
'invoice_eu_currencies',
'invoice_logo_image',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_paid',
@@ -668,7 +659,6 @@ class EventSettingsSerializer(SettingsSerializer):
'cancel_allow_user_paid_keep_percentage',
'cancel_allow_user_paid_adjust_fees',
'cancel_allow_user_paid_adjust_fees_explanation',
'cancel_allow_user_paid_adjust_fees_step',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'change_allow_user_variation',
@@ -680,21 +670,45 @@ class EventSettingsSerializer(SettingsSerializer):
'theme_color_background',
'theme_round_borders',
'primary_font',
'logo_image',
'logo_image_large',
'logo_show_title',
'og_image',
]
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
self.changed_data = []
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(form_kwargs):
form_kwargs = form_kwargs()
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
**kwargs
)
f._label = form_kwargs.get('label', fname)
f._help_text = form_kwargs.get('help_text')
self.fields[fname] = f
for recv, resp in api_event_settings_fields.send(sender=self.event):
for fname, field in resp.items():
field.required = False
self.fields[fname] = field
def update(self, instance: HierarkeyProxy, validated_data):
for attr, value in validated_data.items():
if value is None:
instance.delete(attr)
self.changed_data.append(attr)
elif instance.get(attr, as_type=type(value)) != value:
instance.set(attr, value)
self.changed_data.append(attr)
return instance
def validate(self, data):
data = super().validate(data)
settings_dict = self.instance.freeze()
@@ -702,14 +716,6 @@ class EventSettingsSerializer(SettingsSerializer):
validate_event_settings(self.event, settings_dict)
return data
def get_new_filename(self, name: str) -> str:
nonce = get_random_string(length=8)
fname = '%s/%s/%s.%s.%s' % (
self.event.organizer.slug, self.event.slug, name.split('/')[-1], nonce, name.split('.')[-1]
)
# TODO: make sure pub is always correct
return 'pub/' + fname
class DeviceEventSettingsSerializer(EventSettingsSerializer):
default_fields = [

View File

@@ -1,6 +1,5 @@
from collections import OrderedDict
from django.core.exceptions import ValidationError
from rest_framework import serializers
@@ -28,50 +27,3 @@ class ListMultipleChoiceField(serializers.MultipleChoiceField):
]
return remove_duplicates_from_list(representation_data)
class UploadedFileField(serializers.Field):
default_error_messages = {
'required': 'No file was submitted.',
'not_found': 'The submitted file ID was not found.',
'invalid_type': 'The submitted file has a file type that is not allowed in this field.',
'size': 'The submitted file is too large to be used in this field.',
}
def __init__(self, *args, **kwargs):
self.allowed_types = kwargs.pop('allowed_types', None)
self.max_size = kwargs.pop('max_size', None)
super().__init__(*args, **kwargs)
def to_internal_value(self, data):
from pretix.base.models import CachedFile
request = self.context.get('request', None)
try:
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}',
file__isnull=False,
pk=data[len("file:"):],
)
except (ValidationError, IndexError): # invalid uuid
self.fail('not_found')
except CachedFile.DoesNotExist:
self.fail('not_found')
if self.allowed_types and cf.type not in self.allowed_types:
self.fail('invalid_type')
if self.max_size and cf.file.size > self.max_size:
self.fail('size')
return cf.file
def to_representation(self, value):
if not value:
return None
try:
url = value.url
except AttributeError:
return None
request = self.context['request']
return request.build_absolute_uri(url)

View File

@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
@@ -114,9 +113,6 @@ class ItemSerializer(I18nAwareModelSerializer):
variations = InlineItemVariationSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
'image/png', 'image/jpeg', 'image/gif'
), max_size=10 * 1024 * 1024)
class Meta:
model = Item
@@ -127,7 +123,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
read_only_fields = ('has_variations',)
read_only_fields = ('has_variations', 'picture')
def validate(self, data):
data = super().validate(data)

View File

@@ -3,7 +3,6 @@ from collections import Counter, defaultdict
from decimal import Decimal
import pycountry
from django.core.files import File
from django.db.models import F, Q
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
@@ -18,9 +17,8 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, Invoice, InvoiceAddress, InvoiceLine, Item,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer, Seat,
SubEvent, TaxRule, Voucher,
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
@@ -96,9 +94,12 @@ class AnswerQuestionIdentifierField(serializers.Field):
class AnswerQuestionOptionsIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
if isinstance(instance, WrappedModel) or instance.pk:
return [o.identifier for o in instance.options.all()]
return []
return [o.identifier for o in instance.options.all()]
class AnswerQuestionOptionsField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.pk for o in instance.options.all()]
class InlineSeatSerializer(I18nAwareModelSerializer):
@@ -111,91 +112,12 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
options = AnswerQuestionOptionsField(source='*', read_only=True)
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
def validate_question(self, q):
if q.event != self.context['event']:
raise ValidationError(
'The specified question does not belong to this event.'
)
return q
def _handle_file_upload(self, data):
try:
ao = self.context["request"].user or self.context["request"].auth
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(ao))}-{ao.pk}',
file__isnull=False,
pk=data['answer'][len("file:"):],
)
except (ValidationError, IndexError): # invalid uuid
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
except CachedFile.DoesNotExist:
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
allowed_types = (
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > 10 * 1024 * 1024:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
data['options'] = []
data['answer'] = cf.file
return data
def validate(self, data):
if data.get('question').type == Question.TYPE_FILE:
return self._handle_file_upload(data)
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
if not data.get('options'):
raise ValidationError(
'You need to specify options if the question is of a choice type.'
)
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
raise ValidationError(
'You can specify at most one option for this question.'
)
for o in data.get('options'):
if o.question_id != data.get('question').pk:
raise ValidationError(
'The specified option does not belong to this question.'
)
data['answer'] = ", ".join([str(o) for o in data.get('options')])
else:
if data.get('options'):
raise ValidationError(
'You should not specify options if the question is not of a choice type.'
)
if data.get('question').type == Question.TYPE_BOOLEAN:
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
data['answer'] = 'True'
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
data['answer'] = 'False'
else:
raise ValidationError(
'Please specify "true" or "false" for boolean questions.'
)
elif data.get('question').type == Question.TYPE_NUMBER:
serializers.DecimalField(
max_digits=50,
decimal_places=25
).to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATE:
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_TIME:
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATETIME:
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
return data
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
@@ -258,7 +180,7 @@ class PdfDataSerializer(serializers.Field):
res = {}
ev = instance.subevent or instance.order.event
with language(instance.order.locale, instance.order.event.settings.region):
with language(instance.order.locale):
# This needs to have some extra performance improvements to avoid creating hundreds of queries when
# we serialize a list.
@@ -283,14 +205,13 @@ class PdfDataSerializer(serializers.Field):
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True)
checkins = CheckinSerializer(many=True)
answers = AnswerSerializer(many=True)
downloads = PositionDownloadsField(source='*', read_only=True)
downloads = PositionDownloadsField(source='*')
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
pdf_data = PdfDataSerializer(source='*', read_only=True)
pdf_data = PdfDataSerializer(source='*')
seat = InlineSeatSerializer(read_only=True)
country = CompatibleCountryField(source='*')
attendee_name = serializers.CharField(required=False)
class Meta:
model = OrderPosition
@@ -298,99 +219,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
'seat', 'canceled'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
self.fields.pop('pdf_data')
def validate(self, data):
if data.get('attendee_name') and data.get('attendee_name_parts'):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country').code):
raise ValidationError(
{'country': ['Invalid country code.']}
)
if data.get('state'):
cc = str(data.get('country') or self.instance.country or '')
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
raise ValidationError(
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
return data
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 = [
'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
'state', 'attendee_email',
]
answers_data = validated_data.pop('answers', None)
name = validated_data.pop('attendee_name', '')
if name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': name
}
for attr, value in validated_data.items():
if attr in update_fields:
setattr(instance, attr, value)
instance.save(update_fields=update_fields)
if answers_data is not None:
qs_seen = set()
answercache = {
a.question_id: a for a in instance.answers.all()
}
for answ_data in answers_data:
options = answ_data.pop('options', [])
if answ_data['question'].pk in qs_seen:
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
if answ_data['question'].pk in answercache:
a = answercache[answ_data['question'].pk]
if isinstance(answ_data['answer'], File):
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
a.answer = 'file://' + a.file.name
else:
for attr, value in answ_data.items():
setattr(a, attr, value)
a.save()
else:
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
a = instance.answers.create(**answ_data, answer='')
a.file.save(an.name, an, save=False)
a.answer = 'file://' + a.file.name
a.save()
else:
a = instance.answers.create(**answ_data)
a.options.set(options)
qs_seen.add(a.question_id)
for qid, a in answercache.items():
if qid not in qs_seen:
a.delete()
return instance
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
@@ -502,7 +336,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderRefund
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'comment', 'provider')
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
class OrderURLField(serializers.URLField):
@@ -527,7 +361,7 @@ class OrderSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = (
'code', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'code', 'status', 'testmode', 'secret', 'email', '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'
@@ -559,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', 'phone']
update_fields = ['comment', 'checkin_attention', 'email', 'locale']
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -591,17 +425,7 @@ class OrderSerializer(I18nAwareModelSerializer):
return instance
class AnswerQuestionOptionsField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.pk for o in instance.options.all()]
class SimulatedAnswerSerializer(AnswerSerializer):
options = AnswerQuestionOptionsField(read_only=True, source='*')
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
answers = SimulatedAnswerSerializer(many=True)
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
@@ -628,8 +452,62 @@ class PriceCalcSerializer(serializers.Serializer):
del self.fields['subevent']
class AnswerCreateSerializer(AnswerSerializer):
pass
class AnswerCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'options')
def validate_question(self, q):
if q.event != self.context['event']:
raise ValidationError(
'The specified question does not belong to this event.'
)
return q
def validate(self, data):
if data.get('question').type == Question.TYPE_FILE:
raise ValidationError(
'File uploads are currently not supported via the API.'
)
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
if not data.get('options'):
raise ValidationError(
'You need to specify options if the question is of a choice type.'
)
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
raise ValidationError(
'You can specify at most one option for this question.'
)
data['answer'] = ", ".join([str(o) for o in data.get('options')])
else:
if data.get('options'):
raise ValidationError(
'You should not specify options if the question is not of a choice type.'
)
if data.get('question').type == Question.TYPE_BOOLEAN:
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
data['answer'] = 'True'
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
data['answer'] = 'False'
else:
raise ValidationError(
'Please specify "true" or "false" for boolean questions.'
)
elif data.get('question').type == Question.TYPE_NUMBER:
serializers.DecimalField(
max_digits=50,
decimal_places=25
).to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATE:
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_TIME:
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATETIME:
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
return data
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
@@ -813,7 +691,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_email', 'simulate')
@@ -1166,16 +1044,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.save()
for answ_data in answers_data:
options = answ_data.pop('options', [])
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = pos.answers.create(**answ_data, answer='')
answ.file.save(an.name, an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
else:
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
pos_map[pos.positionid] = pos
if not simulate:
@@ -1324,7 +1194,7 @@ class OrderRefundCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderRefund
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info', 'comment')
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info')
def create(self, validated_data):
pid = validated_data.pop('payment', None)

View File

@@ -1,28 +1,23 @@
import logging
from decimal import Decimal
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django.utils.translation import get_language, gettext_lazy as _
from hierarkey.proxy import HierarkeyProxy
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.api.serializers.settings import SettingsSerializer
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,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail
from pretix.base.settings import validate_organizer_settings
from pretix.base.settings import DEFAULTS, validate_organizer_settings
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
class OrganizerSerializer(I18nAwareModelSerializer):
class Meta:
@@ -150,7 +145,7 @@ class TeamInviteSerializer(serializers.ModelSerializer):
})
},
event=None,
locale=get_language_without_region() # TODO: expose?
locale=get_language() # TODO: expose?
)
except SendMailException:
pass # Already logged
@@ -211,7 +206,7 @@ class TeamMemberSerializer(serializers.ModelSerializer):
)
class OrganizerSettingsSerializer(SettingsSerializer):
class OrganizerSettingsSerializer(serializers.Serializer):
default_fields = [
'organizer_info_text',
'event_list_type',
@@ -222,20 +217,46 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'giftcard_length',
'giftcard_expiry_years',
'locales',
'region',
'event_team_provisioning',
'primary_color',
'theme_color_success',
'theme_color_danger',
'theme_color_background',
'theme_round_borders',
'primary_font',
'organizer_logo_image'
'primary_font'
]
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
self.changed_data = []
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(form_kwargs):
form_kwargs = form_kwargs()
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
**kwargs
)
f._label = form_kwargs.get('label', fname)
f._help_text = form_kwargs.get('help_text')
self.fields[fname] = f
def update(self, instance: HierarkeyProxy, validated_data):
for attr, value in validated_data.items():
if value is None:
instance.delete(attr)
self.changed_data.append(attr)
elif instance.get(attr, as_type=type(value)) != value:
instance.set(attr, value)
self.changed_data.append(attr)
return instance
def validate(self, data):
data = super().validate(data)
@@ -243,11 +264,3 @@ class OrganizerSettingsSerializer(SettingsSerializer):
settings_dict.update(data)
validate_organizer_settings(self.organizer, settings_dict)
return data
def get_new_filename(self, name: str) -> str:
nonce = get_random_string(length=8)
fname = '%s/%s.%s.%s' % (
self.organizer.slug, name.split('/')[-1], nonce, name.split('.')[-1]
)
# TODO: make sure pub is always correct
return 'pub/' + fname

View File

@@ -1,77 +0,0 @@
import logging
from django.core.files import File
from django.core.files.storage import default_storage
from django.db.models.fields.files import FieldFile
from hierarkey.proxy import HierarkeyProxy
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.fields import UploadedFileField
from pretix.base.settings import DEFAULTS
logger = logging.getLogger(__name__)
class SettingsSerializer(serializers.Serializer):
default_fields = []
def __init__(self, *args, **kwargs):
self.changed_data = []
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(form_kwargs):
form_kwargs = form_kwargs()
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
**kwargs
)
f._label = form_kwargs.get('label', fname)
f._help_text = form_kwargs.get('help_text')
f.parent = self
self.fields[fname] = f
def update(self, instance: HierarkeyProxy, validated_data):
for attr, value in validated_data.items():
if isinstance(value, FieldFile):
# Delete old file
fname = instance.get(attr, as_type=File)
if fname:
try:
default_storage.delete(fname.name)
except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fname.name)
# Create new file
newname = default_storage.save(self.get_new_filename(value.name), value)
instance.set(attr, File(file=value, name=newname))
self.changed_data.append(attr)
elif isinstance(self.fields[attr], UploadedFileField):
if value is None:
fname = instance.get(attr, as_type=File)
if fname:
try:
default_storage.delete(fname.name)
except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fname.name)
instance.delete(attr)
else:
# file is unchanged
continue
elif value is None:
instance.delete(attr)
self.changed_data.append(attr)
elif instance.get(attr, as_type=type(value)) != value:
instance.set(attr, value)
self.changed_data.append(attr)
return instance
def get_new_filename(self, name: str) -> str:
raise NotImplementedError()

View File

@@ -7,8 +7,8 @@ from rest_framework import routers
from pretix.api.views import cart
from .views import (
checkin, device, event, exporters, item, oauth, order, organizer, upload,
user, version, voucher, waitinglist, webhooks,
checkin, device, event, exporters, item, oauth, order, organizer, user,
version, voucher, waitinglist, webhooks,
)
router = routers.DefaultRouter()
@@ -95,7 +95,6 @@ urlpatterns = [
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
url(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
url(r"^upload$", upload.UploadView.as_view(), name="upload"),
url(r"^me$", user.MeView.as_view(), name="user.me"),
url(r"^version$", version.VersionView.as_view(), name="version"),
]

View File

@@ -22,7 +22,7 @@ from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, CheckinList, Event, Order, OrderPosition, Question,
Checkin, CheckinList, Event, Order, OrderPosition,
)
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin,
@@ -302,10 +302,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
for q in op.item.questions.filter(ask_during_checkin=True):
if str(q.pk) in aws:
try:
if q.type == Question.TYPE_FILE:
given_answers[q] = self._handle_file_upload(aws[str(q.pk)])
else:
given_answers[q] = q.clean_answer(aws[str(q.pk)])
given_answers[q] = q.clean_answer(aws[str(q.pk)])
except ValidationError:
pass
@@ -355,25 +352,3 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def _handle_file_upload(self, data):
try:
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}',
file__isnull=False,
pk=data[len("file:"):],
)
except (ValidationError, IndexError): # invalid uuid
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
except CachedFile.DoesNotExist:
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
allowed_types = (
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > 10 * 1024 * 1024:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
return cf.file

View File

@@ -365,13 +365,9 @@ class EventSettingsView(views.APIView):
def get(self, request, *args, **kwargs):
if isinstance(request.auth, Device):
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
'request': request
})
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
elif 'can_change_event_settings' in request.eventpermset:
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
'request': request
})
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
else:
raise PermissionDenied()
if 'explain' in request.GET:
@@ -386,7 +382,7 @@ class EventSettingsView(views.APIView):
def patch(self, request, *wargs, **kwargs):
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
event=request.event, context={'request': request})
event=request.event)
s.is_valid(raise_exception=True)
with transaction.atomic():
s.save()
@@ -396,9 +392,6 @@ class EventSettingsView(views.APIView):
}
)
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
regenerate_css.apply_async(args=(request.event.pk,))
s = EventSettingsSerializer(
instance=request.event.settings, event=request.event, context={
'request': request
})
regenerate_css.apply_async(args=(request.organizer.pk,))
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
return Response(s.data)

View File

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

View File

@@ -582,7 +582,7 @@ class OrderViewSet(viewsets.ModelViewSet):
auth=request.auth,
)
with language(order.locale, self.request.event.settings.region):
with language(order.locale):
order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order)
@@ -674,17 +674,6 @@ 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',
@@ -763,7 +752,7 @@ with scopes_disabled():
}
class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -783,11 +772,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
},
}
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def get_queryset(self):
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
qs = OrderPosition.all
@@ -902,7 +886,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
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, self.request.event.settings.region):
with language(data.get('locale') or self.request.event.settings.locale):
return Response({
'gross': price.gross,
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
@@ -956,44 +940,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
except Quota.QuotaExceededException as e:
raise ValidationError(str(e))
def update(self, request, *args, **kwargs):
partial = kwargs.get('partial', False)
if not partial:
return Response(
{"detail": "Method \"PUT\" not allowed."},
status=status.HTTP_405_METHOD_NOT_ALLOWED,
)
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
with transaction.atomic():
old_data = self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data
serializer.save()
new_data = serializer.data
if old_data != new_data:
log_data = self.request.data
if 'answers' in log_data:
for a in new_data['answers']:
log_data[f'question_{a["question"]}'] = a["answer"]
log_data.pop('answers', None)
serializer.instance.order.log_action(
'pretix.event.order.modified',
user=self.request.user,
auth=self.request.auth,
data={
'data': [
dict(
position=serializer.instance.pk,
**log_data
)
]
}
)
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPaymentSerializer

View File

@@ -425,9 +425,7 @@ class OrganizerSettingsView(views.APIView):
permission = 'can_change_organizer_settings'
def get(self, request, *args, **kwargs):
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
'request': request
})
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
if 'explain' in request.GET:
return Response({
fname: {
@@ -441,9 +439,7 @@ class OrganizerSettingsView(views.APIView):
def patch(self, request, *wargs, **kwargs):
s = OrganizerSettingsSerializer(
instance=request.organizer.settings, data=request.data, partial=True,
organizer=request.organizer, context={
'request': request
}
organizer=request.organizer
)
s.is_valid(raise_exception=True)
with transaction.atomic():
@@ -455,7 +451,5 @@ class OrganizerSettingsView(views.APIView):
)
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
'request': request
})
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
return Response(s.data)

View File

@@ -1,55 +0,0 @@
import datetime
from django.utils.timezone import now
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import ValidationError
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response
from rest_framework.views import APIView
from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
from pretix.api.auth.token import TeamTokenAuthentication
from pretix.base.models import CachedFile
ALLOWED_TYPES = {
'image/gif': {'.gif'},
'image/jpeg': {'.jpg', '.jpeg'},
'image/png': {'.png'},
'application/pdf': {'.pdf'},
}
class UploadView(APIView):
authentication_classes = (
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
)
parser_classes = [FileUploadParser]
permission_classes = [AnyAuthenticatedClientPermission]
def post(self, request):
if 'file' not in request.data:
raise ValidationError('No file has been submitted.')
file_obj = request.data['file']
content_type = file_obj.content_type.split(";")[0] # ignore e.g. "; charset=…"
if content_type not in ALLOWED_TYPES:
raise ValidationError('Content type "{type}" is not allowed'.format(type=content_type))
if not any(file_obj.name.endswith(ext) for ext in ALLOWED_TYPES[content_type]):
raise ValidationError('File name "{name}" has an invalid extension for type "{type}"'.format(
name=file_obj.name,
type=content_type
))
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
web_download=False,
filename=file_obj.name,
type=content_type,
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}'
)
cf.file.save(file_obj.name, file_obj)
cf.save()
return Response({
'id': f'file:{cf.pk}'
}, status=201)

View File

@@ -6,7 +6,6 @@ from rest_framework.views import APIView
from pretix import __version__
from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
from pretix.api.auth.token import TeamTokenAuthentication
@@ -49,7 +48,6 @@ class VersionView(APIView):
authentication_classes = (
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
)
permission_classes = [AnyAuthenticatedClientPermission]
def get(self, request, format=None):
return Response({

View File

@@ -115,7 +115,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
'body': body_md,
'subject': str(subject),
'color': settings.PRETIX_PRIMARY_COLOR,
'rtl': get_language() in settings.LANGUAGES_RTL or get_language().split('-')[0] in settings.LANGUAGES_RTL,
'rtl': get_language() in settings.LANGUAGES_RTL
}
if self.event:
htmlctx['event'] = self.event
@@ -427,30 +427,28 @@ def base_placeholders(sender, **kwargs):
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
'secret': order.secret
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret'],
'hash': order['hash'],
'secret': order['secret']
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
]
),
),

View File

@@ -59,12 +59,6 @@ class OrderListExporter(MultiSheetListExporter):
initial=False,
required=False
)),
('group_multiple_choice',
forms.BooleanField(
label=_('Show multiple choice answers grouped in one column'),
initial=False,
required=False
)),
]
)
@@ -145,7 +139,7 @@ class OrderListExporter(MultiSheetListExporter):
tax_rates = self._get_all_tax_rates(qs)
headers = [
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'), _('Order date'),
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Order time'), _('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
@@ -221,7 +215,6 @@ 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'),
]
@@ -310,7 +303,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Order code'),
_('Status'),
_('Email'),
_('Phone number'),
_('Order date'),
_('Order time'),
_('Fee type'),
@@ -342,7 +334,6 @@ 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(),
@@ -411,7 +402,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Position ID'),
_('Status'),
_('Email'),
_('Phone number'),
_('Order date'),
_('Order time'),
]
@@ -455,14 +445,9 @@ class OrderListExporter(MultiSheetListExporter):
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
options[q.pk] = []
if form_data['group_multiple_choice']:
for o in q.options.all():
options[q.pk].append(o)
headers.append(str(q.question))
else:
for o in q.options.all():
headers.append(str(q.question) + ' ' + str(o.answer))
options[q.pk].append(o)
for o in q.options.all():
headers.append(str(q.question) + ' ' + str(o.answer))
options[q.pk].append(o)
else:
headers.append(str(q.question))
headers += [
@@ -496,7 +481,6 @@ 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'),
]
@@ -562,11 +546,8 @@ class OrderListExporter(MultiSheetListExporter):
acache[a.question_id] = str(a)
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
if form_data['group_multiple_choice']:
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
else:
for o in options[q.pk]:
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
for o in options[q.pk]:
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
else:
row.append(acache.get(q.pk, ''))
@@ -652,7 +633,7 @@ class PaymentListExporter(ListExporter):
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method'), _('Comment')
_('Status code'), _('Amount'), _('Payment method')
]
yield headers
@@ -674,8 +655,7 @@ class PaymentListExporter(ListExporter):
obj.get_state_display(),
obj.state,
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
provider_names.get(obj.provider, obj.provider),
obj.comment if isinstance(obj, OrderRefund) else "",
provider_names.get(obj.provider, obj.provider)
]
yield row

View File

@@ -1,17 +1,12 @@
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):
@@ -23,7 +18,6 @@ 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.")
}
@@ -45,36 +39,10 @@ class LoginForm(forms.Form):
else:
move_to_end(self.fields, 'keep_logged_in')
@cached_property
def ratelimit_key(self):
if not settings.HAS_REDIS:
return None
client_ip = get_client_ip(self.request)
if not client_ip:
return None
try:
client_ip = ipaddress.ip_address(client_ip)
except ValueError:
# Web server not set up correctly
return None
if client_ip.is_private:
# This is the private IP of the server, web server not set up correctly
return None
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
def clean(self):
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
if self.ratelimit_key:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.get(self.ratelimit_key)
if cnt and int(cnt) > 10:
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
if self.user_cache is None:
if self.ratelimit_key:
rc.incr(self.ratelimit_key)
rc.expire(self.ratelimit_key, 300)
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login'

View File

@@ -9,34 +9,33 @@ import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
from babel import Locale
from babel import localedata
from django import forms
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import QuerySet
from django.forms import Select
from django.utils import translation
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 gettext_lazy as _, pgettext_lazy
from django.utils.translation import (
get_language, 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, national_significant_number
from phonenumbers import NumberParseException
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 (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.i18n import language
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import (
EU_COUNTRIES, cc_to_vat_prefix, is_eu_country,
@@ -205,47 +204,7 @@ class NamePartsFormField(forms.MultiValueField):
return value
class WrappedPhonePrefixSelect(Select):
initial = None
def __init__(self, initial=None):
choices = [("", "---------")]
language = get_babel_locale() # changed from default implementation that used the django locale
locale = Locale(translation.to_locale(language))
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
prefix = "+%d" % prefix
if initial and initial in values:
self.initial = prefix
for country_code in values:
country_name = locale.territories.get(country_code)
if country_name:
choices.append((prefix, "{} {}".format(country_name, prefix)))
super().__init__(choices=sorted(choices, key=lambda item: item[1]))
def render(self, name, value, *args, **kwargs):
return super().render(name, value or self.initial, *args, **kwargs)
def get_context(self, name, value, attrs):
if value and self.choices[1][0] != value:
matching_choices = len([1 for p, c in self.choices if p == value])
if matching_choices > 1:
# Some countries share a phone prefix, for example +1 is used all over the Americas.
# This causes a UX problem: If the default value or the existing data is +12125552368,
# the widget will just show the first <option> entry with value="+1" as selected,
# which alphabetically is America Samoa, although most numbers statistically are from
# the US. As a workaround, we detect this case and add an aditional choice value with
# just <option value="+1">+1</option> without an explicit country.
self.choices.insert(1, (value, value))
context = super().get_context(name, value, attrs)
return context
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))
@@ -253,44 +212,12 @@ 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_without_region()
country = event.settings.region or event.settings.invoice_address_from_country
locale = get_language()
country = event.settings.invoice_address_from_country
if not country:
valid_countries = countries.countries
if '-' in locale:
@@ -605,7 +532,13 @@ class BaseQuestionsForm(forms.Form):
if q.valid_datetime_max:
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
elif q.type == Question.TYPE_PHONENUMBER:
with language(get_babel_locale()):
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):
default_country = guess_country(event)
default_prefix = None
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
@@ -690,9 +623,8 @@ class BaseQuestionsForm(forms.Form):
if not self.all_optional:
for q in question_cache.values():
answer = d.get('question_%d' % q.pk)
field = self['question_%d' % q.pk]
if question_is_required(q) and not answer and answer != 0 and not field.errors:
raise ValidationError({'question_%d' % q.pk: [_('This field is required.')]})
if question_is_required(q) and not answer and answer != 0:
raise ValidationError({'question_%d' % q.pk: [_('This field is required')]})
return d

View File

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

View File

@@ -18,13 +18,10 @@ class DatePickerWidget(forms.DateInput):
date_attrs['class'] += ' datepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
def placeholder():
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
date_attrs['placeholder'] = lazy(placeholder, str)
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
forms.DateInput.__init__(self, date_attrs, date_format)
@@ -39,13 +36,10 @@ class TimePickerWidget(forms.TimeInput):
time_attrs['class'] += ' timepickerfield'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
def placeholder():
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
time_attrs['placeholder'] = lazy(placeholder, str)
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(tf)
forms.TimeInput.__init__(self, time_attrs, time_format)

View File

@@ -1,6 +1,5 @@
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
@@ -67,52 +66,10 @@ 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, 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.
"""
def language(lng):
_lng = translation.get_language()
lng = lng or settings.LANGUAGE_CODE
if '-' not in lng and region:
lng += '-' + region.lower()
translation.activate(lng)
translation.activate(lng or settings.LANGUAGE_CODE)
try:
yield
finally:

View File

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

View File

@@ -1,6 +1,4 @@
import json
import math
import time
from collections import defaultdict
from django.apps import apps
@@ -8,7 +6,6 @@ from django.conf import settings
from django.db import connection
from pretix.base.models import Event, Invoice, Order, OrderPosition, Organizer
from pretix.celery_app import app
if settings.HAS_REDIS:
import django_redis
@@ -251,19 +248,6 @@ def metric_values():
else:
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
if settings.HAS_CELERY:
client = app.broker_connection().channel().client
for q in settings.CELERY_TASK_QUEUES:
llen = client.llen(q.name)
lfirst = client.lindex(q.name, -1)
metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = llen
if lfirst:
ldata = json.loads(lfirst)
dt = time.time() - ldata.get('created', 0)
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = dt
else:
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = 0
return metrics

View File

@@ -15,8 +15,7 @@ from django.utils.translation.trans_real import (
parse_accept_lang_header,
)
from pretix.base.i18n import get_language_without_region
from pretix.base.settings import global_settings_object
from pretix.base.settings import GlobalSettingsObject
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
@@ -36,30 +35,19 @@ 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 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
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
translation.activate(language)
request.LANGUAGE_CODE = get_language_without_region()
request.LANGUAGE_CODE = translation.get_language()
tzname = None
if hasattr(request, 'event'):
@@ -204,7 +192,7 @@ class SecurityMiddleware(MiddlewareMixin):
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
img_src = []
gs = global_settings_object(request)
gs = GlobalSettingsObject()
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
@@ -228,8 +216,6 @@ class SecurityMiddleware(MiddlewareMixin):
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
if settings.CSP_ADDITIONAL_HEADER:
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
staticdomain = "'self'"
dynamicdomain = "'self'"

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.0.11 on 2021-01-15 09:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0174_merge_20201222_1031'),
]
operations = [
migrations.AddField(
model_name='orderrefund',
name='comment',
field=models.TextField(null=True),
),
]

View File

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

View File

@@ -10,9 +10,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import (
MaxValueValidator, MinLengthValidator, MinValueValidator, RegexValidator,
)
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
from django.template.defaultfilters import date as _date
@@ -355,11 +353,8 @@ class Event(EventMixin, LoggedModel):
"remembered, but you can also choose to use a random value. "
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
validators=[
MinLengthValidator(
limit_value=2,
),
RegexValidator(
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$",
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
message=_("The slug may only contain letters, numbers, dots and dashes."),
),
EventSlugBanlistValidator()
@@ -398,18 +393,10 @@ class Event(EventMixin, LoggedModel):
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
validators=[
MinValueValidator(-90),
MaxValueValidator(90),
]
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True,
validators=[
MinValueValidator(-180),
MaxValueValidator(180),
]
)
plugins = models.TextField(
null=False, blank=True,
@@ -541,7 +528,7 @@ class Event(EventMixin, LoggedModel):
return locking.LockManager(self)
def get_mail_backend(self, timeout=None, force_custom=False):
def get_mail_backend(self, 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.
@@ -555,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, timeout=timeout)
fail_silently=False)
else:
return get_connection(fail_silently=False)
@@ -1134,18 +1121,10 @@ class SubEvent(EventMixin, LoggedModel):
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
validators=[
MinValueValidator(-90),
MaxValueValidator(90),
]
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True,
validators=[
MinValueValidator(-180),
MaxValueValidator(180),
]
null=True, blank=True
)
frontpage_text = I18nTextField(
null=True, blank=True,

View File

@@ -1026,7 +1026,7 @@ class Question(LoggedModel):
(TYPE_PHONENUMBER, _("Phone number")),
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_PHONENUMBER]
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE, TYPE_PHONENUMBER]
event = models.ForeignKey(
Event,
@@ -1069,7 +1069,6 @@ class Question(LoggedModel):
)
ask_during_checkin = models.BooleanField(
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
help_text=_('Not supported by all check-in apps for all question types.'),
default=False
)
hidden = models.BooleanField(

View File

@@ -31,7 +31,6 @@ 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
@@ -87,8 +86,6 @@ 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
@@ -147,10 +144,6 @@ 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')
@@ -333,9 +326,6 @@ 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),
@@ -615,26 +605,21 @@ class Order(LockModel, LoggedModel):
return proposals
@staticmethod
def normalize_code(code, is_fallback=False):
d = {
def normalize_code(code):
tr = str.maketrans({
'2': 'Z',
'4': 'A',
'5': 'S',
'6': 'G',
}
if is_fallback:
d['8'] = 'B'
# 8 has been removed from the character set only in 2021, which means there are a lot of order codes
# with an 8 in it around. We only want to replace this when this is used in a fallback.
tr = str.maketrans(d)
})
return code.upper().translate(tr)
def assign_code(self):
# This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
# and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
# handwriting (2/Z, 4/A, 5/S, 6/G, 8/B). This allows for better detection e.g. in incoming wire transfers that
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
# might include OCR'd handwritten text
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ379')
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
iteration = 0
length = settings.ENTROPY['order_code']
while True:
@@ -872,7 +857,7 @@ class Order(LockModel, LoggedModel):
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale, self.event.settings.region):
with language(self.locale):
recipient = self.email
if position and position.attendee_email:
recipient = position.attendee_email
@@ -905,7 +890,7 @@ class Order(LockModel, LoggedModel):
)
def resend_link(self, user=None, auth=None):
with language(self.locale, self.event.settings.region):
with language(self.locale):
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}
@@ -917,7 +902,7 @@ class Order(LockModel, LoggedModel):
@property
def positions_with_tickets(self):
for op in self.positions.select_related('item'):
for op in self.positions.all():
if not op.generate_ticket:
continue
yield op
@@ -1170,7 +1155,7 @@ class AbstractPosition(models.Model):
(2) questions: a list of Question objects, extended by an 'answer' property
"""
self.answ = {}
for a in getattr(self, 'answerlist', self.answers.all()): # use prefetch_related cache from get_cart
for a in self.answers.all():
self.answ[a.question_id] = a
# We need to clone our question objects, otherwise we will override the cached
@@ -1224,9 +1209,6 @@ class AbstractPosition(models.Model):
else self.variation.quotas.filter(subevent=self.subevent))
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
if 'attendee_name_parts' in update_fields:
update_fields.append('attendee_name_cached')
self.attendee_name_cached = self.attendee_name
if self.attendee_name_parts is None:
self.attendee_name_parts = {}
@@ -1532,7 +1514,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, self.order.event.settings.region):
with language(self.order.locale):
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}
@@ -1550,7 +1532,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, self.order.event.settings.region):
with language(self.order.locale):
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}
@@ -1716,11 +1698,6 @@ class OrderRefund(models.Model):
max_length=255,
verbose_name=_("Payment provider")
)
comment = models.TextField(
verbose_name=_("Refund reason"),
help_text=_('May be shown to the end user or used e.g. as part of a payment reference.'),
null=True, blank=True
)
info = models.TextField(
verbose_name=_("Payment information"),
null=True, blank=True
@@ -1759,7 +1736,7 @@ class OrderRefund(models.Model):
Marks the refund as complete. This does not modify the state of the order.
:param user: The user who performed the change
:param auth: The API auth token that performed the change
:param user: The API auth token that performed the change
"""
self.state = self.REFUND_STATE_DONE
self.execution_date = self.execution_date or now()
@@ -2127,7 +2104,7 @@ class OrderPosition(AbstractPosition):
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.order.locale, self.order.event.settings.region):
with language(self.order.locale):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
@@ -2155,7 +2132,7 @@ class OrderPosition(AbstractPosition):
def resend_link(self, user=None, auth=None):
with language(self.order.locale, self.order.event.settings.region):
with language(self.order.locale):
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = _('Your event registration: %(code)s') % {'code': self.order.code}

View File

@@ -1,7 +1,7 @@
import string
from datetime import date, datetime, time
from django.core.validators import MinLengthValidator, RegexValidator
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.utils.crypto import get_random_string
@@ -38,11 +38,8 @@ class Organizer(LoggedModel):
"Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used "
"once. This is being used in URLs to refer to your organizer accounts and your events."),
validators=[
MinLengthValidator(
limit_value=2,
),
RegexValidator(
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$",
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$",
message=_("The slug may only contain letters, numbers, dots and dashes.")
),
OrganizerSlugBanlistValidator()

View File

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

View File

@@ -9,7 +9,7 @@ import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.exceptions import ImproperlyConfigured
from django.db import transaction
from django.dispatch import receiver
from django.forms import Form
@@ -706,24 +706,12 @@ class BasePaymentProvider:
It should return HTML code containing information regarding the current payment
status and, if applicable, next steps.
The default implementation returns an empty string.
The default implementation returns the verbose name of the payment provider.
:param order: The order object
"""
return ''
def payment_control_render_short(self, payment: OrderPayment) -> str:
"""
Will be called if the *event administrator* performs an action on the payment. Should
return a very short version of the payment method. Usually, this should return e.g.
a transaction ID or account identifier, but no information on status, dates, etc.
The default implementation falls back to payment_presa_elrender.
:param order: The order object
"""
return self.payment_presale_render(payment)
def refund_control_render(self, request: HttpRequest, refund: OrderRefund) -> str:
"""
Will be called if the *event administrator* views the details of a refund.
@@ -737,19 +725,6 @@ class BasePaymentProvider:
"""
return ''
def payment_presale_render(self, payment: OrderPayment) -> str:
"""
Will be called if the *ticket customer* views the details of a payment. This is
currently used e.g. when the customer requests a refund to show which payment
method is used for the refund. This should only include very basic information
about the payment, such as "VISA card ****9999", and never raw payment information.
The default implementation returns the public name of the payment provider.
:param order: The order object
"""
return self.public_name
def payment_refund_supported(self, payment: OrderPayment) -> bool:
"""
Will be called to check if the provider supports automatic refunding for this
@@ -785,32 +760,6 @@ class BasePaymentProvider:
"""
raise PaymentException(_('Automatic refunds are not supported by this payment provider.'))
def new_refund_control_form_render(self, request: HttpRequest, order: Order) -> str:
"""
Render a form that will be shown to backend users when trying to create a new refund.
Usually, refunds are created from an existing payment object, e.g. if there is a credit card
payment and the credit card provider returns ``True`` from ``payment_refund_supported``, the system
will automatically create an ``OrderRefund`` and call ``execute_refund`` on that payment. This method
can and should not be used in that situation! Instead, by implementing this method you can add a refund
flow for this payment provider that starts without an existing payment. For example, even though an order
was paid by credit card, it could easily be refunded by SEPA bank transfer. In that case, the SEPA bank
transfer provider would implement this method and return a form that asks for the IBAN.
This method should return HTML or ``None``. All form fields should have a globally unique name.
"""
return
def new_refund_control_form_process(self, request: HttpRequest, amount: Decimal, order: Order) -> OrderRefund:
"""
Process a backend user's request to initiate a new refund with an amount of ``amount`` for ``order``.
This method should parse the input provided to the form created and either raise ``ValidationError``
or return an ``OrderRefund`` object in ``created`` state that has not yet been saved to the database.
The system will then call ``execute_refund`` on that object.
"""
raise ValidationError('Not implemented')
def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
"""
When personal data is removed from an event, this method is called to scrub payment-related data
@@ -950,7 +899,7 @@ class ManualPayment(BasePaymentProvider):
@property
def public_name(self):
return str(self.settings.get('public_name', as_type=LazyI18nString) or _('Manual payment'))
return str(self.settings.get('public_name', as_type=LazyI18nString))
@property
def settings_form_fields(self):

View File

@@ -39,7 +39,6 @@ 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__)
@@ -230,11 +229,6 @@ 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"),
@@ -427,7 +421,6 @@ 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)
@@ -494,7 +487,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'], self.event.settings.region):
with language(o['locale']):
return self._get_text_content(op, order, o, True)
ev = self._get_ev(op, order)

View File

@@ -3,7 +3,6 @@ from decimal import Decimal
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
from django.utils.translation import gettext
from i18nfield.strings import LazyI18nString
from pretix.base.decimal import round_decimal
@@ -25,7 +24,7 @@ logger = logging.getLogger(__name__)
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
with language(wle.locale, wle.event.settings.region):
with language(wle.locale):
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
try:
mail(
@@ -42,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, order.event.settings.region):
with language(order.locale):
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
@@ -196,8 +195,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
comment=gettext('Event canceled'))
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions)
finally:
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
@@ -254,8 +252,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
if auto_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
comment=gettext('Event canceled'))
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions)
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)

View File

@@ -1,7 +1,6 @@
from datetime import timedelta
import dateutil
from django.core.files import File
from django.db import transaction
from django.db.models.functions import TruncDate
from django.dispatch import receiver
@@ -24,7 +23,7 @@ def get_logic_environment(ev):
elif t == 'date_from':
return ev.date_from
elif t == 'date_to':
return ev.date_to or ev.date_from
return ev.date_to
elif t == 'date_admission':
return ev.date_admission or ev.date_from
@@ -126,14 +125,6 @@ def _save_answers(op, answers, given_answers):
else:
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
qa.options.add(*a)
elif isinstance(a, File):
if q in answers:
qa = answers[q]
else:
qa = op.answers.create(question=q, answer=str(a))
qa.file.save(a.name, a, save=False)
qa.answer = 'file://' + qa.file.name
qa.save()
else:
if q in answers:
qa = answers[q]

View File

@@ -32,7 +32,7 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
)
file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
with language(event.settings.locale), override(event.settings.timezone):
responses = register_data_exporters.send(event)
for receiver, response in responses:
ex = response(event, set_progress)
@@ -67,18 +67,15 @@ 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
region = None
with language(locale, region), override(timezone):
with language(locale), override(timezone):
if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else:

View File

@@ -43,7 +43,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
lp = invoice.order.payments.last()
with language(invoice.locale, invoice.event.settings.region):
with language(invoice.locale):
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, invoice.event.settings.region):
with language(invoice.locale):
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, i.event.settings.region):
with language(i.locale):
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, event.settings.region):
with rolledback_transaction(), language(locale):
order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
invoice = Invoice(

View File

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

View File

@@ -65,7 +65,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
# TODO: quotacheck?
cf = CachedFile.objects.get(id=fileid)
user = User.objects.get(pk=user)
with language(locale, event.settings.region):
with language(locale):
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, event.settings.region):
with language(o.locale):
order_placed.send(event, order=o)
if o.status == Order.STATUS_PAID:
order_paid.send(event, order=o)

View File

@@ -23,9 +23,7 @@ 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, get_language_without_region, language,
)
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Order,
OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
@@ -262,7 +260,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, order.event.settings.region):
with language(order.locale):
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}
@@ -313,7 +311,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, order.event.settings.region):
with language(order.locale):
email_subject = _('Order denied: %(code)s') % {'code': order.code}
try:
order.send_mail(
@@ -424,7 +422,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, order.event.settings.region):
with language(order.locale):
email_context = get_email_context(event=order.event, order=order)
email_subject = _('Order canceled: %(code)s') % {'code': order.code}
try:
@@ -778,9 +776,8 @@ 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=get_language_without_region(locale),
locale=locale,
total=total,
testmode=True if sales_channel.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}),
@@ -1036,7 +1033,7 @@ def send_expiry_warnings(sender, **kwargs):
# Race condition
continue
with language(o.locale, settings.region):
with language(o.locale):
o.expiry_reminder_sent = True
o.save(update_fields=['expiry_reminder_sent'])
email_template = settings.mail_text_order_expire_warning
@@ -1113,7 +1110,7 @@ def send_download_reminders(sender, **kwargs):
if not send:
continue
with language(o.locale, o.event.settings.region):
with language(o.locale):
o.download_reminder_sent = True
o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder
@@ -1153,7 +1150,7 @@ def send_download_reminders(sender, **kwargs):
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
with language(order.locale, order.event.settings.region):
with language(order.locale):
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}
@@ -2034,7 +2031,7 @@ _unset = object()
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None, comment=None):
refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None):
notify_admin = False
error = False
if isinstance(order, int):
@@ -2059,7 +2056,6 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
order=order,
payment=None,
source=source,
comment=comment,
state=OrderRefund.REFUND_STATE_CREATED,
execution_date=now(),
amount=can_auto_refund_sum,
@@ -2097,7 +2093,6 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
comment=comment,
provider=p.provider
)
order.log_action('pretix.event.order.refund.created', {
@@ -2127,7 +2122,6 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
with transaction.atomic():
r = order.refunds.create(
source=source,
comment=comment,
state=OrderRefund.REFUND_STATE_CREATED,
amount=refund_amount - can_auto_refund_sum,
provider='manual'
@@ -2152,14 +2146,13 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@scopes_disabled()
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None):
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False):
try:
try:
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee)
if try_auto_refund:
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
comment=comment)
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard)
return ret
except LockTimeoutException:
self.retry()

View File

@@ -113,11 +113,10 @@ class QuotaAvailability:
raise e
def _write_cache(self, quotas, now_dt):
# 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
events = {q.event for q in quotas}
update = []
for e in events:
e.cache.delete('item_quota_cache')
for q in quotas:
rewrite_cache = self._count_waitinglist and (
not q.cache_is_hot(now_dt) or self.results[q][0] > q.cached_availability_state

View File

@@ -36,7 +36,7 @@ def validate_plan_change(event, subevent, plan):
'already sold.'), leftovers[0])
def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
def generate_seats(event, subevent, plan, mapping):
current_seats = {}
for s in event.seats.select_related('product').annotate(
has_op=Count('orderposition'), has_v=Count('vouchers')
@@ -68,10 +68,7 @@ def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
update(seat, 'seat_label', ss.seat_label),
update(seat, 'x', ss.x),
update(seat, 'y', ss.y),
] + (
[update(seat, 'blocked', ss.guid in blocked_guids)]
if blocked_guids else []
))
])
if updated:
seat.save()
else:
@@ -87,7 +84,6 @@ def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
seat_label=ss.seat_label,
x=ss.x,
y=ss.y,
blocked=bool(blocked_guids and ss.guid in blocked_guids),
product=p,
))

View File

@@ -17,7 +17,7 @@ from pretix.celery_app import app
@app.task(base=ProfiledEventTask)
def export(event: Event, shredders: List[str], session_key=None) -> None:
def export(event: Event, shredders: List[str]) -> None:
known_shredders = event.get_data_shredders()
with NamedTemporaryFile() as rawfile:
@@ -55,8 +55,6 @@ def export(event: Event, shredders: List[str], session_key=None) -> 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)
@@ -75,19 +73,16 @@ def shred(event: Event, fileid: str, confirm_code: str) -> None:
indexdata = json.loads(zipfile.read('index.json').decode())
if indexdata['organizer'] != event.organizer.slug or indexdata['event'] != event.slug:
raise ShredError(_("This file is from a different event."))
shredders = []
if indexdata['confirm_code'] != confirm_code:
raise ShredError(_("The confirm code you entered was incorrect."))
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):
raise ShredError(_("Something happened in your event after the export, please try again."))
for s in indexdata['shredders']:
shredder = known_shredders.get(s)
if not shredder:
continue
shredders.append(shredder)
if any(shredder.require_download_confirmation for shredder in shredders):
if indexdata['confirm_code'] != confirm_code:
raise ShredError(_("The confirm code you entered was incorrect."))
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):
raise ShredError(_("Something happened in your event after the export, please try again."))
for shredder in shredders:
shredder.shred_data()
cf.file.delete(save=False)

View File

@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
def generate_orderposition(order_position: int, provider: str):
order_position = OrderPosition.objects.select_related('order', 'order__event').get(id=order_position)
with language(order_position.order.locale, order_position.order.event.settings.region):
with language(order_position.order.locale):
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, order.event.settings.region):
with language(order.locale):
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, event.settings.region):
with rolledback_transaction(), language(event.settings.locale):
item = event.items.create(name=_("Sample product"), default_price=42.23,
description=_("Sample product description"))
item2 = event.items.create(name=_("Sample workshop"), default_price=23.40)

View File

@@ -45,8 +45,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
continue
if wle.subevent and not wle.subevent.presale_is_running:
continue
if not wle.item.is_available():
gone.add((wle.item, wle.variation, wle.subevent))
if not wle.item.active or (wle.variation and not wle.variation.active):
continue
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)

View File

@@ -21,9 +21,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
from rest_framework import serializers
from pretix.api.serializers.fields import (
ListMultipleChoiceField, UploadedFileField,
)
from pretix.api.serializers.fields import ListMultipleChoiceField
from pretix.api.serializers.i18n import I18nField
from pretix.base.models.tax import TaxRule
from pretix.base.reldate import (
@@ -31,7 +29,7 @@ from pretix.base.reldate import (
SerializerRelativeDateField, SerializerRelativeDateTimeField,
)
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
)
from pretix.helpers.countries import CachedCountries
@@ -195,25 +193,6 @@ 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,
@@ -853,20 +832,6 @@ 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,
@@ -1225,21 +1190,6 @@ DEFAULTS = {
"e.g. to explain choosing a lower refund will help your organization.")
)
},
'cancel_allow_user_paid_adjust_fees_step': {
'default': None,
'type': Decimal,
'form_class': forms.DecimalField,
'serializer_class': serializers.DecimalField,
'serializer_kwargs': dict(
max_digits=10, decimal_places=2
),
'form_kwargs': dict(
max_digits=10, decimal_places=2,
label=_("Step size for reduction amount"),
help_text=_('By default, customers can choose an arbitrary amount for you to keep. If you set this to e.g. '
'10, they will only be able to choose values in increments of 10.')
)
},
'cancel_allow_user_paid_require_approval': {
'default': 'False',
'type': bool,
@@ -1801,66 +1751,19 @@ Your {event} team"""))
},
'logo_image': {
'default': None,
'type': File,
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
max_size=10 * 1024 * 1024,
help_text=_('If you provide a logo image, we will by default not show your event name and date '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.')
),
'serializer_class': UploadedFileField,
'serializer_kwargs': dict(
allowed_types=[
'image/png', 'image/jpeg', 'image/gif'
],
max_size=10 * 1024 * 1024,
)
'type': File
},
'logo_image_large': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_('Use header image in its full size'),
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
)
'type': bool
},
'logo_show_title': {
'default': 'True',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_('Show event title even if a header image is present'),
help_text=_('The title will only be shown on the event front page.'),
)
'type': bool
},
'organizer_logo_image': {
'default': None,
'type': File,
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
max_size=10 * 1024 * 1024,
help_text=_('If you provide a logo image, we will by default not show your organization name '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.')
),
'serializer_class': UploadedFileField,
'serializer_kwargs': dict(
allowed_types=[
'image/png', 'image/jpeg', 'image/gif'
],
max_size=10 * 1024 * 1024,
)
'type': File
},
'organizer_logo_image_large': {
'default': 'False',
@@ -1874,43 +1777,11 @@ Your {event} team"""))
},
'og_image': {
'default': None,
'type': File,
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Social media image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
max_size=10 * 1024 * 1024,
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
'only the center square is shown. If you do not fill this, we will use the logo given above.')
),
'serializer_class': UploadedFileField,
'serializer_kwargs': dict(
allowed_types=[
'image/png', 'image/jpeg', 'image/gif'
],
max_size=10 * 1024 * 1024,
)
'type': File
},
'invoice_logo_image': {
'default': None,
'type': File,
'form_class': ExtFileField,
'form_kwargs': dict(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=10 * 1024 * 1024,
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
),
'serializer_class': UploadedFileField,
'serializer_kwargs': dict(
allowed_types=[
'image/png', 'image/jpeg', 'image/gif'
],
max_size=10 * 1024 * 1024,
)
'type': File
},
'frontpage_text': {
'default': '',
@@ -1974,17 +1845,6 @@ 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 '
@@ -2543,9 +2403,3 @@ def validate_organizer_settings(organizer, settings_dict):
#
# N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered.
pass
def global_settings_object(holder):
if not hasattr(holder, '_global_settings_object'):
holder._global_settings_object = GlobalSettingsObject()
return holder._global_settings_object

View File

@@ -20,7 +20,6 @@ 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):
@@ -82,14 +81,6 @@ class BaseDataShredder:
"""
return False
@property
def require_download_confirmation(self):
"""
Indicates whether the data of this shredder needs to be downloaded, before it is actually shredded. By default
this value is equal to the tax relevant flag.
"""
return self.tax_relevant
@property
def verbose_name(self) -> str:
"""
@@ -130,31 +121,6 @@ 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'
@@ -406,10 +372,9 @@ class PaymentInfoShredder(BaseDataShredder):
@receiver(register_data_shredders, dispatch_uid="shredders_builtin")
def register_core_shredders(sender, **kwargs):
def register_payment_provider(sender, **kwargs):
return [
EmailAddressShredder,
PhoneNumberShredder,
AttendeeInfoShredder,
InvoiceAddressShredder,
QuestionAnswerShredder,

View File

@@ -13,7 +13,7 @@
&middot; <a id='reload' href='#'>{% trans "Try again" %}</a>
</p>
{% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
<p>
{% csrf_token %}
<button type="submit" class="btn btn-default" id="button-sudo">

View File

@@ -12,7 +12,7 @@
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
</p>
{% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
<p>
{% csrf_token %}
<button type="submit" class="btn btn-default" id="button-sudo">

View File

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

View File

@@ -1,4 +1,3 @@
import re
import urllib.parse
import bleach
@@ -72,10 +71,6 @@ 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')
@@ -85,42 +80,7 @@ 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(r'[^a-zA-Z0-9.\-/_]', '', attrs.get('_text')) # clean up link text
href_url = urllib.parse.urlparse(attrs[None, 'href'])
if URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
# 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)
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)
@@ -133,7 +93,6 @@ 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(
@@ -186,7 +145,7 @@ def rich_text(text: str, **kwargs):
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
parse_email=True
)
body_md = linker.linkify(markdown_compile(text))
@@ -202,7 +161,7 @@ def rich_text_snippet(text: str, **kwargs):
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
parse_email=True
)
body_md = linker.linkify(markdown_compile(text, snippet=True))

View File

@@ -13,11 +13,7 @@ class DownloadView(TemplateView):
@cached_property
def object(self) -> CachedFile:
try:
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
return get_object_or_404(CachedFile, id=self.kwargs['id'])
except ValueError: # Invalid URLs
raise Http404()

View File

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

View File

@@ -19,7 +19,9 @@ 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
@@ -27,7 +29,7 @@ from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
)
from pretix.control.forms import (
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
ExtFileField, MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
@@ -35,6 +37,7 @@ from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
class EventWizardFoundationForm(forms.Form):
@@ -391,7 +394,7 @@ class EventUpdateForm(I18nModelForm):
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
'sales_channels': CheckboxSelectMultiple(),
'sales_channels': CheckboxSelectMultiple()
}
@@ -412,6 +415,36 @@ class EventSettingsForm(SettingsForm):
"restrict the set of selectable titles."),
required=False,
)
logo_image = ExtFileField(
label=_('Header image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=10 * 1024 * 1024,
help_text=_('If you provide a logo image, we will by default not show your event name and date '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.')
)
logo_image_large = forms.BooleanField(
label=_('Use header image in its full size'),
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
required=False,
)
logo_show_title = forms.BooleanField(
label=_('Show event title even if a header image is present'),
help_text=_('The title will only be shown on the event front page.'),
required=False,
)
og_image = ExtFileField(
label=_('Social media image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=10 * 1024 * 1024,
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
'only the center square is shown. If you do not fill this, we will use the logo given above.')
)
auto_fields = [
'imprint_url',
@@ -426,7 +459,6 @@ class EventSettingsForm(SettingsForm):
'presale_start_show_date',
'locales',
'locale',
'region',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_hours',
@@ -450,9 +482,6 @@ 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',
@@ -464,41 +493,12 @@ class EventSettingsForm(SettingsForm):
'theme_color_background',
'theme_round_borders',
'primary_font',
'logo_image',
'logo_image_large',
'logo_show_title',
'og_image',
]
def clean(self):
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
@@ -522,39 +522,9 @@ class EventSettingsForm(SettingsForm):
if not self.event.has_subevents:
del self.fields['frontpage_subevent_ordering']
del self.fields['event_list_type']
# 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'
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts().items()
]
class CancelSettingsForm(SettingsForm):
@@ -568,7 +538,6 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_paid_keep_percentage',
'cancel_allow_user_paid_adjust_fees',
'cancel_allow_user_paid_adjust_fees_explanation',
'cancel_allow_user_paid_adjust_fees_step',
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'change_allow_user_variation',
@@ -704,7 +673,6 @@ class InvoiceSettingsForm(SettingsForm):
'invoice_additional_text',
'invoice_footer_text',
'invoice_eu_currencies',
'invoice_logo_image',
]
invoice_generate_sales_channels = forms.MultipleChoiceField(
@@ -724,6 +692,13 @@ class InvoiceSettingsForm(SettingsForm):
label=_("Invoice language"),
choices=[('__user__', _('The user\'s language'))] + settings.LANGUAGES,
)
invoice_logo_image = ExtFileField(
label=_('Logo image'),
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
required=False,
max_size=10 * 1024 * 1024,
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
)
def __init__(self, *args, **kwargs):
event = kwargs.get('obj')

View File

@@ -5,7 +5,7 @@ from urllib.parse import urlencode
from django import forms
from django.apps import apps
from django.conf import settings
from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet
from django.db.models import Exists, F, Model, OuterRef, Q, QuerySet
from django.db.models.functions import Coalesce, ExtractWeekDay
from django.urls import reverse, reverse_lazy
from django.utils.formats import date_format, localize
@@ -150,8 +150,8 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_PENDING + Order.STATUS_PAID, _('Pending or paid')),
)),
(_('Cancellations'), (
(Order.STATUS_CANCELED, _('Canceled (fully)')),
('cp', _('Canceled (fully or with paid fee)')),
(Order.STATUS_CANCELED, _('Canceled')),
('cp', _('Canceled (or with paid fee)')),
('rc', _('Cancellation requested')),
)),
(_('Payment process'), (
@@ -159,8 +159,7 @@ class OrderFilterForm(FilterForm):
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _('Pending or expired')),
('o', _('Pending (overdue)')),
('overpaid', _('Overpaid')),
('partially_paid', _('Partially paid')),
('underpaid', _('Underpaid (but confirmed)')),
('underpaid', _('Underpaid')),
('pendingpaid', _('Pending (but fully paid)')),
)),
(_('Approval process'), (
@@ -239,10 +238,6 @@ class OrderFilterForm(FilterForm):
elif s == 'rc':
qs = qs.filter(
cancellation_requests__isnull=False
).annotate(
cancellation_request_time=Max('cancellation_requests__created')
).order_by(
'-cancellation_request_time'
)
elif s == 'pendingpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
@@ -250,14 +245,6 @@ 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(
@@ -462,16 +449,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
required=False,
label=_('Total amount'),
)
payment_sum_min = forms.DecimalField(
localize=True,
required=False,
label=_('Minimal sum of payments and refunds'),
)
payment_sum_max = forms.DecimalField(
localize=True,
required=False,
label=_('Maximal sum of payments and refunds'),
)
sales_channel = forms.ChoiceField(
label=_('Sales channel'),
required=False,
@@ -598,16 +575,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
qs = qs.filter(email_known_to_work=fdata.get('email_known_to_work'))
if fdata.get('locale'):
qs = qs.filter(locale=fdata.get('locale'))
if fdata.get('payment_sum_min') is not None:
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
computed_payment_refund_sum__gte=fdata['payment_sum_min'],
)
if fdata.get('payment_sum_max') is not None:
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
computed_payment_refund_sum__lte=fdata['payment_sum_max'],
)
if fdata.get('invoice_address_company'):
qs = qs.filter(invoice_address__company__icontains=fdata.get('invoice_address_company'))
if fdata.get('invoice_address_name'):
@@ -1510,9 +1477,6 @@ class VoucherTagFilterForm(FilterForm):
class RefundFilterForm(FilterForm):
orders = {'provider': 'provider', 'state': 'state', 'order': 'order__code',
'source': 'source', 'amount': 'amount', 'created': 'created'}
provider = forms.ChoiceField(
label=_('Payment provider'),
choices=[
@@ -1549,10 +1513,6 @@ class RefundFilterForm(FilterForm):
qs = qs.filter(state__in=[OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_EXTERNAL])
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
qs = qs.order_by('-created')
return qs

View File

@@ -10,15 +10,11 @@ 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(list(self.fields.items()) + [
self.fields = OrderedDict([
('footer_text', I18nFormField(
widget=I18nTextInput,
required=False,

View File

@@ -16,7 +16,6 @@ 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,
)
@@ -461,15 +460,7 @@ class OrderContactForm(forms.ModelForm):
class Meta:
model = Order
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']
fields = ['email', 'email_known_to_work']
class OrderLocaleForm(forms.ModelForm):
@@ -616,7 +607,7 @@ class EventCancelForm(forms.Form):
required=False
)
manual_refund = forms.BooleanField(
label=_('Create manual refund if the payment method does not support automatic refunds'),
label=_('Create manual refund if the payment method odes not support automatic refunds'),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
initial=True,
required=False,

View File

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

View File

@@ -22,12 +22,6 @@ from pretix.helpers.money import change_decimal_field
class SubEventForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
instance = kwargs.get('instance')
if instance and not instance.pk:
kwargs['initial'].setdefault('name', self.event.name)
kwargs['initial'].setdefault('location', self.event.location)
kwargs['initial'].setdefault('geo_lat', self.event.geo_lat)
kwargs['initial'].setdefault('geo_lon', self.event.geo_lon)
super().__init__(*args, **kwargs)
self.fields['location'].widget.attrs['rows'] = '3'

View File

@@ -393,7 +393,7 @@ class VoucherBulkForm(VoucherForm):
data['bulk'] = True
del data['codes']
objs.append(obj)
Voucher.objects.bulk_create(objs, batch_size=200)
Voucher.objects.bulk_create(objs)
objs = []
for v in event.vouchers.filter(code__in=self.cleaned_data['codes']):
# We need to query them again as bulk_create does not fill in .pk values on databases

View File

@@ -292,10 +292,6 @@ 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.'),
@@ -357,8 +353,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'account.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
'pretix.control.auth.user.forgot_password.denied.repeated': _('A repeated password reset has been denied, as '
'the last request was less than 24 hours ago.'),
'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),

View File

@@ -6,13 +6,6 @@ from django.urls import reverse
from django.utils.translation import gettext as _
def current_url(request):
if len(request.GET):
return request.path + '?' + request.GET.urlencode()
else:
return request.path
def event_permission_required(permission):
"""
This view decorator rejects all requests with a 403 response which are not from
@@ -101,7 +94,7 @@ def administrator_permission_required():
raise PermissionDenied()
if not request.user.has_active_staff_session(request.session.session_key):
if request.user.is_staff:
return redirect(reverse('control:user.sudo') + '?next=' + quote(current_url(request)))
return redirect(reverse('control:user.sudo') + '?next=' + quote(request.path))
raise PermissionDenied(_('You do not have permission to view this content.'))
return function(request, *args, **kw)
return wrapper

View File

@@ -291,7 +291,7 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
"""
subevent_forms = EventPluginSignal(
providing_args=['request', 'subevent', 'copy_from']
providing_args=['request', 'subevent']
)
"""
This signal allows you to return additional forms that should be rendered on the subevent creation
@@ -301,8 +301,7 @@ as part of the standard validation and rendering cycle and rendered using defaul
styles. It is advisable to set a prefix for your form to avoid clashes with other plugins.
``subevent`` can be ``None`` during creation. Before ``save()`` is called, a ``subevent`` property of
your form instance will automatically being set to the subevent that has just been created. During
creation, ``copy_from`` can be a subevent that is being copied from.
your form instance will automatically being set to the subevent that has just been created.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""

View File

@@ -18,7 +18,7 @@
<script type="text/javascript" src="{% url 'javascript-catalog' lang=request.LANGUAGE_CODE %}"
defer></script>
{% else %}
<script src="{% statici18n request.LANGUAGE_CODE %}" async></script>
<script src="{% statici18n LANGUAGE_CODE %}" async></script>
{% endif %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
@@ -186,7 +186,7 @@
{% if request.user.is_staff and not staff_session %}
<li>
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-link" id="button-sudo">
<i class="fa fa-id-card"></i> {% trans "Admin mode" %}

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