Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel
1279c8720f Add tests for price calculation API 2019-04-22 11:14:33 +02:00
Raphael Michel
7a3afde7b1 Change semantics of changing orders
This basically does two things to the "Change products" view of orders and the
OrderChangeManager program API:

1) It decouples changing items or subevents from changing prices.
   OrderChangeManager.change_item() and .change_subevent() no longer
   touch the price of a position. Instead .change_price() needs to be
   called explicitly. However, a client-side JavaScript component now
   *proposes* a new price based on the changed item or subevent.

2) The user interface now exposes the possibility of doing multiple
   things at the same time, i.e. changing the item, subevent and price
   in the same operation. OrderChangeManager already allowed this
   before.

(1) is basically a consequence of (2), while (2) is a prerequesite for
e.g. the `seating` branch, where changing the subevent will always
require changing the seat.
2019-04-21 23:02:32 +02:00
315 changed files with 47646 additions and 114616 deletions

View File

@@ -25,6 +25,8 @@ matrix:
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.5
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.7
env: JOB=plugins
- python: 3.7
env: JOB=doc-spelling
- python: 3.7

View File

@@ -125,8 +125,6 @@ Example::
Indicates if the database backend is a MySQL/MariaDB Galera cluster and
turns on some optimizations/special case handlers. Default: ``False``
.. _`config-replica`:
Database replica settings
-------------------------
@@ -144,8 +142,6 @@ Example::
[replica]
host=192.168.0.2
.. _`config-urls`:
URLs
----
@@ -273,24 +269,6 @@ to speed up various operations::
If redis is not configured, pretix will store sessions and locks in the database. If memcached
is configured, memcached will be used for caching instead of redis.
Translations
------------
pretix comes with a number of translations. Some of them are marked as "incubating", which means
they can usually only be selected in development mode. If you want to use them nevertheless, you
can activate them like this::
[languages]
allow_incubating=pt-br,da
You can also tell pretix about additional paths where it will search for translations::
[languages]
path=/path/to/my/translations
For a given language (e.g. ``pt-br``), pretix will then look in the
specific sub-folder, e.g. ``/path/to/my/translations/pt_BR/LC_MESSAGES/django.po``.
Celery task queue
-----------------

View File

@@ -11,4 +11,3 @@ This documentation is for everyone who wants to install pretix on a server.
installation/index
config
maintainance
scaling

View File

@@ -1,5 +1,3 @@
.. _`installation`:
Installation guide
==================

View File

@@ -1,236 +0,0 @@
.. _`scaling`:
Scaling guide
=============
Our :ref:`installation guide <installation>` only covers "small-scale" setups, by which we mostly mean
setups that run on a **single (virtual) machine** and do not encounter large traffic peaks.
We do not offer an installation guide for larger-scale setups of pretix, mostly because we believe that
there is no one-size-fits-all solution for this and the desired setup highly depends on your use case,
the platform you run pretix on, and your technical capabilities. We do not recommend trying set up pretix
in a multi-server environment if you do not already have experience with managing server clusters.
This document is intended to give you a general idea on what issues you will encounter when you scale up
and what you should think of.
.. tip::
If you require more help on this, we're happy to help. Our pretix Enterprise support team has built
and helped building, scaling and load-testing pretix installations at any scale and we're looking
forward to work with you on fine-tuning your system. If you intend to sell **more than a thousand
tickets in a very short amount of time**, we highly recommend reaching out and at least talking this
through. Just get in touch at sales@pretix.eu!
Scaling reasons
---------------
There's mainly two reasons to scale up a pretix installation beyond a single server:
* **Availability:** Distributing pretix over multiple servers can allow you to survive failure of one or more single machines, leading to a higher uptime and reliability of your system.
* **Traffic and throughput:** Distributing pretix over multiple servers can allow you to process more web requests and ticket sales at the same time.
You are very unlikely to require scaling for other reasons, such as having too much data in your database.
Components
----------
A pretix installation usually consists of the following components which run performance-relevant processes:
* ``pretix-web`` is the Django-based web application that serves all user interaction.
* ``pretix-worker`` is a Celery-based application that processes tasks that should be run asynchronously outside of the web application process.
* A **SQL database** keeps all the important data and processes the actual transactions. We recommend using PostgreSQL, but MySQL/MariaDB works as well.
* A **web server** that terminates TLS and HTTP connections and forwards them to ``pretix-web``. In some cases, e.g. when serving static files, the web servers might return a response directly. We recommend using ``nginx``.
* A **redis** server responsible for the communication between ``pretix-web`` and ``pretix-worker``, as well as for caching.
* A directory of **media files** such as user-uploaded files or generated files (tickets, invoices, …) that are created and used by ``pretix-web``, ``pretix-worker`` and the web server.
In the following, we will discuss the scaling behavior of every component individually. In general, you can run all of the components
on the same server, but you can just as well distribute every component to its own server, or even use multiple servers for some single
components.
.. warning::
When setting up your system, don't forget about security. In a multi-server environment,
you need to take special care to ensure that no unauthorized access to your database
is possible through the network and that it's not easy to wiretap your connections. We
recommend a rigorous use of firewalls and encryption on all communications. You can
ensure this either on an application level (such as using the TLS support in your
database) or on a network level with a VPN solution.
Web server
""""""""""
Your web server is at the very front of your installation. It will need to absorb all of the traffic, and it should be able to
at least show a decent error message, even when everything else fails. Luckily, web servers are really fast these days, so this
can be achieved without too much work.
We recommend reading up on tuning your web server for high concurrency. For nginx, this means thinking about the number of worker
processes and the number of connections each worker process accepts. Double-check that TLS session caching works, because TLS
handshakes can get really expensive.
During a traffic peak, your web server will be able to make us of more CPU resources, while memory usage will stay comparatively low,
so if you invest in more hardware here, invest in more and faster CPU cores.
Make sure that pretix' static files (such as CSS and JavaScript assets) as well as user-uploaded media files (event logos, etc)
are served directly by your web server and your web server caches them in-memory (nginx does it by default) and sets useful
headers for client-side caching. As an additional performance improvement, you can turn of access logging for these types of files.
If you want, you can even farm out serving static files to a different web server entirely and :ref:`configure pretix to reference
them from a different URL <config-urls>`.
.. tip::
If you expect *really high traffic* for your very popular event, you might want to do some rate limiting on this layer, or,
if you want to ensure a fair and robust first-come-first-served experience and prefer letting users wait over showing them
errors, consider a queuing solution. We're happy to provide you with such systems, just get in touch at sales@pretix.eu.
pretix-web
""""""""""
The ``pretix-web`` process does not carry any internal state can be easily started on as many machines as you like, and you can
use the load balancing features of your frontend web server to redirect to all of them.
You can adjust the number of processes in the ``gunicorn`` command line, and we recommend choosing roughly two times the number
of CPU cores available. Under load, the memory consumption of ``pretix-web`` will stay comparatively constant, while the CPU usage
will increase a lot. Therefore, if you can add more or faster CPU cores, you will be able to serve more users.
pretix-worker
"""""""""""""
The ``pretix-worker`` process performs all operations that are not directly executed in the request-response-cycle of ``pretix-web``.
Just like ``pretix-web`` you can easily start up as many instances as you want on different machines to share the work. As long as they
all talk to the same redis server, they will all receive tasks from ``pretix-web``, work on them and post their result back.
You can configure the number of threads that run tasks in parallel through the ``--concurrency`` command line option of ``celery``.
Just like ``pretix-web``, this process is mostly heavy on CPU, disk IO and network IO, although memory peaks can occur e.g. during the
generation of large PDF files, so we recommend having some reserves here.
``pretix-worker`` performs a variety of tasks which are of different importance.
Some of them are mission-critical and need to be run quickly even during high load (such as
creating a cart or an order), others are irrelevant and can easily run later (such as
distributing tickets on the waiting list). You can fine-tune the capacity you assign to each
of these tasks by running ``pretix-worker`` processes that only work on a specific **queue**.
For example, you could have three servers dedicated only to process order creations and one
server dedicated only to sending emails. This allows you to set priorities and also protects
you from e.g. a slow email server lowering your ticket throughput.
You can do so by specifying one or more queues on the ``celery`` command line of this process, such as ``celery -A pretix.celery_app worker -Q notifications,mail``. Currently,
the following queues exist:
* ``checkout`` -- This queue handles everything related to carts and orders and thereby everything required to process a sale. This includes adding and deleting items from carts as well as creating and canceling orders.
* ``mail`` -- This queue handles sending of outgoing emails.
* ``notifications`` -- This queue handles the processing of any outgoing notifications, such as email notifications to admin users (except for the actual sending) or API notifications to registered webhooks.
* ``background`` -- This queue handles tasks that are expected to take long or have no human waiting for their result immediately, such as refreshing caches, re-generating CSS files, assigning tickets on the waiting list or parsing bank data files.
* ``default`` -- This queue handles everything else with "medium" or unassigned priority, most prominently the generation of files for tickets, invoices, badges, admin exports, etc.
Media files
"""""""""""
Both ``pretix-web``, ``pretix-worker`` and in some cases your webserver need to work with
media files. Media files are all files generated *at runtime* by the software. This can
include files uploaded by the event organizers, such as the event logo, files uploaded by
ticket buyers (if you use such features) or files generated by the software, such as
ticket files, invoice PDFs, data exports or customized CSS files.
Those files are by default stored to the ``media/`` sub-folder of the data directory given
in the ``pretix.cfg`` configuration file. Inside that ``media/`` folder, you will find a
``pub/`` folder containing the subset of files that should be publicly accessible through
the web server. Everything else only needs to be accessible by ``pretix-web`` and
``pretix-worker`` themselves.
If you distribute ``pretix-web`` or ``pretix-worker`` across more than one machine, you
**must** make sure that they all have access to a shared storage to read and write these
files, otherwise you **will** run into errors with the user interface.
The easiest solution for this is probably to store them on a NFS server that you mount
on each of the other servers.
Since we use Django's file storage mechanism internally, you can in theory also use a object-storage solution like Amazon S3, Ceph, or Minio to store these files, although we currently do not expose this through pretix' configuration file and this would require you to ship your own variant of ``pretix/settings.py`` and reference it through the ``DJANGO_SETTINGS_MODULE`` environment variable.
At pretix.eu, we use a custom-built `object storage cluster`_.
SQL database
""""""""""""
One of the most critical parts of the whole setup is the SQL database -- and certainly the
hardest to scale. Tuning relational databases is an art form, and while there's lots of
material on it on the internet, there's not a single recipe that you can apply to every case.
As a general rule of thumb, the more resources you can give your databases, the better.
Most databases will happily use all CPU cores available, but only use memory up to an amount
you configure, so make sure to set this memory usage as high as you can afford. Having more
memory available allows your database to make more use of caching, which is usually good.
Scaling your database to multiple machines needs to be treated with great caution. It's a
good to have a replica of your database for availability reasons. In case your primary
database server fails, you can easily switch over to the replica and continue working.
However, using database replicas for performance gains is much more complicated. When using
replicated database systems, you are always trading in consistency or availability to get
additional performance and the consequences of this can be subtle and it is important
that you have a deep understanding of the semantics of your replication mechanism.
.. warning::
Using an off-the-shelf database proxy solution that redirects read queries to your
replicas and write queries to your primary database **will lead to very nasty bugs.**
As an example, if you buy a ticket, pretix first needs to calculate how many tickets
are left to sell. If this calculation is done on a database replica that lags behind
even for fractions of a second, the decision to allow selling the ticket will be made
on out-of-data data and you can end up with more tickets sold than configured. Similarly,
you could imagine situations leading to double payments etc.
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
This way, pretix can offload complex read-only queries to the replica when it is safe to do so.
As of pretix 2.7, this is mainly used for search queries in the backend and for rendering the
product list and event lists in the frontend, but we plan on expanding this in the future.
Therefore, for now our clear recommendation is: Try to scale your database vertically and put
it on the most powerful machine you have available.
redis
"""""
While redis is a very important part that glues together some of the components, it isn't used
heavily and can usually handle a fairly large pretix installation easily on a single modern
CPU core.
Having some memory available is good in case of e.g. lots of tasks queuing up during a traffic peak, but we wouldn't expect ever needing more than a gigabyte of it.
Feel free to set up a redis cluster for availability but you won't need it for performance in a long time.
The limitations
---------------
Up to a certain point, pretix scales really well. However, there are a few things that we consider
even more important than scalability, and those are correctness and reliability. We want you to be
able to trust that pretix will not sell more tickets than you intended or run into similar error
cases.
Combined with pretix' flexibility and complexity, especially around vouchers and quotas, this creates
some hard issues. In many cases, we need to fall back to event-global locking for some actions which
are likely to run with high concurrency and cause harm.
For every event, only one of these locking actions can be run at the same time. Examples for this are
adding products limited by a quota to a cart, adding items to a cart using a voucher or placing an order
consisting of cart positions that don't have a valid reservation for much longer. In these cases, it is
currently not realistically possible to exceed selling **approx. 500 orders per minute per event**, even
if you add more hardware.
If you have an unlimited number of tickets, we can apply fewer locking and we've reached **approx.
1500 orders per minute per event** in benchmarks, although even more should be possible.
We're working to reduce the number of cases in which this is relevant and thereby improve the possible
throughput. If you want to use pretix for an event with 10,000+ tickets that are likely to be sold out
within minutes, please get in touch to discuss possible solutions. We'll work something out for you!
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/

View File

@@ -1,131 +0,0 @@
pretix Hosted billing invoices
==============================
This endpoint allows you to access invoices you received for pretix Hosted. It only contains invoices created starting
November 2017.
.. note:: Only available on pretix Hosted, not on self-hosted pretix instances.
Resource description
--------------------
The resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
invoice_number string Invoice number
date_issued date Invoice date
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/
Returns a list of all invoices to a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/billing_invoices/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"invoice_number": "R2019002",
"date_issued": "2019-06-03"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_issued`` and
its reverse, ``-date_issued``. Default: ``date_issued``.
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/(invoice_number)/
Returns information on one invoice, identified by its invoice number.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/billing_invoices/R2019002/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"invoice_number": "R2019002",
"date_issued": "2019-06-03"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param invoice_number: The ``invoice_number`` field of the invoice to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/(invoice_number)/download/
Download an invoice in PDF format.
.. warning:: After we created the invoices, they are placed in review with our accounting department. You will
already see them in the API at this point, but you are not able to download them until they completed
review and are sent to you via email. This usually takes a few hours. If you try to download them
in this time frame, you will receive a status code :http:statuscode:`423`.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/billing_invoices/R2019002/download/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/pdf
...
:param organizer: The ``slug`` field of the organizer to fetch
:param invoice_number: The ``invoice_number`` field of the invoice to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 423: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.

View File

@@ -336,24 +336,11 @@ Order position endpoints
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
.. versionchanged:: 2.7
The resource now contains the new attributes ``require_attention`` and ``order__status`` and accepts the new
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
returns ``400`` instead of ``404`` on tickets which are known but not paid.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as
the :ref:`order-position-resource`, with the following differences:
* The ``checkins`` value will only include check-ins for the selected list.
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
have the ``checkin_attention`` flag set.
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
the :ref:`order-position-resource`, with one important difference: the ``checkins`` value will only include
check-ins for the selected list.
**Example request**:
@@ -420,8 +407,6 @@ Order position endpoints
}
:query integer page: The page number in case of a multi-page result set, default is 1
:query string ignore_status: If set to ``true``, results will be returned regardless of the state of
the order they belong to and you will need to do your own filtering by order status.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``order__code``,
``order__datetime``, ``positionid``, ``attendee_name``, ``last_checked_in`` and ``order__email``. Default:
``attendee_name,positionid``
@@ -457,15 +442,8 @@ Order position endpoints
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/(id)/
Returns information on one order position, identified by its internal ID.
The result is the same as the :ref:`order-position-resource`, with the following differences:
* The ``checkins`` value will only include check-ins for the selected list.
* An additional boolean property ``require_attention`` will inform you whether either the order or the item
have the ``checkin_attention`` flag set.
* If ``attendee_name`` is empty, it will automatically fall back to values from a parent product or from invoice
addresses.
The result format is the same as the :ref:`order-position-resource`, with one important difference: the
``checkins`` value will only include check-ins for the selected list.
**Instead of an ID, you can also use the ``secret`` field as the lookup parameter.**
@@ -550,8 +528,7 @@ Order position endpoints
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
questions that have not been filled. Defaults to ``false``.
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
list.
Defaults to ``false``.
:<json string nonce: You can set this parameter to a unique random value to identify this check-in. If you're sending
this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This

View File

@@ -50,10 +50,6 @@ plugins list A list of packa
The ``testmode`` attribute has been added.
.. versionchanged:: 2.8
When cloning events, the ``testmode`` attribute will now be cloned, too.
Endpoints
---------
@@ -116,9 +112,6 @@ Endpoints
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
Default: ``slug``.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -253,7 +246,7 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
their value will be copied from the existing event.

View File

@@ -23,4 +23,3 @@ Resources and endpoints
waitinglist
carts
webhooks
billing_invoices

View File

@@ -56,8 +56,6 @@ Endpoints
}
:query page: The page number in case of a multi-page result set, default is 1
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``slug`` and
``name``. Default: ``slug``.
:statuscode 200: no error
:statuscode 401: Authentication failure

View File

@@ -30,7 +30,6 @@ type string The expected ty
* ``D`` date
* ``H`` time
* ``W`` date and time
* ``CC`` country code (ISO 3666-1 alpha-2)
required boolean If ``true``, the question needs to be filled out.
position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to.
@@ -39,8 +38,6 @@ identifier string An arbitrary st
ask_during_checkin boolean If ``true``, this question will not be asked while
buying the ticket, but will show up when redeeming
the ticket instead.
hidden boolean If ``true``, the question will only be shown in the
backend.
options list of objects In case of question type ``C`` or ``M``, this lists the
available objects. Only writable during creation,
use separate endpoint to modify this later.
@@ -71,10 +68,6 @@ dependency_value string The value ``dep
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
options resource. The ``position`` attribute has been added to the options resource.
.. versionchanged:: 2.7
The attribute ``hidden`` and the question type ``CC`` have been added.
Endpoints
---------
@@ -117,7 +110,6 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -185,7 +177,6 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -237,7 +228,6 @@ Endpoints
"items": [1, 2],
"position": 1,
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -271,7 +261,6 @@ Endpoints
"position": 1,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [
@@ -343,7 +332,6 @@ Endpoints
"position": 2,
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"dependency_question": null,
"dependency_value": null,
"options": [

View File

@@ -20,8 +20,6 @@ name multi-lingual string The sub-event's
event string The slug of the parent event
active boolean If ``true``, the sub-event ticket shop is publicly
available.
is_public boolean If ``true``, the sub-event ticket shop is publicly
shown in lists.
date_from datetime The sub-event's start date
date_to datetime The sub-event's end date (or ``null``)
date_admission datetime The sub-event's admission date (or ``null``)
@@ -50,10 +48,6 @@ meta_data dict Values set for
.. versionchanged:: 2.6
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 2.7
The attribute ``is_public`` has been added.
Endpoints
---------
@@ -87,7 +81,6 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -135,7 +128,6 @@ Endpoints
{
"name": {"en": "First Sample Conference"},
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -165,7 +157,6 @@ Endpoints
"id": 1,
"name": {"en": "First Sample Conference"},
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -216,7 +207,6 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -280,7 +270,6 @@ Endpoints
"name": {"en": "New Subevent Name"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,
@@ -364,7 +353,6 @@ Endpoints
"name": {"en": "First Sample Conference"},
"event": "sampleconf",
"active": false,
"is_public": true,
"date_from": "2017-12-27T10:00:00Z",
"date_to": null,
"date_admission": null,

View File

@@ -66,7 +66,7 @@ source_suffix = '.rst'
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = 'contents'
# General information about the project.
project = 'pretix'
@@ -234,7 +234,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'pretix.tex', 'pretix Documentation',
('contents', 'pretix.tex', 'pretix Documentation',
'Raphael Michel', 'manual'),
]

View File

@@ -12,7 +12,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels, register_global_settings
item_copy_data, register_sales_channels
Order events
""""""""""""
@@ -20,13 +20,17 @@ Order events
There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:members: validate_cart, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download
Frontend
--------
.. automodule:: pretix.presale.signals
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, checkout_flow_steps, order_info, order_meta_from_request, position_info
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble
.. automodule:: pretix.presale.signals
:members: order_info, order_meta_from_request
Request flow
""""""""""""
@@ -45,11 +49,11 @@ Backend
.. automodule:: pretix.control.signals
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, nav_item
order_info, event_settings_widget, oauth_application_registered, order_position_buttons
.. automodule:: pretix.base.signals
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
:members: logentry_display, logentry_object_link, requiredaction_display
Vouchers
""""""""

View File

@@ -49,19 +49,15 @@ description string A more verbose description of what your
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
for an event by system administrators / superusers.
compatibility string Specifier for compatible pretix versions.
================== ==================== ===========================================================
A working example would be::
try:
from pretix.base.plugins import PluginConfig
except ImportError:
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class PaypalApp(PluginConfig):
class PaypalApp(AppConfig):
name = 'pretix_paypal'
verbose_name = _("PayPal")
@@ -72,7 +68,6 @@ A working example would be::
visible = True
restricted = False
description = _("This plugin allows you to receive payments via PayPal")
compatibility = "pretix>=2.7.0"
default_app_config = 'pretix_paypal.PaypalApp'

View File

@@ -23,7 +23,7 @@ Organizers and events
:members:
.. autoclass:: pretix.base.models.Event
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, invoice_renderer, settings
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
.. autoclass:: pretix.base.models.SubEvent
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running

View File

@@ -21,12 +21,10 @@ Your should install the following on your system:
* Python 3.5 or newer
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
* On Debian/Ubuntu: ``python-venv`` for Python 3 (Debian package: ``python3-venv``)
* ``libffi`` (Debian package: ``libffi-dev``)
* ``libssl`` (Debian package: ``libssl-dev``)
* ``libxml2`` (Debian package ``libxml2-dev``)
* ``libxslt`` (Debian package ``libxslt1-dev``)
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
* ``msgfmt`` (Debian package ``gettext``)
* ``git``

View File

@@ -15,7 +15,6 @@ boolean
booleans
cancelled
casted
Ceph
checkbox
checksum
config
@@ -54,7 +53,6 @@ linters
memcached
metadata
middleware
Minio
mixin
mixins
multi
@@ -96,7 +94,6 @@ renderer
renderers
reportlab
SaaS
scalability
screenshot
scss
searchable
@@ -127,7 +124,6 @@ unconfigured
unix
unprefixed
untrusted
uptime
username
url
versa

View File

@@ -1 +1 @@
__version__ = "2.9.0.dev0"
__version__ = "2.6.0"

View File

@@ -1,5 +1,4 @@
from django.contrib.auth.models import AnonymousUser
from django_scopes import scopes_disabled
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
@@ -13,8 +12,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
model = self.get_model()
try:
with scopes_disabled():
device = model.objects.select_related('organizer').get(api_token=key)
device = model.objects.select_related('organizer').get(api_token=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token.')

View File

@@ -1,9 +1,8 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Device, Event, User
from pretix.base.models.auth import SuperuserPermissionSet
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models import Device, Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
)
@@ -38,24 +37,21 @@ class EventPermission(BasePermission):
slug=request.resolver_match.kwargs['event'],
organizer__slug=request.resolver_match.kwargs['organizer'],
).select_related('organizer').first()
if not request.event or not perm_holder.has_event_permission(request.event.organizer, request.event, request=request):
if not request.event or not perm_holder.has_event_permission(request.event.organizer, request.event):
return False
request.organizer = request.event.organizer
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
if required_permission and required_permission not in request.eventpermset:
return False
elif 'organizer' in request.resolver_match.kwargs:
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
request.organizer = Organizer.objects.filter(
slug=request.resolver_match.kwargs['organizer'],
).first()
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer):
return False
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
if required_permission and required_permission not in request.orgapermset:
return False

View File

@@ -4,13 +4,10 @@ from hashlib import sha1
from django.conf import settings
from django.db import transaction
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.urls import resolve
from django.utils.timezone import now
from django_scopes import scope
from rest_framework import status
from pretix.api.models import ApiCall
from pretix.base.models import Organizer
class IdempotencyMiddleware:
@@ -92,21 +89,3 @@ class IdempotencyMiddleware:
for k, v in json.loads(call.response_headers).values():
r[k] = v
return r
class ApiScopeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: HttpRequest):
if not request.path.startswith('/api/'):
return self.get_response(request)
url = resolve(request.path_info)
if 'organizer' in url.kwargs:
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
).first()
with scope(organizer=getattr(request, 'organizer', None)):
return self.get_response(request)

View File

@@ -164,7 +164,6 @@ class CloneEventSerializer(EventSerializer):
def create(self, validated_data):
plugins = validated_data.pop('plugins', None)
is_public = validated_data.pop('is_public', None)
testmode = validated_data.pop('testmode', None)
new_event = super().create(validated_data)
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
@@ -174,8 +173,6 @@ class CloneEventSerializer(EventSerializer):
new_event.set_active_plugins(plugins)
if is_public is not None:
new_event.is_public = is_public
if testmode is not None:
new_event.testmode = testmode
new_event.save()
return new_event
@@ -202,7 +199,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'event', 'is_public',
'presale_start', 'presale_end', 'location', 'event',
'item_price_overrides', 'variation_price_overrides', 'meta_data')
def validate(self, data):

View File

@@ -207,8 +207,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
class Meta:
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value',
'hidden')
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)

View File

@@ -179,55 +179,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
self.fields.pop('pdf_data')
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.order.checkin_attention or instance.item.checkin_attention
class AttendeeNameField(serializers.Field):
def to_representation(self, instance: OrderPosition):
an = instance.attendee_name
if not an:
if instance.addon_to_id:
an = instance.addon_to.attendee_name
if not an:
try:
an = instance.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return an
class AttendeeNamePartsField(serializers.Field):
def to_representation(self, instance: OrderPosition):
an = instance.attendee_name
p = instance.attendee_name_parts
if not an:
if instance.addon_to_id:
an = instance.addon_to.attendee_name
p = instance.addon_to.attendee_name_parts
if not an:
try:
p = instance.order.invoice_address.name_parts
except InvoiceAddress.DoesNotExist:
pass
return p
class CheckinListOrderPositionSerializer(OrderPositionSerializer):
require_attention = RequireAttentionField(source='*')
attendee_name = AttendeeNameField(source='*')
attendee_name_parts = AttendeeNamePartsField(source='*')
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
class Meta:
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'require_attention',
'order__status')
class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
def to_representation(self, instance: Order):

View File

@@ -2,7 +2,6 @@ from datetime import timedelta
from django.dispatch import Signal, receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.api.models import ApiCall, WebHookCall
from pretix.base.signals import periodic_task
@@ -18,12 +17,10 @@ instances.
@receiver(periodic_task)
@scopes_disabled()
def cleanup_webhook_logs(sender, **kwargs):
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
@receiver(periodic_task)
@scopes_disabled()
def cleanup_api_logs(sender, **kwargs):
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()

View File

@@ -6,7 +6,6 @@ from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.fields import DateTimeField
@@ -14,7 +13,7 @@ from rest_framework.response import Response
from pretix.api.serializers.checkin import CheckinListSerializer
from pretix.api.serializers.item import QuestionSerializer
from pretix.api.serializers.order import CheckinListOrderPositionSerializer
from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import (
@@ -25,11 +24,11 @@ from pretix.base.services.checkin import (
)
from pretix.helpers.database import FixedOrderBy
with scopes_disabled():
class CheckinListFilter(FilterSet):
class Meta:
model = CheckinList
fields = ['subevent']
class CheckinListFilter(FilterSet):
class Meta:
model = CheckinList
fields = ['subevent']
class CheckinListViewSet(viewsets.ModelViewSet):
@@ -147,16 +146,15 @@ class CheckinListViewSet(viewsets.ModelViewSet):
return Response(response)
with scopes_disabled():
class CheckinOrderPositionFilter(OrderPositionFilter):
class CheckinOrderPositionFilter(OrderPositionFilter):
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(last_checked_in__isnull=not value)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(last_checked_in__isnull=not value)
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CheckinListOrderPositionSerializer
queryset = OrderPosition.all.none()
serializer_class = OrderPositionSerializer
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('attendee_name_cached', 'positionid')
ordering_fields = (
@@ -191,7 +189,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
except ValueError:
raise Http404()
def get_queryset(self, ignore_status=False):
def get_queryset(self):
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.checkinlist.pk
@@ -201,15 +199,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID],
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
)
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
qs = qs.filter(
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID]
)
if self.request.query_params.get('pdf_data', 'false') == 'true':
qs = qs.prefetch_related(
Prefetch(
@@ -231,7 +225,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address'
'item', 'variation', 'item__category', 'addon_to'
)
else:
qs = qs.prefetch_related(
@@ -241,7 +235,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order')
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address')
if not self.checkinlist.all_products:
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
@@ -253,7 +247,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
force = bool(self.request.data.get('force', False))
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
op = self.get_object(ignore_status=True)
op = self.get_object()
if 'datetime' in self.request.data:
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
@@ -287,7 +281,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
return Response({
'status': 'incomplete',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data,
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data,
'questions': [
QuestionSerializer(q).data for q in e.questions
]
@@ -297,17 +291,17 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'status': 'error',
'reason': e.code,
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=400)
else:
return Response({
'status': 'ok',
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
'position': OrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201)
def get_object(self, ignore_status=False):
queryset = self.filter_queryset(self.get_queryset(ignore_status=ignore_status))
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
if self.kwargs['pk'].isnumeric():
obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:

View File

@@ -3,7 +3,6 @@ from django.db import transaction
from django.db.models import ProtectedError, Q
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import filters, viewsets
from rest_framework.exceptions import PermissionDenied
@@ -19,51 +18,51 @@ from pretix.base.models import (
from pretix.base.models.event import SubEvent
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
class EventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class Meta:
model = Event
fields = ['is_public', 'live', 'has_subevents']
class EventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
def ends_after_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
class Meta:
model = Event
fields = ['is_public', 'live', 'has_subevents']
def ends_after_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
)
return queryset.filter(expr)
def is_past_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_past_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
def is_future_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = (
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
class EventViewSet(viewsets.ModelViewSet):
@@ -73,8 +72,6 @@ class EventViewSet(viewsets.ModelViewSet):
lookup_url_kwarg = 'event'
permission_classes = (EventCRUDPermission,)
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
ordering = ('slug',)
ordering_fields = ('date_from', 'slug')
filterset_class = EventFilter
def get_queryset(self):
@@ -183,42 +180,41 @@ class CloneEventViewSet(viewsets.ModelViewSet):
)
with scopes_disabled():
class SubEventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class SubEventFilter(FilterSet):
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
class Meta:
model = SubEvent
fields = ['active', 'event__live']
class Meta:
model = SubEvent
fields = ['active', 'event__live']
def ends_after_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
def ends_after_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=value))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=value))
)
return queryset.filter(expr)
def is_past_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_past_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
def is_future_qs(self, queryset, name, value):
expr = Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
)
if value:
return queryset.filter(expr)
else:
return queryset.exclude(expr)
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
@@ -246,14 +242,8 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
)
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
super().perform_update(serializer)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
serializer.instance.log_action(
'pretix.subevent.changed',
user=self.request.user,

View File

@@ -3,7 +3,6 @@ from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
@@ -22,19 +21,19 @@ from pretix.base.models import (
)
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
class ItemFilter(FilterSet):
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
def tax_rate_qs(self, queryset, name, value):
if value in ("0", "None", "0.00"):
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
else:
return queryset.filter(tax_rule__rate=value)
class ItemFilter(FilterSet):
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
class Meta:
model = Item
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
def tax_rate_qs(self, queryset, name, value):
if value in ("0", "None", "0.00"):
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
else:
return queryset.filter(tax_rule__rate=value)
class Meta:
model = Item
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
@@ -66,14 +65,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
return ctx
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
serializer.save(event=self.request.event)
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
serializer.instance.log_action(
'pretix.event.item.changed',
user=self.request.user,
@@ -320,11 +312,10 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
super().perform_destroy(instance)
with scopes_disabled():
class QuestionFilter(FilterSet):
class Meta:
model = Question
fields = ['ask_during_checkin', 'required', 'identifier']
class QuestionFilter(FilterSet):
class Meta:
model = Question
fields = ['ask_during_checkin', 'required', 'identifier']
class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
@@ -420,11 +411,10 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
super().perform_destroy(instance)
with scopes_disabled():
class QuotaFilter(FilterSet):
class Meta:
model = Quota
fields = ['subevent']
class QuotaFilter(FilterSet):
class Meta:
model = Quota
fields = ['subevent']
class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
@@ -462,17 +452,9 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
return ctx
def perform_update(self, serializer):
original_data = self.get_serializer(instance=serializer.instance).data
current_subevent = serializer.instance.subevent
serializer.save(event=self.request.event)
request_subevent = serializer.instance.subevent
if serializer.data == original_data:
# Performance optimization: If nothing was changed, we do not need to save or log anything.
# This costs us a few cycles on save, but avoids thousands of lines in our log.
return
serializer.instance.log_action(
'pretix.event.quota.changed',
user=self.request.user,

View File

@@ -11,7 +11,6 @@ from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import (
@@ -51,17 +50,17 @@ from pretix.base.signals import (
)
from pretix.base.templatetags.money import money_filter
with scopes_disabled():
class OrderFilter(FilterSet):
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
class OrderFilter(FilterSet):
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
class OrderViewSet(viewsets.ModelViewSet):
@@ -483,7 +482,6 @@ class OrderViewSet(viewsets.ModelViewSet):
)
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
serializer.instance.email_known_to_work = False
serializer.instance.log_action(
'pretix.event.order.contact.changed',
user=self.request.user,
@@ -516,7 +514,6 @@ class OrderViewSet(viewsets.ModelViewSet):
)
serializer.save()
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.event.pk, 'order': serializer.instance.pk})
if 'invoice_address' in self.request.data:
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
@@ -532,49 +529,48 @@ class OrderViewSet(viewsets.ModelViewSet):
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
with scopes_disabled():
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
search = django_filters.CharFilter(method='search_qs')
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(attendee_email__icontains=value)
| Q(addon_to__attendee_email__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
)
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(attendee_email__icontains=value)
| Q(addon_to__attendee_email__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def has_checkin_qs(self, queryset, name, value):
return queryset.filter(checkins__isnull=not value)
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
def attendee_name_qs(self, queryset, name, value):
return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value))
class Meta:
model = OrderPosition
fields = {
'item': ['exact', 'in'],
'variation': ['exact', 'in'],
'secret': ['exact'],
'order__status': ['exact', 'in'],
'addon_to': ['exact', 'in'],
'subevent': ['exact', 'in'],
'pseudonymization_id': ['exact'],
'voucher__code': ['exact'],
'voucher': ['exact'],
}
class Meta:
model = OrderPosition
fields = {
'item': ['exact', 'in'],
'variation': ['exact', 'in'],
'secret': ['exact'],
'order__status': ['exact', 'in'],
'addon_to': ['exact', 'in'],
'subevent': ['exact', 'in'],
'pseudonymization_id': ['exact'],
'voucher__code': ['exact'],
'voucher': ['exact'],
}
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.all.none()
queryset = OrderPosition.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
@@ -962,23 +958,22 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer.save()
with scopes_disabled():
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs')
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
def refers_qs(self, queryset, name, value):
return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value)
def refers_qs(self, queryset, name, value):
return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
class Meta:
model = Invoice
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
class Meta:
model = Invoice
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
class RetryException(APIException):

View File

@@ -1,4 +1,4 @@
from rest_framework import filters, viewsets
from rest_framework import viewsets
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import OrganizerSerializer
@@ -10,9 +10,6 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Organizer.objects.none()
lookup_field = 'slug'
lookup_url_kwarg = 'organizer'
filter_backends = (filters.OrderingFilter,)
ordering = ('slug',)
ordering_fields = ('name', 'slug')
def get_queryset(self):
if self.request.user.is_authenticated:

View File

@@ -6,7 +6,6 @@ from django.utils.timezone import now
from django_filters.rest_framework import (
BooleanFilter, DjangoFilterBackend, FilterSet,
)
from django_scopes import scopes_disabled
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
@@ -16,22 +15,22 @@ from rest_framework.response import Response
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
with scopes_disabled():
class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active')
class Meta:
model = Voucher
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active')
def filter_active(self, queryset, name, value):
if value:
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
else:
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
class Meta:
model = Voucher
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
def filter_active(self, queryset, name, value):
if value:
return queryset.filter(Q(redeemed__lt=F('max_usages')) &
(Q(valid_until__isnull=True) | Q(valid_until__gt=now())))
else:
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
class VoucherViewSet(viewsets.ModelViewSet):

View File

@@ -1,6 +1,5 @@
import django_filters
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
@@ -11,16 +10,16 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.models import WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
with scopes_disabled():
class WaitingListFilter(FilterSet):
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
def has_voucher_qs(self, queryset, name, value):
return queryset.filter(voucher__isnull=not value)
class WaitingListFilter(FilterSet):
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
class Meta:
model = WaitingListEntry
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
def has_voucher_qs(self, queryset, name, value):
return queryset.filter(voucher__isnull=not value)
class Meta:
model = WaitingListEntry
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
class WaitingListViewSet(viewsets.ModelViewSet):

View File

@@ -8,7 +8,6 @@ from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django_scopes import scope, scopes_disabled
from requests import RequestException
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
@@ -204,52 +203,51 @@ def notify_webhooks(logentry_id: int):
@app.task(base=ProfiledTask, bind=True, max_retries=9)
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
# 9 retries with 2**(2*x) timing is roughly 72 hours
with scopes_disabled():
webhook = WebHook.objects.get(id=webhook_id)
with scope(organizer=webhook.organizer):
logentry = LogEntry.all.get(id=logentry_id)
types = get_all_webhook_events()
event_type = types.get(action_type)
if not event_type or not webhook.enabled:
return # Ignore, e.g. plugin not installed
logentry = LogEntry.all.get(id=logentry_id)
webhook = WebHook.objects.get(id=webhook_id)
payload = event_type.build_payload(logentry)
t = time.time()
types = get_all_webhook_events()
event_type = types.get(action_type)
if not event_type or not webhook.enabled:
return # Ignore, e.g. plugin not installed
payload = event_type.build_payload(logentry)
t = time.time()
try:
try:
try:
resp = requests.post(
webhook.target_url,
json=payload,
allow_redirects=False
)
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=resp.status_code,
payload=json.dumps(payload),
response_body=resp.text[:1024 * 1024],
success=200 <= resp.status_code <= 299
)
if resp.status_code == 410:
webhook.enabled = False
webhook.save()
elif resp.status_code > 299:
raise self.retry(countdown=2 ** (self.request.retries * 2))
except RequestException as e:
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=0,
payload=json.dumps(payload),
response_body=str(e)[:1024 * 1024]
)
resp = requests.post(
webhook.target_url,
json=payload,
allow_redirects=False
)
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=resp.status_code,
payload=json.dumps(payload),
response_body=resp.text[:1024 * 1024],
success=200 <= resp.status_code <= 299
)
if resp.status_code == 410:
webhook.enabled = False
webhook.save()
elif resp.status_code > 299:
raise self.retry(countdown=2 ** (self.request.retries * 2))
except MaxRetriesExceededError:
pass
except RequestException as e:
WebHookCall.objects.create(
webhook=webhook,
action_type=logentry.action_type,
target_url=webhook.target_url,
is_retry=self.request.retries > 0,
execution_time=time.time() - t,
return_code=0,
payload=json.dumps(payload),
response_body=str(e)[:1024 * 1024]
)
raise self.retry(countdown=2 ** (self.request.retries * 2))
except MaxRetriesExceededError:
pass

View File

@@ -8,7 +8,7 @@ from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.models import Event, Order
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
@@ -44,8 +44,7 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
position: OrderPosition=None) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -53,7 +52,6 @@ class BaseHTMLMailRenderer:
:param plain_signature: The signature with event organizer contact details in plain text.
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:param position: The order position if this email is connected to one, otherwise ``None``.
:return: An HTML string
"""
raise NotImplementedError()
@@ -97,7 +95,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
body_md = markdown_compile_email(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
@@ -118,9 +116,6 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if order:
htmlctx['order'] = order
if position:
htmlctx['position'] = position
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
return body_html

View File

@@ -1,5 +1,4 @@
from .answers import * # noqa
from .dekodi import * # noqa
from .invoices import * # noqa
from .json import * # noqa
from .mail import * # noqa

View File

@@ -40,7 +40,6 @@ class AnswerFilesExporter(BaseExporter):
if form_data.get('questions'):
qs = qs.filter(question__in=form_data['questions'])
with tempfile.TemporaryDirectory() as d:
any = False
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
if i.file:
@@ -52,12 +51,9 @@ class AnswerFilesExporter(BaseExporter):
i.question.pk,
os.path.basename(i.file.name).split('.', 1)[1]
)
any = True
zipf.writestr(fname, i.file.read())
i.file.close()
if not any:
return None
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()

View File

@@ -1,219 +0,0 @@
import json
from collections import OrderedDict
from decimal import Decimal
import dateutil
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
from django.dispatch import receiver
from django.utils.translation import ugettext, ugettext_lazy
from pretix.base.i18n import language
from pretix.base.models import Invoice, OrderPayment
from ..exporter import BaseExporter
from ..signals import register_data_exporters
class DekodiNREIExporter(BaseExporter):
identifier = 'dekodi_nrei'
verbose_name = 'dekodi NREI (JSON)'
# Specification: http://manuals.dekodi.de/nexuspub/schnittstellenbuch/
def _encode_invoice(self, invoice: Invoice):
p_last = invoice.order.payments.filter(state=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED]).last()
gross_total = Decimal('0.00')
net_total = Decimal('0.00')
positions = []
for l in invoice.lines.all():
positions.append({
'ADes': l.description.replace("<br />", "\n"),
'ANetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
'ANo': self.event.slug,
'AQ': -1 if invoice.is_cancellation else 1,
'AVatP': round(float(l.tax_rate), 2),
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
'PosGrossA': round(float(l.gross_value), 2),
'PosNetA': round(float(l.net_value), 2),
})
gross_total += l.gross_value
net_total += l.net_value
payments = []
paypal_email = None
for p in invoice.order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_REFUNDED)
):
if p.provider == 'paypal':
paypal_email = p.info_data.get('payer', {}).get('payer_info', {}).get('email')
try:
ppid = p.info_data['transactions'][0]['related_resources'][0]['sale']['id']
except:
ppid = p.info_data.get('id')
payments.append({
'PTID': '1',
'PTN': 'PayPal',
'PTNo1': ppid,
'PTNo2': p.info_data.get('id'),
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency),
'PTNo11': paypal_email or '',
'PTNo15': p.full_id or '',
})
elif p.provider == 'banktransfer':
payments.append({
'PTID': '4',
'PTN': 'Vorkasse',
'PTNo4': p.info_data.get('reference') or p.payment_provider._code(invoice.order),
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency),
'PTNo10': p.info_data.get('payer') or '',
'PTNo14': p.info_data.get('date') or '',
'PTNo15': p.full_id or '',
})
elif p.provider == 'sepadebit':
with language(invoice.order.locale):
payments.append({
'PTID': '5',
'PTN': 'Lastschrift',
'PTNo4': ugettext('Event ticket {event}-{code}').format(
event=self.event.slug.upper(),
code=invoice.order.code
),
'PTNo5': p.info_data.get('iban') or '',
'PTNo6': p.info_data.get('bic') or '',
'PTNo7': round(float(p.amount), 2),
'PTNo8': str(self.event.currency) or '',
'PTNo9': p.info_data.get('date') or '',
'PTNo10': p.info_data.get('account') or '',
'PTNo14': p.info_data.get('reference') or '',
'PTNo15': p.full_id or '',
})
elif p.provider.startswith('stripe'):
src = p.info_data.get("source", p.info_data)
payments.append({
'PTID': '81',
'PTN': 'Stripe',
'PTNo1': p.info_data.get("id") or '',
'PTNo5': src.get("card", {}).get("last4") or '',
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo10': src.get('owner', {}).get('verified_name') or src.get('owner', {}).get('name') or '',
'PTNo15': p.full_id or '',
})
else:
payments.append({
'PTID': '0',
'PTN': p.provider,
'PTNo7': round(float(p.amount), 2) or '',
'PTNo8': str(self.event.currency) or '',
'PTNo15': p.full_id or '',
})
payments = [
{
k: v for k, v in p.items() if v is not None
} for p in payments
]
hdr = {
'C': str(invoice.invoice_to_country) or self.event.settings.invoice_address_from_country,
'CC': self.event.currency,
'City': invoice.invoice_to_city,
'CN': invoice.invoice_to_company,
'DIC': self.event.settings.invoice_address_from_country,
# DIC is a little bit unclean, should be the event location's country
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'DT': '30' if invoice.is_cancellation else '10',
'EM': invoice.order.email,
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1],
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
'INo': invoice.full_invoice_no,
'IsNet': invoice.reverse_charge,
'ODt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'OID': invoice.order.code,
'SID': self.event.slug,
'SN': str(self.event),
'Str': invoice.invoice_to_street or '',
'TGrossA': round(float(gross_total), 2),
'TNetA': round(float(net_total), 2),
'TVatA': round(float(gross_total - net_total), 2),
'VatDp': False,
'Zip': invoice.invoice_to_zipcode
}
if not hdr['FamN'] and not hdr['CN']:
hdr['CN'] = "Unbekannter Kunde"
if invoice.refers:
hdr['PvrINo'] = invoice.refers.full_invoice_no
if p_last:
hdr['PmDt'] = p_last.payment_date.isoformat().replace('Z', '+00:00')
if paypal_email:
hdr['PPEm'] = paypal_email
if invoice.invoice_to_vat_id:
hdr['VatID'] = invoice.invoice_to_vat_id
return {
'IsValid': True,
'Hdr': hdr,
'InvcPstns': positions,
'PmIs': payments,
'ValidationMessage': ''
}
def render(self, form_data):
qs = self.event.invoices.select_related('order').prefetch_related('lines', 'lines__subevent')
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__gte=date_value)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(date__lte=date_value)
jo = {
'Format': 'NREI',
'Version': '18.10.2.0',
'SourceSystem': 'pretix',
'Data': [
self._encode_invoice(i) for i in qs
]
}
return '{}_nrei.json'.format(self.event.slug), 'application/json', json.dumps(jo, cls=DjangoJSONEncoder, indent=4)
@property
def export_form_fields(self):
return OrderedDict(
[
('date_from',
forms.DateField(
label=ugettext_lazy('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
'not always correspond to the order or payment date.')
)),
('date_to',
forms.DateField(
label=ugettext_lazy('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
'does not always correspond to the order or payment date.')
)),
]
)
@receiver(register_data_exporters, dispatch_uid="exporter_dekodi_nrei")
def register_dekodi_export(sender, **kwargs):
return DekodiNREIExporter

View File

@@ -46,7 +46,6 @@ class InvoiceExporter(BaseExporter):
qs = qs.filter(date__lte=date_value)
with tempfile.TemporaryDirectory() as d:
any = False
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for i in qs:
try:
@@ -55,19 +54,14 @@ class InvoiceExporter(BaseExporter):
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
except FileNotFoundError:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
i.file.open('rb')
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
any = True
i.file.close()
if not any:
return None
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()

View File

@@ -6,7 +6,7 @@ from django import forms
from django.db.models import DateTimeField, F, Max, OuterRef, Subquery, Sum
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition,
@@ -269,10 +269,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Status'),
_('Email'),
_('Order date'),
]
if self.event.has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers += [
_('Product'),
_('Variation'),
_('Price'),
@@ -315,10 +311,6 @@ class OrderListExporter(MultiSheetListExporter):
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
]
if self.event.has_subevents:
row.append(op.subevent)
row += [
str(op.item),
str(op.variation) if op.variation else '',
op.price,

View File

@@ -11,9 +11,7 @@ from django.contrib import messages
from django.core.exceptions import ValidationError
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import get_language, ugettext_lazy as _
from django_countries import countries
from django_countries.fields import Country, CountryField
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms.widgets import (
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
@@ -215,14 +213,6 @@ class BaseQuestionsForm(forms.Form):
widget=forms.Textarea,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_COUNTRYCODE:
field = CountryField().formfield(
label=label, required=required,
help_text=help_text,
widget=forms.Select,
empty_label='',
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField(
queryset=q.options,
@@ -352,27 +342,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.request = kwargs.pop('request', None)
self.validate_vat_id = kwargs.pop('validate_vat_id')
self.all_optional = kwargs.pop('all_optional', False)
kwargs.setdefault('initial', {})
if not kwargs.get('instance') or not kwargs['instance'].country:
# Try to guess the initial country from either the country of the merchant
# or the locale. This will hopefully save at least some users some scrolling :)
locale = get_language()
country = event.settings.invoice_address_from_country
if not country:
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale in valid_countries:
country = Country(locale.upper())
kwargs['initial']['country'] = country
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
@@ -424,12 +393,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.instance.name_parts = data.get('name_parts')
if all(
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
) and len(data.get('name_parts', {})) == 1:
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):

View File

@@ -1,51 +0,0 @@
"""
Django, for theoretically very valid reasons, creates migrations for *every single thing*
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
database backend unknown to us might actually use this information for its database schema.
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
certain that some changes to models will never require a change to the database. In this case,
not creating a migration for certain changes will save us some performance while applying them
*and* allow for a cleaner git history. Win-win!
Only caveat is that we need to do some dirty monkeypatching to achieve it...
"""
from django.core.management.commands.makemigrations import Command as Parent
from django.db import models
from django.db.migrations.operations import models as modelops
from django_countries.fields import CountryField
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name_plural")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("ordering")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("get_latest_by")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, blacklist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
(models.Field, 'editable', [models.DateField, models.DateTimeField, models.DateField, models.BinaryField]),
(models.Field, 'blank', [models.DateField, models.DateTimeField, models.AutoField, models.NullBooleanField,
models.TimeField]),
(models.CharField, 'choices', [CountryField])
]
original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, blacklist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in blacklist):
kwargs.pop(attr, None)
return name, path, args, kwargs
models.Field.deconstruct = new_deconstruct
class Command(Parent):
pass

View File

@@ -1,28 +0,0 @@
"""
Django tries to be helpful by suggesting to run "makemigrations" in red font on every "migrate"
run when there are things we have no migrations for. Usually, this is intended, and running
"makemigrations" can really screw up the environment of a user, so we want to prevent novice
users from doing that by going really dirty and fitlering it from the output.
"""
import sys
from django.core.management.base import OutputWrapper
from django.core.management.commands.migrate import Command as Parent
class OutputFilter(OutputWrapper):
blacklist = (
"Your models have changes that are not yet reflected",
"Run 'manage.py makemigrations' to make new "
)
def write(self, msg, style_func=None, ending=None):
if any(b in msg for b in self.blacklist):
return
super().write(msg, style_func, ending)
class Command(Parent):
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
super().__init__(stdout, stderr, no_color, force_color)
self.stdout = OutputFilter(stdout or sys.stdout)

View File

@@ -1,22 +0,0 @@
# Generated by Django 2.2 on 2019-04-23 08:39
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0117_auto_20190418_1149'),
]
operations = [
migrations.AddField(
model_name='subevent',
name='is_public',
field=models.BooleanField(default=True, help_text='If selected, this event will show up publicly on the list of dates for your event.', verbose_name='Show in lists'),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 2.2 on 2019-05-09 06:54
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0118_auto_20190423_0839'),
]
operations = [
migrations.AddField(
model_name='question',
name='hidden',
field=models.BooleanField(default=False, help_text='This question will only show up in the backend.', verbose_name='Hidden question'),
),
]

View File

@@ -1,77 +0,0 @@
# Generated by Django 2.2 on 2019-05-09 07:36
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0119_auto_20190509_0654'),
]
operations = [
migrations.AlterField(
model_name='cartposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterField(
model_name='cartposition',
name='subevent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
),
migrations.AlterField(
model_name='cartposition',
name='voucher',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Voucher'),
),
migrations.AlterField(
model_name='event',
name='is_public',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='invoiceaddress',
name='name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterField(
model_name='item',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=['web']),
),
migrations.AlterField(
model_name='order',
name='sales_channel',
field=models.CharField(default='web', max_length=190),
),
migrations.AlterField(
model_name='orderposition',
name='attendee_name_parts',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterField(
model_name='orderposition',
name='subevent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
),
migrations.AlterField(
model_name='orderposition',
name='voucher',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Voucher'),
),
migrations.AlterField(
model_name='staffsessionauditlog',
name='method',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(db_index=True, max_length=190, null=True, unique=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.1 on 2019-05-15 05:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0120_auto_20190509_0736'),
]
operations = [
migrations.AddField(
model_name='order',
name='email_known_to_work',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 2.2.1 on 2019-05-15 13:23
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0121_order_email_known_to_work'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='web_secret',
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_secret, max_length=32),
),
]

View File

@@ -12,7 +12,6 @@ from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
@@ -284,7 +283,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return True
return False
@scopes_disabled()
def get_events_with_any_permission(self, request=None):
"""
Returns a queryset of events the user has any permissions to.
@@ -302,7 +300,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
)
@scopes_disabled()
def get_events_with_permission(self, permission, request=None):
"""
Returns a queryset of events the user has a specific permissions to.

View File

@@ -6,9 +6,7 @@ from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from pretix.helpers.json import CustomJSONEncoder
@@ -115,40 +113,6 @@ class LoggedModel(models.Model, LoggingMixin):
class Meta:
abstract = True
@cached_property
def logs_content_type(self):
return ContentType.objects.get_for_model(type(self))
@cached_property
def all_logentries_link(self):
from pretix.base.models import Event
if isinstance(self, Event):
event = self
elif hasattr(self, 'event'):
event = self.event
else:
return None
return reverse(
'control:event.log',
kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}
) + '?content_type={}&object={}'.format(
self.logs_content_type.pk,
self.pk
)
def top_logentries(self):
qs = self.all_logentries()
if self.all_logentries_link:
qs = qs[:25]
return qs
def top_logentries_has_more(self):
return self.all_logentries().count() > 25
def all_logentries(self):
"""
Returns all log entries that are attached to this object.
@@ -158,7 +122,7 @@ class LoggedModel(models.Model, LoggingMixin):
from .log import LogEntry
return LogEntry.objects.filter(
content_type=self.logs_content_type, object_id=self.pk
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')

View File

@@ -3,7 +3,6 @@ from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
from django.db.models.functions import Coalesce
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.models import LoggedModel
@@ -21,8 +20,6 @@ class CheckinList(LoggedModel):
'order have not been paid. This only works with pretixdesk '
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
objects = ScopedManager(organizer='event__organizer')
class Meta:
ordering = ('subevent__date_from', 'name')
@@ -170,8 +167,6 @@ class Checkin(models.Model):
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
objects = ScopedManager(organizer='position__order__event__organizer')
class Meta:
unique_together = (('list', 'position'),)

View File

@@ -4,7 +4,6 @@ from django.db import models
from django.db.models import Max
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.models import LoggedModel
@@ -72,8 +71,6 @@ class Device(LoggedModel):
null=True, blank=True
)
objects = ScopedManager(organizer='organizer')
class Meta:
unique_together = (('organizer', 'device_id'),)

View File

@@ -17,7 +17,6 @@ from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
@@ -165,7 +164,7 @@ class EventMixin:
def annotated(cls, qs, channel='web'):
from pretix.base.models import Item, ItemVariation, Quota
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter(
sq_active_item = Item.objects.filter_available(channel=channel).filter(
Q(variations__isnull=True)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
@@ -187,7 +186,7 @@ class EventMixin:
Prefetch(
'quotas',
to_attr='active_quotas',
queryset=Quota.objects.using(settings.DATABASE_REPLICA).annotate(
queryset=Quota.objects.annotate(
active_items=Subquery(sq_active_item, output_field=models.TextField()),
active_variations=Subquery(sq_active_variation, output_field=models.TextField()),
).exclude(
@@ -337,8 +336,6 @@ class Event(EventMixin, LoggedModel):
default=False
)
objects = ScopedManager(organizer='organizer')
class Meta:
verbose_name = _("Event")
verbose_name_plural = _("Events")
@@ -448,7 +445,6 @@ class Event(EventMixin, LoggedModel):
self.plugins = other.plugins
self.is_public = other.is_public
self.testmode = other.testmode
self.save()
tax_map = {}
@@ -643,6 +639,22 @@ class Event(EventMixin, LoggedModel):
irs = self.get_invoice_renderers()
return irs[self.settings.invoice_renderer]
@property
def active_subevents(self):
"""
Returns a queryset of active subevents.
"""
return self.subevents.filter(active=True).order_by('-date_from', 'name')
@property
def active_future_subevents(self):
return self.subevents.filter(
Q(active=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(date_to__gte=now())
)
).order_by('date_from', 'name')
def subevents_annotated(self, channel):
return SubEvent.annotated(self.subevents, channel)
@@ -655,7 +667,7 @@ class Event(EventMixin, LoggedModel):
'name_descending': ('-name', 'date_from'),
}[ordering]
subevs = queryset.filter(
Q(active=True) & Q(is_public=True) & (
Q(active=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
| Q(date_to__gte=now() - timedelta(hours=24))
)
@@ -821,8 +833,6 @@ class SubEvent(EventMixin, LoggedModel):
:type event: Event
:param active: Whether to show the subevent
:type active: bool
:param is_public: Whether to show the subevent in lists
:type is_public: bool
:param name: This event's full title
:type name: str
:param date_from: The datetime this event starts
@@ -841,10 +851,6 @@ class SubEvent(EventMixin, LoggedModel):
active = models.BooleanField(default=False, verbose_name=_("Active"),
help_text=_("Only with this checkbox enabled, this date is visible in the "
"frontend to users."))
is_public = models.BooleanField(default=True,
verbose_name=_("Show in lists"),
help_text=_("If selected, this event will show up publicly on the list of dates "
"for your event."))
name = I18nCharField(
max_length=200,
verbose_name=_("Name"),
@@ -878,8 +884,6 @@ class SubEvent(EventMixin, LoggedModel):
items = models.ManyToManyField('Item', through='SubEventItem')
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
objects = ScopedManager(organizer='event__organizer')
class Meta:
verbose_name = _("Date in event series")
verbose_name_plural = _("Dates in event series")

View File

@@ -9,7 +9,6 @@ from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import pgettext
from django_countries.fields import CountryField
from django_scopes import ScopedManager
def invoice_filename(instance, filename: str) -> str:
@@ -108,8 +107,6 @@ class Invoice(models.Model):
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
internal_reference = models.TextField(blank=True)
objects = ScopedManager(organizer='event__organizer')
@staticmethod
def _to_numeric_invoice_number(number):
return '{:05d}'.format(int(number))

View File

@@ -16,8 +16,6 @@ from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import Country
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models import fields
@@ -156,41 +154,28 @@ class SubEventItemVariation(models.Model):
self.subevent.event.cache.clear()
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
q = (
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
)
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
qs = qs.filter(q)
vouchq = Q(hide_without_voucher=False)
if voucher:
if voucher.item_id:
vouchq |= Q(pk=voucher.item_id)
qs = qs.filter(pk=voucher.item_id)
elif voucher.quota_id:
qs = qs.filter(quotas__in=[voucher.quota_id])
return qs.filter(vouchq)
class ItemQuerySet(models.QuerySet):
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self, channel, voucher, allow_addons)
q = (
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
)
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
qs = self.filter(q)
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
def __init__(self):
super().__init__()
self._queryset_class = ItemQuerySet
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
vouchq = Q(hide_without_voucher=False)
if voucher:
if voucher.item_id:
vouchq |= Q(pk=voucher.item_id)
qs = qs.filter(pk=voucher.item_id)
elif voucher.quota_id:
qs = qs.filter(quotas__in=[voucher.quota_id])
return qs.filter(vouchq)
class Item(LoggedModel):
@@ -240,7 +225,7 @@ class Item(LoggedModel):
:type sales_channels: bool
"""
objects = ItemQuerySetManager()
objects = ItemQuerySet.as_manager()
event = models.ForeignKey(
Event,
@@ -347,9 +332,7 @@ class Item(LoggedModel):
require_bundling = models.BooleanField(
verbose_name=_('Only sell this product as part of a bundle'),
default=False,
help_text=_('If this option is set, the product will only be sold as part of bundle products. Do '
'<strong>not</strong> check this option if you want to use this product as an add-on product, '
'but only for fixed bundles!')
help_text=_('If this option is set, the product will only be sold as part of bundle products.')
)
allow_cancel = models.BooleanField(
verbose_name=_('Allow product to be canceled'),
@@ -605,8 +588,6 @@ class ItemVariation(models.Model):
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
)
objects = ScopedManager(organizer='item__event__organizer')
class Meta:
verbose_name = _("Product variation")
verbose_name_plural = _("Product variations")
@@ -912,8 +893,6 @@ class Question(LoggedModel):
:param items: A set of ``Items`` objects that this question should be applied to
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
:type ask_during_checkin: bool
:param hidden: Whether to only show the question in the backend
:type hidden: bool
:param identifier: An arbitrary, internal identifier
:type identifier: str
:param dependency_question: This question will only show up if the referenced question is set to `dependency_value`.
@@ -931,7 +910,6 @@ class Question(LoggedModel):
TYPE_DATE = "D"
TYPE_TIME = "H"
TYPE_DATETIME = "W"
TYPE_COUNTRYCODE = "CC"
TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")),
@@ -943,7 +921,6 @@ class Question(LoggedModel):
(TYPE_DATE, _("Date")),
(TYPE_TIME, _("Time")),
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
)
event = models.ForeignKey(
@@ -991,18 +968,11 @@ class Question(LoggedModel):
'pretixdesk 0.2 or newer.'),
default=False
)
hidden = models.BooleanField(
verbose_name=_('Hidden question'),
help_text=_('This question will only show up in the backend.'),
default=False
)
dependency_question = models.ForeignKey(
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
)
dependency_value = models.TextField(null=True, blank=True)
objects = ScopedManager(organizer='event__organizer')
class Meta:
verbose_name = _("Question")
verbose_name_plural = _("Questions")
@@ -1101,12 +1071,6 @@ class Question(LoggedModel):
return dt
except:
raise ValidationError(_('Invalid datetime input.'))
elif self.type == Question.TYPE_COUNTRYCODE and answer:
c = Country(answer.upper())
if c.name:
return answer
else:
raise ValidationError(_('Unknown country code.'))
return answer
@@ -1252,8 +1216,6 @@ class Quota(LoggedModel):
cached_availability_paid_orders = models.PositiveIntegerField(null=True, blank=True)
cached_availability_time = models.DateTimeField(null=True, blank=True)
objects = ScopedManager(organizer='event__organizer')
class Meta:
verbose_name = _("Quota")
verbose_name_plural = _("Quotas")
@@ -1328,8 +1290,7 @@ class Quota(LoggedModel):
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
'cached_availability_paid_orders'
],
clear_cache=False,
using='default'
clear_cache=False
)
if _cache is not None:

View File

@@ -1,5 +1,4 @@
import copy
import hashlib
import json
import logging
import os
@@ -15,7 +14,7 @@ from django.db import models, transaction
from django.db.models import (
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
)
from django.db.models.functions import Coalesce, Greatest
from django.db.models.functions import Coalesce
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
@@ -25,8 +24,7 @@ from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import Country, CountryField
from django_scopes import ScopedManager, scopes_disabled
from django_countries.fields import CountryField
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
@@ -34,7 +32,6 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LockModel, LoggedModel
@@ -182,12 +179,6 @@ class Order(LockModel, LoggedModel):
default=False
)
sales_channel = models.CharField(max_length=190, default="web")
email_known_to_work = models.BooleanField(
default=False,
verbose_name=_('E-mail address verified')
)
objects = ScopedManager(organizer='event__organizer')
class Meta:
verbose_name = _("Order")
@@ -198,8 +189,6 @@ class Order(LockModel, LoggedModel):
return self.full_code
def gracefully_delete(self, user=None, auth=None):
from . import Voucher
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
self.event.log_action(
@@ -208,12 +197,6 @@ class Order(LockModel, LoggedModel):
'code': self.code,
}
)
if self.status != Order.STATUS_CANCELED:
for position in self.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
@@ -222,9 +205,6 @@ class Order(LockModel, LoggedModel):
self.event.cache.delete('complain_testmode_orders')
self.delete()
def email_confirm_hash(self):
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
@property
def fees(self):
"""
@@ -234,7 +214,6 @@ class Order(LockModel, LoggedModel):
return self.all_fees(manager='objects')
@cached_property
@scopes_disabled()
def count_positions(self):
if hasattr(self, 'pcnt'):
return self.pcnt or 0
@@ -258,7 +237,6 @@ class Order(LockModel, LoggedModel):
return None
@property
@scopes_disabled()
def payment_refund_sum(self):
payment_sum = self.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED)
@@ -270,7 +248,6 @@ class Order(LockModel, LoggedModel):
return payment_sum - refund_sum
@property
@scopes_disabled()
def pending_sum(self):
total = self.total
if self.status == Order.STATUS_CANCELED:
@@ -445,7 +422,6 @@ class Order(LockModel, LoggedModel):
return round_decimal(fee, self.event.currency)
@property
@scopes_disabled()
def user_cancel_allowed(self) -> bool:
"""
Returns whether or not this order can be canceled by the user.
@@ -630,7 +606,7 @@ class Order(LockModel, LoggedModel):
), tz)
return term_last
def _can_be_paid(self, count_waitinglist=True, ignore_date=False) -> Union[bool, str]:
def _can_be_paid(self, count_waitinglist=True) -> Union[bool, str]:
error_messages = {
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
"payment settings is over."),
@@ -641,13 +617,13 @@ class Order(LockModel, LoggedModel):
if self.require_approval:
return error_messages['require_approval']
term_last = self.payment_term_last
if term_last and not ignore_date:
if term_last:
if now() > term_last:
return error_messages['late_lastdate']
if self.status == self.STATUS_PENDING:
return True
if not self.event.settings.get('payment_term_accept_late') and not ignore_date:
if not self.event.settings.get('payment_term_accept_late'):
return error_messages['late']
return self._is_still_available(count_waitinglist=count_waitinglist)
@@ -688,7 +664,7 @@ class Order(LockModel, LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None):
auth=None, attach_tickets=False):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -705,9 +681,6 @@ class Order(LockModel, LoggedModel):
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param position: An order position this refers to. If given, no invoices will be attached, the tickets will
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
@@ -719,16 +692,12 @@ class Order(LockModel, LoggedModel):
with language(self.locale):
recipient = self.email
if position and position.attendee_email:
recipient = position.attendee_email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position
self.event, self.locale, self, headers, sender,
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
@@ -740,7 +709,6 @@ class Order(LockModel, LoggedModel):
data={
'subject': subject,
'message': email_content,
'position': position.positionid if position else None,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
@@ -760,10 +728,9 @@ class Order(LockModel, LoggedModel):
email_template = self.event.settings.mail_text_resend_link
email_context = {
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.order.open', kwargs={
'url': build_absolute_uri(self.event, 'presale:event.order', kwargs={
'order': self.code,
'secret': self.secret,
'hash': self.email_confirm_hash()
'secret': self.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
@@ -829,8 +796,6 @@ class QuestionAnswer(models.Model):
max_length=255
)
objects = ScopedManager(organizer='question__event__organizer')
@property
def backend_file_url(self):
if self.file:
@@ -894,8 +859,6 @@ class QuestionAnswer(models.Model):
return date_format(d, "TIME_FORMAT")
except ValueError:
return self.answer
elif self.question.type == Question.TYPE_COUNTRYCODE and self.answer:
return Country(self.answer).name or self.answer
else:
return self.answer
@@ -995,10 +958,6 @@ class AbstractPosition(models.Model):
else:
return {}
@meta_info_data.setter
def meta_info_data(self, d):
self.meta_info = json.dumps(d)
def cache_answers(self, all=True):
"""
Creates two properties on the object.
@@ -1016,8 +975,7 @@ class AbstractPosition(models.Model):
if hasattr(self.item, 'questions_to_ask'):
questions = list(copy.copy(q) for q in self.item.questions_to_ask)
else:
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False,
hidden=False))
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False))
else:
questions = list(copy.copy(q) for q in self.item.questions.all())
@@ -1154,8 +1112,6 @@ class OrderPayment(models.Model):
)
migrated = models.BooleanField(default=False)
objects = ScopedManager(organizer='order__event__organizer')
class Meta:
ordering = ('local_id',)
@@ -1181,9 +1137,9 @@ class OrderPayment(models.Model):
"""
return self.order.event.get_payment_providers().get(self.provider)
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
def _mark_paid(self, force, count_waitinglist, user, auth, overpaid=False):
from pretix.base.signals import order_paid
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date)
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist)
if not force and can_be_paid is not True:
self.order.log_action('pretix.event.order.quotaexceeded', {
'message': can_be_paid
@@ -1203,8 +1159,7 @@ class OrderPayment(models.Model):
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_date=None):
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='', lock=True):
"""
Marks the payment as complete. If possible, this also marks the order as paid if no further
payment is required
@@ -1214,7 +1169,6 @@ class OrderPayment(models.Model):
:type count_waitinglist: boolean
:param force: Whether this payment should be marked as paid even if no remaining
quota is available (default: ``False``).
:param ignore_date: Whether this order should be marked as paid even when the last date of payments is over.
:type force: boolean
:param send_mail: Whether an email should be sent to the user about this event (default: ``True``).
:type send_mail: boolean
@@ -1225,6 +1179,8 @@ class OrderPayment(models.Model):
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
@@ -1233,7 +1189,7 @@ class OrderPayment(models.Model):
return
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
locked_instance.payment_date = payment_date or now()
locked_instance.payment_date = now()
locked_instance.info = self.info # required for backwards compatibility
locked_instance.save(update_fields=['state', 'payment_date', 'info'])
@@ -1265,13 +1221,11 @@ class OrderPayment(models.Model):
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
# database transaction is more than enough.
lockfn = NoLockManager
with transaction.atomic():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total)
else:
lockfn = self.order.event.lock
with lockfn():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
ignore_date=ignore_date)
with self.order.event.lock():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total)
invoice = None
if invoice_qualified(self.order):
@@ -1288,77 +1242,35 @@ class OrderPayment(models.Model):
)
if send_mail:
self._send_paid_mail(invoice, user, mail_text)
if self.order.event.settings.mail_send_order_paid_attendee:
for p in self.order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != self.order.email:
self._send_paid_mail_attendee(p, user)
def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
name_scheme = PERSON_NAME_SCHEMES[self.order.event.settings.name_scheme]
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_context = {
'event': self.order.event.name,
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'url': build_absolute_uri(self.order.event, 'presale:event.order.position', kwargs={
'order': self.order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[], position=position,
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': self.order.code,
'secret': self.order.secret,
'hash': self.order.email_confirm_hash()
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@property
def refunded_amount(self):
@@ -1512,8 +1424,6 @@ class OrderRefund(models.Model):
null=True, blank=True
)
objects = ScopedManager(organizer='order__event__organizer')
class Meta:
ordering = ('local_id',)
@@ -1575,7 +1485,7 @@ class OrderRefund(models.Model):
super().save(*args, **kwargs)
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
class ActivePositionManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(canceled=False)
@@ -1652,7 +1562,7 @@ class OrderFee(models.Model):
)
canceled = models.BooleanField(default=False)
all = ScopedManager(organizer='order__event__organizer')
all = models.Manager()
objects = ActivePositionManager()
@property
@@ -1749,7 +1659,6 @@ class OrderPosition(AbstractPosition):
verbose_name=_('Tax value')
)
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
web_secret = models.CharField(max_length=32, default=generate_secret, db_index=True)
pseudonymization_id = models.CharField(
max_length=16,
unique=True,
@@ -1757,7 +1666,7 @@ class OrderPosition(AbstractPosition):
)
canceled = models.BooleanField(default=False)
all = ScopedManager(organizer='order__event__organizer')
all = models.Manager()
objects = ActivePositionManager()
class Meta:
@@ -1873,60 +1782,6 @@ class OrderPosition(AbstractPosition):
def event(self):
return self.order.event
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, ``recipient`` and
``order`` parameters.
* Create a ``LogEntry`` with the email contents.
:param subject: Subject of the email
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
:param context: Dictionary to use for rendering the template
:param log_entry_type: Key to be used for the log entry
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale):
recipient = self.email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
}
)
class CartPosition(AbstractPosition):
"""
@@ -1964,8 +1819,6 @@ class CartPosition(AbstractPosition):
)
is_bundled = models.BooleanField(default=False)
objects = ScopedManager(organizer='event__organizer')
class Meta:
verbose_name = _("Cart position")
verbose_name_plural = _("Cart positions")
@@ -2015,8 +1868,6 @@ class InvoiceAddress(models.Model):
blank=True
)
objects = ScopedManager(organizer='order__event__organizer')
def save(self, **kwargs):
if self.order:
self.order.touch()

View File

@@ -8,7 +8,6 @@ from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from ..decimal import round_decimal
from .base import LoggedModel
@@ -174,8 +173,6 @@ class Voucher(LoggedModel):
"convenience.")
)
objects = ScopedManager(organizer='event__organizer')
class Meta:
verbose_name = _("Voucher")
verbose_name_plural = _("Vouchers")
@@ -347,8 +344,6 @@ class Voucher(LoggedModel):
a variation).
"""
if self.quota_id:
if variation:
return variation.quotas.filter(pk=self.quota_id).exists()
return item.quotas.filter(pk=self.quota_id).exists()
if self.item_id and not self.variation_id:
return self.item_id == item.pk

View File

@@ -4,7 +4,6 @@ from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.i18n import language
from pretix.base.models import Voucher
@@ -68,8 +67,6 @@ class WaitingListEntry(LoggedModel):
)
priority = models.IntegerField(default=0)
objects = ScopedManager(organizer='event__organizer')
class Meta:
verbose_name = _("Waiting list entry")
verbose_name_plural = _("Waiting list entries")

View File

@@ -87,15 +87,6 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price, event.currency)
}),
("price_with_addons", {
"label": _("Price including add-ons"),
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price + sum(
p.price
for p in op.addons.all()
if not p.canceled
), event.currency)
}),
("attendee_name", {
"label": _("Attendee name"),
"editor_sample": _("John Doe"),

View File

@@ -1,10 +1,8 @@
import sys
from enum import Enum
from typing import List
from django.apps import AppConfig, apps
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class PluginType(Enum):
@@ -41,22 +39,3 @@ def get_all_plugins(event=None) -> List[type]:
plugins,
key=lambda m: (0 if m.module.startswith('pretix.') else 1, str(m.name).lower().replace('pretix ', ''))
)
class PluginConfig(AppConfig):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not hasattr(self, 'PretixPluginMeta'):
raise ImproperlyConfigured("A pretix plugin config should have a PretixPluginMeta inner class.")
if hasattr(self.PretixPluginMeta, 'compatibility'):
import pkg_resources
try:
pkg_resources.require(self.PretixPluginMeta.compatibility)
except pkg_resources.VersionConflict as e:
print("Incompatible plugins found!")
print("Plugin {} requires you to have {}, but you installed {}.".format(
self.name, e.req, e.dist
))
sys.exit(1)

View File

@@ -71,7 +71,7 @@ class RelativeDateWrapper:
else:
base_date = getattr(event, self.data.base_date_name) or event.date_from
oldoffset = base_date.astimezone(tz).utcoffset()
oldoffset = base_date.utcoffset()
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
if self.data.time:
new_date = new_date.replace(

View File

@@ -1,16 +1,15 @@
from collections import Counter, defaultdict, namedtuple
from datetime import datetime, time, timedelta
from datetime import timedelta
from decimal import Decimal
from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError
from django.core.exceptions import ValidationError
from django.db import DatabaseError, transaction
from django.db import transaction
from django.db.models import Q
from django.dispatch import receiver
from django.utils.timezone import make_aware, now
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext as _
from django_scopes import scopes_disabled
from pretix.base.i18n import language
from pretix.base.models import (
@@ -20,11 +19,10 @@ from pretix.base.models import (
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.checkin import _save_answers
from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.templatetags.rich_text import rich_text
from pretix.celery_app import app
@@ -136,15 +134,6 @@ class CartManager:
raise CartError(error_messages['not_started'])
if self.event.presale_has_ended:
raise CartError(error_messages['ended'])
if not self.event.has_subevents:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(self.event).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < self.now_dt:
raise CartError(error_messages['ended'])
def _extend_expiry_of_valid_existing_positions(self):
# Extend this user's cart session to ensure all items in the cart expire at the same time
@@ -163,18 +152,6 @@ class CartManager:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
if cp.subevent:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < self.now_dt:
err = error_messages['some_subevent_ended']
cp.addons.all().delete()
cp.delete()
return err
def _update_subevents_cache(self, se_ids: List[int]):
@@ -239,16 +216,6 @@ class CartManager:
if op.subevent and op.subevent.presale_has_ended:
raise CartError(error_messages['ended'])
if op.subevent:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(op.subevent).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < self.now_dt:
raise CartError(error_messages['ended'])
if isinstance(op, self.AddOperation):
if op.item.category and op.item.category.is_addon and not (op.addon_to and op.addon_to != 'FAKE'):
raise CartError(error_messages['addon_only'])
@@ -635,7 +602,7 @@ class CartManager:
Q(voucher=voucher) & Q(event=self.event) &
Q(expires__gte=self.now_dt)
).exclude(pk__in=[
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
op.position.voucher_id for op in self._operations if isinstance(op, self.ExtendOperation)
])
cart_count = redeemed_in_carts.count()
v_avail = voucher.max_usages - voucher.redeemed - cart_count
@@ -792,11 +759,7 @@ class CartManager:
if available_count == 1:
op.position.expires = self._expiry
op.position.price = op.price.gross
try:
op.position.save(force_update=True)
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
op.position.save()
elif available_count == 0:
op.position.addons.all().delete()
op.position.delete()
@@ -811,33 +774,17 @@ class CartManager:
CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk])
return err
def _require_locking(self):
if self._voucher_use_diff:
# If any vouchers are used, we lock to make sure we don't redeem them to often
return True
if self._quota_diff and any(q.size is not None for q in self._quota_diff):
# If any quotas are affected that are not unlimited, we lock
return True
return False
def commit(self):
self._check_presale_dates()
self._check_max_cart_size()
self._calculate_expiry()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
lockfn = NoLockManager
if self._require_locking():
lockfn = self.event.lock
with lockfn() as now_dt:
with self.event.lock() as now_dt:
with transaction.atomic():
self.now_dt = now_dt
self._extend_expiry_of_valid_existing_positions()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = self._perform_operations() or err
if err:
raise CartError(err)
@@ -903,7 +850,7 @@ def get_fees(event, request, total, invoice_address, provider):
return fees
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
"""
@@ -914,11 +861,12 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
:raises CartError: On any error that occured
"""
with language(locale):
event = Event.objects.get(id=event)
ia = False
if invoice_address:
try:
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=invoice_address)
ia = InvoiceAddress.objects.get(pk=invoice_address)
except InvoiceAddress.DoesNotExist:
pass
@@ -934,8 +882,8 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
raise CartError(error_messages['busy'])
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en') -> None:
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: int, position: int, cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
@@ -943,6 +891,7 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
:param session: Session ID of a guest
"""
with language(locale):
event = Event.objects.get(id=event)
try:
try:
cm = CartManager(event=event, cart_id=cart_id)
@@ -954,14 +903,15 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
raise CartError(error_messages['busy'])
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: Event, cart_id: str=None, locale='en') -> None:
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param session: Session ID of a guest
"""
with language(locale):
event = Event.objects.get(id=event)
try:
try:
cm = CartManager(event=event, cart_id=cart_id)
@@ -973,8 +923,8 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en') -> None:
raise CartError(error_messages['busy'])
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, sales_channel='web') -> None:
"""
Removes a list of items from a user's cart.
@@ -983,11 +933,12 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
:param session: Session ID of a guest
"""
with language(locale):
event = Event.objects.get(id=event)
ia = False
if invoice_address:
try:
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=invoice_address)
ia = InvoiceAddress.objects.get(pk=invoice_address)
except InvoiceAddress.DoesNotExist:
pass
try:

View File

@@ -2,7 +2,6 @@ from datetime import timedelta
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import CachedCombinedTicket, CachedTicket
@@ -11,7 +10,6 @@ from ..signals import periodic_task
@receiver(signal=periodic_task)
@scopes_disabled()
def clean_cart_positions(sender, **kwargs):
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=False):
cp.delete()
@@ -22,14 +20,12 @@ def clean_cart_positions(sender, **kwargs):
@receiver(signal=periodic_task)
@scopes_disabled()
def clean_cached_files(sender, **kwargs):
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()):
cf.delete()
@receiver(signal=periodic_task)
@scopes_disabled()
def clean_cached_tickets(sender, **kwargs):
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
cf.delete()

View File

@@ -2,33 +2,24 @@ from typing import Any, Dict
from django.core.files.base import ContentFile
from django.utils.timezone import override
from django.utils.translation import ugettext
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.services.tasks import ProfiledTask
from pretix.base.signals import register_data_exporters
from pretix.celery_app import app
class ExportError(LazyLocaleException):
pass
@app.task(base=ProfiledEventTask, throws=(ExportError,))
def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
@app.task(base=ProfiledTask)
def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
event = Event.objects.get(id=event)
file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale), override(event.settings.timezone):
responses = register_data_exporters.send(event)
for receiver, response in responses:
ex = response(event)
if ex.identifier == provider:
d = ex.render(form_data)
if d is None:
raise ExportError(
ugettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
file.filename, file.type, data = ex.render(form_data)
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
return file.pk

View File

@@ -15,7 +15,6 @@ from django.utils import timezone
from django.utils.timezone import now
from django.utils.translation import pgettext, ugettext as _
from django_countries.fields import Country
from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
@@ -245,23 +244,20 @@ def generate_invoice(order: Order, trigger_pdf=True):
@app.task(base=TransactionAwareTask)
def invoice_pdf_task(invoice: int):
with scopes_disabled():
i = Invoice.objects.get(pk=invoice)
with scope(organizer=i.order.event.organizer):
if i.shredded:
return None
if i.file:
i.file.delete()
with language(i.locale):
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
i.file.save(fname, ContentFile(fcontent))
i.save()
return i.file.name
i = Invoice.objects.get(pk=invoice)
if i.shredded:
return None
if i.file:
i.file.delete()
with language(i.locale):
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
i.file.save(fname, ContentFile(fcontent))
i.save()
return i.file.name
def invoice_qualified(order: Order):
if order.total == Decimal('0.00') or order.require_approval or \
order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
if order.total == Decimal('0.00') or order.require_approval:
return False
return True

View File

@@ -13,18 +13,6 @@ logger = logging.getLogger('pretix.base.locking')
LOCK_TIMEOUT = 120
class NoLockManager:
def __init__(self):
pass
def __enter__(self):
return now()
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
return False
class LockManager:
def __init__(self, event):
self.event = event

View File

@@ -1,6 +1,5 @@
import logging
import smtplib
import warnings
from email.utils import formataddr
from typing import Any, Dict, List, Union
@@ -10,14 +9,11 @@ from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import get_template
from django.utils.translation import ugettext as _
from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import (
Event, Invoice, InvoiceAddress, Order, OrderPosition,
)
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
@@ -42,8 +38,8 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
invoices: list=None, attach_tickets=False):
order: Order=None, headers: dict=None, sender: str=None, invoices: list=None,
attach_tickets=False):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -64,9 +60,6 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param order: The order this email is related to (optional). If set, this will be used to include a link to the
order below the email.
:param order: The order position this email is related to (optional). If set, this will be used to include a link
to the order position instead of the order below the email.
:param headers: A dict of custom mail headers to add to the mail
:param locale: The locale to be used while evaluating the subject and the template
@@ -107,8 +100,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
subject = str(subject).format_map(context)
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
if event:
sender_name = event.settings.mail_from_name or str(event.name)
sender = formataddr((sender_name, sender))
sender = formataddr((str(event.name), sender))
else:
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
@@ -119,8 +111,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
if event:
renderer = event.get_html_mail_renderer()
if event.settings.mail_bcc:
for bcc_mail in event.settings.mail_bcc.split(','):
bcc.append(bcc_mail.strip())
bcc.append(event.settings.mail_bcc)
if event.settings.mail_from == settings.DEFAULT_FROM_EMAIL and event.settings.contact_mail and not headers.get('Reply-To'):
headers['Reply-To'] = event.settings.contact_mail
@@ -139,26 +130,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
body_plain += signature
body_plain += "\r\n\r\n-- \r\n"
if order and order.testmode:
subject = "[TESTMODE] " + subject
if order and position:
body_plain += _(
"You are receiving this email because someone placed an order for {event} for you."
).format(event=event.name)
body_plain += "\r\n"
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
'position': position.positionid,
}
)
)
elif order:
if order:
if order.testmode:
subject = "[TESTMODE] " + subject
body_plain += _(
"You are receiving this email because you placed an order for {event}."
).format(event=event.name)
@@ -167,24 +141,16 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order.open', kwargs={
order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'secret': order.secret
}
)
)
body_plain += "\r\n"
try:
try:
body_html = renderer.render(content_plain, signature, str(subject), order, position)
except TypeError:
# Backwards compatibility
warnings.warn('E-mail renderer called without position argument because position argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, str(subject), order)
body_html = renderer.render(content_plain, signature, str(subject), order)
except:
logger.exception('Could not render HTML body')
body_html = None
@@ -198,9 +164,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
sender=sender,
event=event.id if event else None,
headers=headers,
invoices=[i.pk for i in invoices] if invoices and not position else [],
invoices=[i.pk for i in invoices] if invoices else [],
order=order.pk if order else None,
position=position.pk if position else None,
attach_tickets=attach_tickets
)
@@ -215,8 +180,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
invoices: List[int]=None, order: int=None, attach_tickets=False) -> bool:
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
order: int=None, attach_tickets=False) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
email.attach_alternative(html, "text/html")
@@ -235,87 +200,78 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
pass
if event:
with scopes_disabled():
event = Event.objects.get(id=event)
event = Event.objects.get(id=event)
backend = event.get_mail_backend()
cm = lambda: scope(organizer=event.organizer) # noqa
else:
backend = get_connection(fail_silently=False)
cm = lambda: scopes_disabled() # noqa
with cm():
if event:
if order:
try:
order = event.orders.get(pk=order)
except Order.DoesNotExist:
order = None
else:
if position:
try:
position = order.positions.get(pk=position)
except OrderPosition.DoesNotExist:
attach_tickets = False
if attach_tickets:
args = []
attach_size = 0
for name, ct in get_tickets_for_order(order, base_position=position):
content = ct.file.read()
args.append((name, content, ct.type))
attach_size += len(content)
if event:
if order:
try:
order = event.orders.get(pk=order)
except Order.DoesNotExist:
order = None
else:
if attach_tickets:
args = []
attach_size = 0
for name, ct in get_tickets_for_order(order):
content = ct.file.read()
args.append((name, content, ct.type))
attach_size += len(content)
if attach_size < 4 * 1024 * 1024:
# Do not attach more than 4MB, it will bounce way to often.
for a in args:
try:
email.attach(*a)
except:
pass
else:
order.log_action(
'pretix.event.order.email.attachments.skipped',
data={
'subject': 'Attachments skipped',
'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size),
'recipient': '',
'invoices': [],
}
)
if attach_size < 4 * 1024 * 1024:
# Do not attach more than 4MB, it will bounce way to often.
for a in args:
try:
email.attach(*a)
except:
pass
else:
order.log_action(
'pretix.event.order.email.error',
data={
'subject': 'Attachments skipped',
'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size),
'recipient': '',
'invoices': [],
}
)
email = email_filter.send_chained(event, 'message', message=email, order=order)
email = email_filter.send_chained(event, 'message', message=email, order=order)
try:
backend.send_messages([email])
except smtplib.SMTPResponseException as e:
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
logger.exception('Error sending email')
try:
backend.send_messages([email])
except smtplib.SMTPResponseException as e:
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
logger.exception('Error sending email')
if order:
order.log_action(
'pretix.event.order.email.error',
data={
'subject': 'SMTP code {}'.format(e.smtp_code),
'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error),
'recipient': '',
'invoices': [],
}
)
if order:
order.log_action(
'pretix.event.order.email.error',
data={
'subject': 'SMTP code {}'.format(e.smtp_code),
'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error),
'recipient': '',
'invoices': [],
}
)
raise SendMailException('Failed to send an email to {}.'.format(to))
except Exception as e:
if order:
order.log_action(
'pretix.event.order.email.error',
data={
'subject': 'Internal error',
'message': str(e),
'recipient': '',
'invoices': [],
}
)
logger.exception('Error sending email')
raise SendMailException('Failed to send an email to {}.'.format(to))
raise SendMailException('Failed to send an email to {}.'.format(to))
except Exception as e:
if order:
order.log_action(
'pretix.event.order.email.error',
data={
'subject': 'Internal error',
'message': str(e),
'recipient': '',
'invoices': [],
}
)
logger.exception('Error sending email')
raise SendMailException('Failed to send an email to {}.'.format(to))
def mail_send(*args, **kwargs):

View File

@@ -1,6 +1,5 @@
from django.conf import settings
from django.template.loader import get_template
from django_scopes import scope, scopes_disabled
from inlinestyler.utils import inline_css
from pretix.base.i18n import language
@@ -13,7 +12,6 @@ from pretix.helpers.urls import build_absolute_uri
@app.task(base=TransactionAwareTask)
@scopes_disabled()
def notify(logentry_id: int):
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.event:
@@ -68,22 +66,17 @@ def notify(logentry_id: int):
@app.task(base=ProfiledTask)
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
logentry = LogEntry.all.get(id=logentry_id)
if logentry.event:
sm = lambda: scope(organizer=logentry.event.organizer) # noqa
else:
sm = lambda: scopes_disabled() # noqa
with sm():
user = User.objects.get(id=user_id)
types = get_all_notification_types(logentry.event)
notification_type = types.get(action_type)
if not notification_type:
return # Ignore, e.g. plugin not active for this event
user = User.objects.get(id=user_id)
types = get_all_notification_types(logentry.event)
notification_type = types.get(action_type)
if not notification_type:
return # Ignore, e.g. plugin not active for this event
with language(user.locale):
notification = notification_type.build_notification(logentry)
with language(user.locale):
notification = notification_type.build_notification(logentry)
if method == "mail":
send_notification_mail(notification, user)
if method == "mail":
send_notification_mail(notification, user)
def send_notification_mail(notification: Notification, user: User):
@@ -111,11 +104,7 @@ def send_notification_mail(notification: Notification, user: User):
mail_send_task.apply_async(kwargs={
'to': [user.email],
'subject': '[{}] {}: {}'.format(
settings.PRETIX_INSTANCE_NAME,
notification.event.settings.mail_prefix or notification.event.slug.upper(),
notification.title
),
'subject': '[{}] {}'.format(settings.PRETIX_INSTANCE_NAME, notification.title),
'body': body_plain,
'html': body_html,
'sender': settings.MAIL_FROM,

View File

@@ -1,7 +1,7 @@
import json
import logging
from collections import Counter, namedtuple
from datetime import datetime, time, timedelta
from datetime import datetime, timedelta
from decimal import Decimal
from typing import List, Optional
@@ -14,9 +14,8 @@ from django.db.models.functions import Greatest
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
from pretix.base.i18n import (
@@ -35,20 +34,18 @@ from pretix.base.models.orders import (
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services import tickets
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified,
)
from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.services.tasks import ProfiledTask
from pretix.base.signals import (
allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_placed,
periodic_task, validate_order,
periodic_task,
)
from pretix.celery_app import app
from pretix.helpers.models import modelcopy
@@ -164,7 +161,7 @@ def mark_order_expired(order, user=None, auth=None):
return order
def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False):
def approve_order(order, user=None, send_mail: bool=True, auth=None):
"""
Mark this order as approved
:param order: The order to change
@@ -187,7 +184,7 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
fee=None
)
try:
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth, ignore_date=True, force=force)
p.confirm(send_mail=False, count_waitinglist=False, user=user, auth=auth)
except Quota.QuotaExceededException:
raise OrderError(error_messages['unavailable'])
@@ -224,10 +221,9 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
'date': LazyDate(order.expires),
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'secret': order.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
@@ -285,10 +281,9 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
'date': LazyDate(order.expires),
'event': order.event.name,
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'secret': order.secret
}),
'comment': comment,
'invoice_name': invoice_name,
@@ -379,10 +374,9 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
email_context = {
'event': order.event.name,
'code': order.code,
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'secret': order.secret
})
}
with language(order.locale):
@@ -409,16 +403,6 @@ def _check_date(event: Event, now_dt: datetime):
if event.presale_has_ended:
raise OrderError(error_messages['ended'])
if not event.has_subevents:
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(event).date(),
time(hour=23, minute=59, second=59)
), event.timezone)
if term_last < now_dt:
raise OrderError(error_messages['ended'])
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None):
err = None
@@ -473,18 +457,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp)
break
if cp.subevent:
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59)
), event.timezone)
if term_last < now_dt:
err = err or error_messages['some_subevent_ended']
delete(cp)
break
if cp.subevent and cp.subevent.presale_has_ended:
err = err or error_messages['some_subevent_ended']
delete(cp)
@@ -536,6 +508,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
continue
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
positions[i] = cp
cp.price = price.gross
cp.includes_tax = bool(price.rate)
cp.save()
@@ -559,6 +532,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
break
if quota_ok:
positions[i] = cp
cp.expires = now_dt + timedelta(
minutes=event.settings.get('reservation_time', as_type=int))
cp.save()
@@ -647,77 +621,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
return order, p
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
invoice):
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
if pprov:
payment_info = str(pprov.order_pending_mail_render(order))
else:
payment_info = None
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'payment_info': payment_info,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order received email could not be sent')
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str):
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
email_context = {
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_subject = _('Your event registration: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[],
attach_tickets=True,
position=position
)
except SendMailException:
logger.exception('Order received email could not be sent to attendee')
def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
event = Event.objects.get(id=event)
if payment_provider:
pprov = event.get_payment_providers().get(payment_provider)
if not pprov:
@@ -731,26 +638,13 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
addr = None
if address is not None:
try:
with scopes_disabled():
addr = InvoiceAddress.objects.get(pk=address)
addr = InvoiceAddress.objects.get(pk=address)
except InvoiceAddress.DoesNotExist:
pass
positions = CartPosition.objects.filter(id__in=position_ids, event=event)
validate_order.send(event, payment_provider=pprov, email=email, positions=positions,
locale=locale, invoice_address=addr, meta_info=meta_info)
lockfn = NoLockManager
locked = False
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2))).exists():
# Performance optimization: If no voucher is used and no cart position is dangerously close to its expiry date,
# creating this order shouldn't be prone to any race conditions and we don't need to lock the event.
locked = True
lockfn = event.lock
with lockfn() as now_dt:
positions = list(positions.select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons'))
with event.lock() as now_dt:
positions = list(CartPosition.objects.filter(
id__in=position_ids).select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons'))
if len(positions) == 0:
raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions):
@@ -761,10 +655,7 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
free_order_flow = payment and payment_provider == 'free' and order.total == Decimal('0.00') and not order.require_approval
if free_order_flow:
try:
payment.confirm(send_mail=False, lock=not locked)
except Quota.QuotaExceededException:
pass
payment.confirm(send_mail=False, lock=False)
invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
@@ -779,32 +670,54 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
if order.require_approval:
email_template = event.settings.mail_text_order_placed_require_approval
log_entry = 'pretix.event.order.email.order_placed_require_approval'
email_attendees = False
elif free_order_flow:
email_template = event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = event.settings.mail_send_order_free_attendee
email_attendees_template = event.settings.mail_text_order_free_attendee
else:
email_template = event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = event.settings.mail_send_order_placed_attendee
email_attendees_template = event.settings.mail_text_order_placed_attendee
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
_order_placed_email(event, order, pprov, email_template, log_entry, invoice)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry)
if pprov:
payment_info = str(pprov.order_pending_mail_render(order))
else:
payment_info = None
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret
}),
'payment_info': payment_info,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order received email could not be sent')
return order.id
@receiver(signal=periodic_task)
@scopes_disabled()
def expire_orders(sender, **kwargs):
eventcache = {}
@@ -819,7 +732,6 @@ def expire_orders(sender, **kwargs):
@receiver(signal=periodic_task)
@scopes_disabled()
def send_expiry_warnings(sender, **kwargs):
eventcache = {}
today = now().replace(hour=0, minute=0, second=0)
@@ -853,10 +765,9 @@ def send_expiry_warnings(sender, **kwargs):
email_template = eventsettings.mail_text_order_expire_warning
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret,
'hash': o.email_confirm_hash()
'secret': o.secret
}),
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
'invoice_name': invoice_name,
@@ -877,7 +788,6 @@ def send_expiry_warnings(sender, **kwargs):
@receiver(signal=periodic_task)
@scopes_disabled()
def send_download_reminders(sender, **kwargs):
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
@@ -906,10 +816,9 @@ def send_download_reminders(sender, **kwargs):
email_template = e.settings.mail_text_download_reminder
email_context = {
'event': o.event.name,
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret,
'hash': o.email_confirm_hash()
'secret': o.secret
}),
}
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
@@ -922,31 +831,6 @@ def send_download_reminders(sender, **kwargs):
except SendMailException:
logger.exception('Reminder email could not be sent')
if e.settings.mail_send_download_reminder_attendee:
name_scheme = PERSON_NAME_SCHEMES[e.settings.name_scheme]
for p in o.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = e.settings.mail_text_download_reminder_attendee
email_context = {
'event': e.name,
'url': build_absolute_uri(e, 'presale:event.order.position', kwargs={
'order': o.code,
'secret': p.web_secret,
'position': p.positionid
}),
'attendee_name': p.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = p.attendee_name_parts.get(f, '')
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
except SendMailException:
logger.exception('Reminder email could not be sent to attendee')
class OrderChangeManager:
error_messages = {
@@ -1431,10 +1315,9 @@ class OrderChangeManager:
email_template = order.event.settings.mail_text_order_changed
email_context = {
'event': order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
'secret': order.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
@@ -1500,8 +1383,8 @@ class OrderChangeManager:
return pprov
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def perform_order(self, event: Event, payment_provider: str, positions: List[str],
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def perform_order(self, event: str, payment_provider: str, positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web'):
with language(locale):
@@ -1516,7 +1399,6 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
@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):
try:

View File

@@ -1,12 +1,11 @@
from datetime import timedelta
from django.conf import settings
from django.db.models import Max, Q
from django.db import models
from django.db.models import F, Max, OuterRef, Q, Subquery
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import Event, LogEntry
from pretix.base.models import LogEntry, Quota
from pretix.celery_app import app
from ..signals import periodic_task
@@ -18,27 +17,20 @@ def build_all_quota_caches(sender, **kwargs):
@app.task
@scopes_disabled()
def refresh_quota_caches():
# Active events
active = LogEntry.objects.using(settings.DATABASE_REPLICA).filter(
datetime__gt=now() - timedelta(days=7)
last_activity = LogEntry.objects.filter(
event=OuterRef('event_id'),
).order_by().values('event').annotate(
last_activity=Max('datetime')
m=Max('datetime')
).values(
'm'
)
for a in active:
try:
e = Event.objects.using(settings.DATABASE_REPLICA).get(pk=a['event'])
except Event.DoesNotExist:
continue
quotas = e.quotas.filter(
Q(cached_availability_time__isnull=True) |
Q(cached_availability_time__lt=a['last_activity']) |
Q(cached_availability_time__lt=now() - timedelta(hours=2))
).filter(
Q(subevent__isnull=True) |
Q(subevent__date_to__isnull=False, subevent__date_to__gte=now() - timedelta(days=14)) |
Q(subevent__date_from__gte=now() - timedelta(days=14))
)
for q in quotas:
q.availability()
quotas = Quota.objects.annotate(
last_activity=Subquery(last_activity, output_field=models.DateTimeField())
).filter(
Q(cached_availability_time__isnull=True) |
Q(cached_availability_time__lt=F('last_activity')) |
Q(cached_availability_time__lt=now() - timedelta(hours=2), last_activity__gt=now() - timedelta(days=7))
).select_related('subevent')
for q in quotas:
q.availability()

View File

@@ -11,13 +11,14 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.services.tasks import ProfiledTask
from pretix.base.shredder import ShredError
from pretix.celery_app import app
@app.task(base=ProfiledEventTask)
def export(event: Event, shredders: List[str]) -> None:
@app.task(base=ProfiledTask)
def export(event: str, shredders: List[str]) -> None:
event = Event.objects.get(id=event)
known_shredders = event.get_data_shredders()
with NamedTemporaryFile() as rawfile:
@@ -62,8 +63,9 @@ def export(event: Event, shredders: List[str]) -> None:
return cf.pk
@app.task(base=ProfiledEventTask, throws=(ShredError,))
def shred(event: Event, fileid: str, confirm_code: str) -> None:
@app.task(base=ProfiledTask, throws=(ShredError,))
def shred(event: str, fileid: str, confirm_code: str) -> None:
event = Event.objects.get(id=event)
known_shredders = event.get_data_shredders()
try:
cf = CachedFile.objects.get(pk=fileid)

View File

@@ -14,12 +14,10 @@ import time
from django.conf import settings
from django.db import transaction
from django_scopes import scope, scopes_disabled
from pretix.base.metrics import (
pretix_task_duration_seconds, pretix_task_runs_total,
)
from pretix.base.models import Event
from pretix.celery_app import app
@@ -63,35 +61,6 @@ class ProfiledTask(app.Task):
return super().on_success(retval, task_id, args, kwargs)
class EventTask(app.Task):
def __call__(self, *args, **kwargs):
if 'event_id' in kwargs:
event_id = kwargs.get('event_id')
with scopes_disabled():
event = Event.objects.select_related('organizer').get(pk=event_id)
del kwargs['event_id']
kwargs['event'] = event
elif 'event' in kwargs:
event_id = kwargs.get('event')
with scopes_disabled():
event = Event.objects.select_related('organizer').get(pk=event_id)
kwargs['event'] = event
else:
args = list(args)
event_id = args[0]
with scopes_disabled():
event = Event.objects.select_related('organizer').get(pk=event_id)
args[0] = event
with scope(organizer=event.organizer):
ret = super().__call__(*args, **kwargs)
return ret
class ProfiledEventTask(ProfiledTask, EventTask):
pass
class TransactionAwareTask(ProfiledTask):
"""
Task class which is aware of django db transactions and only executes tasks

View File

@@ -4,14 +4,13 @@ import os
from django.core.files.base import ContentFile
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django_scopes import scopes_disabled
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
OrderPosition,
)
from pretix.base.services.tasks import EventTask, ProfiledTask
from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.celery_app import app
@@ -58,11 +57,10 @@ def generate_order(order: int, provider: str):
@app.task(base=ProfiledTask)
def generate(model: str, pk: int, provider: str):
with scopes_disabled():
if model == 'order':
return generate_order(pk, provider)
elif model == 'orderposition':
return generate_orderposition(pk, provider)
if model == 'order':
return generate_order(pk, provider)
elif model == 'orderposition':
return generate_orderposition(pk, provider)
class DummyRollbackException(Exception):
@@ -98,7 +96,7 @@ def preview(event: int, provider: str):
return prov.generate(p)
def get_tickets_for_order(order, base_position=None):
def get_tickets_for_order(order):
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
return []
@@ -113,20 +111,13 @@ def get_tickets_for_order(order, base_position=None):
tickets = []
positions = list(order.positions_with_tickets)
if base_position:
# Only the given position and its children
positions = [
p for p in positions if p.pk == base_position.pk or p.addon_to_id == base_position.pk
]
for p in providers:
if not p.is_enabled:
continue
if p.multi_download_enabled and not base_position:
if p.multi_download_enabled:
try:
if len(positions) == 0:
if len(list(order.positions_with_tickets)) == 0:
continue
ct = CachedCombinedTicket.objects.filter(
order=order, provider=p.identifier, file__isnull=False
@@ -145,7 +136,7 @@ def get_tickets_for_order(order, base_position=None):
except:
logger.exception('Failed to generate ticket.')
else:
for pos in positions:
for pos in order.positions_with_tickets:
try:
ct = CachedTicket.objects.filter(
order_position=pos, provider=p.identifier, file__isnull=False
@@ -154,7 +145,7 @@ def get_tickets_for_order(order, base_position=None):
retval = generate_orderposition(pos.pk, p.identifier)
if not retval:
continue
ct = CachedTicket.objects.get(pk=retval)
ct = CachedCombinedTicket.objects.get(pk=retval)
tickets.append((
"{}-{}-{}-{}{}".format(
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,
@@ -167,8 +158,9 @@ def get_tickets_for_order(order, base_position=None):
return tickets
@app.task(base=EventTask)
def invalidate_cache(event: Event, item: int=None, provider: str=None, order: int=None, **kwargs):
@app.task(base=ProfiledTask)
def invalidate_cache(event: int, item: int=None, provider: str=None, order: int=None, **kwargs):
event = Event.objects.get(id=event)
qs = CachedTicket.objects.filter(order_position__order__event=event)
qsc = CachedCombinedTicket.objects.filter(order__event=event)

View File

@@ -6,7 +6,6 @@ import requests
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix import __version__
@@ -30,7 +29,6 @@ def run_update_check(sender, **kwargs):
@app.task
@scopes_disabled()
def update_check():
gs = GlobalSettingsObject()

View File

@@ -1,17 +1,17 @@
import sys
from django.dispatch import receiver
from django_scopes import scopes_disabled
from pretix.base.models import Event, User, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.tasks import EventTask
from pretix.base.services.tasks import ProfiledTask
from pretix.base.signals import periodic_task
from pretix.celery_app import app
@app.task(base=EventTask)
def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None):
@app.task(base=ProfiledTask)
def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None):
event = Event.objects.get(id=event_id)
if user_id:
user = User.objects.get(id=user_id)
else:
@@ -69,7 +69,6 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
@receiver(signal=periodic_task)
@scopes_disabled()
def process_waitinglist(sender, **kwargs):
qs = Event.objects.filter(
live=True

View File

@@ -145,10 +145,6 @@ DEFAULTS = {
'default': 'False',
'type': str
},
'invoice_generate_sales_channels': {
'default': json.dumps(['web']),
'type': list
},
'invoice_address_from': {
'default': '',
'type': str
@@ -301,10 +297,6 @@ DEFAULTS = {
'default': settings.MAIL_FROM,
'type': str
},
'mail_from_name': {
'default': None,
'type': str
},
'mail_text_signature': {
'type': LazyI18nString,
'default': ""
@@ -331,18 +323,6 @@ The list is as follows:
{orders}
Best regards,
Your {event} team"""))
},
'mail_text_order_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
you have been registered for {event} successfully.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -359,10 +339,6 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_send_order_free_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_placed_require_approval': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
@@ -389,22 +365,6 @@ of {total_with_currency}. Please complete your payment before {date}.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_placed_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_placed_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
a ticket for {event} has been ordered for you.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -431,22 +391,6 @@ we successfully received your payment for {event}. Thank you!
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_paid_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_paid_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
a ticket for {event} that has been ordered for you is now paid.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -548,22 +492,6 @@ Your {event} team"""))
'type': int,
'default': None
},
'mail_send_download_reminder_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_download_reminder_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
you are registered for {event}.
If you did not do so already, you can download your ticket here:
{url}
Best regards,
Your {event} team"""))
},
'mail_text_download_reminder': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,

View File

@@ -240,19 +240,6 @@ subclass of pretix.base.exporter.BaseExporter
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_order = EventPluginSignal(
providing_args=["payment_provider", "positions", "email", "locale", "invoice_address",
"meta_info"]
)
"""
This signal is sent out when the user tries to confirm the order, before we actually create
the order. It allows you to inspect the cart positions. Your return value will be ignored,
but you can raise an OrderError with an appropriate exception message if you like to block
the order. We strongly discourage making changes to the order here.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
validate_cart = EventPluginSignal(
providing_args=["positions"]
)
@@ -515,12 +502,3 @@ dictionaries as values that contain keys like in the following example::
The evaluate member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
"""
timeline_events = EventPluginSignal()
"""
This signal is sent out to collect events for the time line shown on event dashboards. You are passed
a ``subevent`` argument which might be none and you are expected to return a list of instances of
``pretix.base.timeline.TimelineEvent``, which is a ``namedtuple`` with the fields ``event``, ``subevent``,
``datetime``, ``description`` and ``edit_url``.
"""

View File

@@ -23,23 +23,13 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% if position %}
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
{% trans "View registration details" %}
</a>
{% else %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% endif %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order" order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
</div>
<!--[if gte mso 9]>
</td></tr></table>

View File

@@ -1,6 +0,0 @@
{% load thumb %}
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}">
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
{{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>

View File

@@ -17,7 +17,6 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
if not arg:
raise ValueError("No currency passed.")
arg = arg.upper()
places = settings.CURRENCY_PLACES.get(arg, 2)
rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)

View File

@@ -47,7 +47,7 @@ ALLOWED_TAGS = [
]
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title', 'class'],
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title'],
'table': ['width'],
@@ -72,11 +72,9 @@ def safelink_callback(attrs, new=False):
def abslink_callback(attrs, new=False):
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)
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, attrs.get((None, 'href'), '/'))
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
@@ -92,7 +90,7 @@ def markdown_compile_email(source):
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS,
), parse_email=True)
))
def markdown_compile(source):
@@ -118,7 +116,6 @@ def rich_text(text: str, **kwargs):
text = str(text)
body_md = bleach.linkify(
markdown_compile(text),
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
parse_email=True
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback])
)
return mark_safe(body_md)

View File

@@ -1,200 +0,0 @@
from collections import namedtuple
from datetime import datetime, time, timedelta
from django.db.models import Q
from django.urls import reverse
from django.utils.timezone import make_aware
from django.utils.translation import pgettext_lazy
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.signals import timeline_events
TimelineEvent = namedtuple('TimelineEvent', ('event', 'subevent', 'datetime', 'description', 'edit_url'))
def timeline_for_event(event, subevent=None):
tl = []
ev = subevent or event
if subevent:
ev_edit_url = reverse(
'control:event.subevent', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'subevent': subevent.pk
}
)
else:
ev_edit_url = reverse(
'control:event.settings', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
}
)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.date_from,
description=pgettext_lazy('timeline', 'Your event starts'),
edit_url=ev_edit_url
))
if ev.date_to:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.date_to,
description=pgettext_lazy('timeline', 'Your event ends'),
edit_url=ev_edit_url
))
if ev.date_admission:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.date_admission,
description=pgettext_lazy('timeline', 'Admissions for your event start'),
edit_url=ev_edit_url
))
if ev.presale_start:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.presale_start,
description=pgettext_lazy('timeline', 'Start of ticket sales'),
edit_url=ev_edit_url
))
if ev.presale_end:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=ev.presale_end,
description=pgettext_lazy('timeline', 'End of ticket sales'),
edit_url=ev_edit_url
))
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
if rd:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer modify their orders'),
edit_url=ev_edit_url
))
rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if rd:
d = make_aware(datetime.combine(
rd.date(ev),
time(hour=23, minute=59, second=59)
), event.timezone)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=d,
description=pgettext_lazy('timeline', 'No more payments can be completed'),
edit_url=reverse('control:event.settings.payment', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
rd = event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
if rd and event.settings.ticket_download:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Tickets can be downloaded'),
edit_url=reverse('control:event.settings.tickets', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
rd = event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
if rd and event.settings.cancel_allow_user:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer cancel free or unpaid orders'),
edit_url=reverse('control:event.settings.tickets', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
rd = event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper)
if rd and event.settings.cancel_allow_user_paid:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer cancel paid orders'),
edit_url=reverse('control:event.settings.tickets', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
if not event.has_subevents:
days = event.settings.get('mail_days_download_reminder', as_type=int)
if days is not None:
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=reminder_date,
description=pgettext_lazy('timeline', 'Download reminders are being sent out'),
edit_url=reverse('control:event.settings.mail', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
})
))
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
if p.available_from:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=p.available_from,
description=pgettext_lazy('timeline', 'Product "{name}" becomes available').format(name=str(p)),
edit_url=reverse('control:event.item', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'item': p.pk,
})
))
if p.available_until:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=p.available_until,
description=pgettext_lazy('timeline', 'Product "{name}" becomes unavailable').format(name=str(p)),
edit_url=reverse('control:event.item', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'item': p.pk,
})
))
pprovs = event.get_payment_providers()
# This is a special case, depending on payment providers not overriding BasePaymentProvider by too much, but it's
# preferrable to having all plugins implement this spearately.
for pprov in pprovs.values():
if not pprov.settings.get('_enabled', as_type=bool):
continue
availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date:
d = make_aware(datetime.combine(
availability_date.date(ev),
time(hour=23, minute=59, second=59)
), event.timezone)
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=d,
description=pgettext_lazy('timeline', 'Payment provider "{name}" can no longer be selected').format(
name=str(pprov.verbose_name)
),
edit_url=reverse('control:event.settings.payment.provider', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'provider': pprov.identifier,
})
))
for recv, resp in timeline_events.send(sender=event, subevent=subevent):
tl += resp
return sorted(tl, key=lambda e: e.datetime)

View File

@@ -3,7 +3,6 @@ import hmac
from django.conf import settings
from django.http import HttpResponse
from django_scopes import scopes_disabled
from .. import metrics
@@ -16,7 +15,6 @@ def unauthed_response():
return response
@scopes_disabled()
def serve_metrics(request):
if not settings.METRICS_ENABLED:
return unauthed_response()

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
from django import forms
from django.core.files.uploadedfile import UploadedFile
from django.db.models import Prefetch, QuerySet
from django.db.models import Prefetch
from django.utils.functional import cached_property
from pretix.base.forms.questions import (
@@ -89,20 +89,19 @@ class BaseQuestionsViewMixin:
elif k == 'attendee_email':
form.pos.attendee_email = v if v != '' else None
form.pos.save()
elif k.startswith('question_'):
elif k.startswith('question_') and v is not None:
field = form.fields[k]
if hasattr(field, 'answer'):
# We already have a cached answer object, so we don't
# have to create a new one
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \
or (isinstance(v, QuerySet) and not v.exists()):
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False):
if field.answer.file:
field.answer.file.delete()
field.answer.delete()
else:
self._save_to_answer(field, field.answer, v)
field.answer.save()
elif v != '' and v is not None:
elif v != '':
answer = QuestionAnswer(
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None),
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None),

View File

@@ -133,7 +133,7 @@ class AsyncAction:
return str(exception)
else:
logger.error('Unexpected exception: %r' % exception)
return _('An unexpected error has occurred, please try again later.')
return _('An unexpected error has occurred.')
def get_success_message(self, value):
return _('The task has been completed.')

View File

@@ -5,7 +5,6 @@ from django.conf import settings
from django.db.models import Q
from django.urls import Resolver404, get_script_prefix, resolve
from django.utils.translation import get_language
from django_scopes import scope
from pretix.base.models.auth import StaffSession
from pretix.base.settings import GlobalSettingsObject
@@ -54,11 +53,10 @@ def contextprocessor(request):
ctx['has_domain'] = request.event.organizer.domains.exists()
if not request.event.testmode:
with scope(organizer=request.organizer):
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
if complain_testmode_orders is None:
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
if complain_testmode_orders is None:
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
ctx['complain_testmode_orders'] = complain_testmode_orders
else:
ctx['complain_testmode_orders'] = False

View File

@@ -4,8 +4,8 @@ import os
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files import File
from django.forms.utils import from_current_timezone
from django.utils.html import conditional_escape
from django.utils.translation import ugettext_lazy as _
from ...base.forms import I18nModelForm
@@ -67,31 +67,16 @@ def selector(values, prop):
class ClearableBasenameFileInput(forms.ClearableFileInput):
template_name = 'pretixbase/forms/widgets/thumbnailed_file_input.html'
class FakeFile(File):
def __init__(self, file):
self.file = file
@property
def name(self):
return self.file.name
@property
def is_img(self):
return any(self.file.name.endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
def __str__(self):
return os.path.basename(self.file.name).split('.', 1)[-1]
@property
def url(self):
return self.file.url
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['widget']['value'] = self.FakeFile(value)
return ctx
def get_template_substitution_values(self, value):
"""
Return value-related substitutions.
"""
bname = os.path.basename(value.name)
return {
'initial': conditional_escape(bname),
'initial_url': conditional_escape(value.url),
}
class ExtFileField(forms.FileField):
@@ -200,7 +185,3 @@ class SplitDateTimeField(forms.SplitDateTimeField):
result = datetime.datetime.combine(*data_list)
return from_current_timezone(result)
return None
class FontSelect(forms.RadioSelect):
option_template_name = 'pretixcontrol/font_option.html'

View File

@@ -1,9 +1,6 @@
from django import forms
from django.urls import reverse
from django.utils.translation import pgettext_lazy
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
from pretix.base.models.checkin import CheckinList
from pretix.control.forms.widgets import Select2
@@ -47,7 +44,3 @@ class CheckinListForm(forms.ModelForm):
'data-inverse-dependency': '<[name$=all_products]'
}),
}
field_classes = {
'limit_products': SafeModelMultipleChoiceField,
'subevent': SafeModelChoiceField,
}

View File

@@ -2,10 +2,9 @@ from django import forms
from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator, validate_email
from django.core.validators import RegexValidator
from django.db.models import Q
from django.forms import formset_factory
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
@@ -19,17 +18,15 @@ from i18nfield.forms import (
)
from pytz import common_timezones, timezone
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
@@ -54,28 +51,16 @@ class EventWizardFoundationForm(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
self.session = kwargs.pop('session')
super().__init__(*args, **kwargs)
qs = Organizer.objects.all()
if not self.user.has_active_staff_session(self.session.session_key):
qs = qs.filter(
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
)
self.fields['organizer'] = forms.ModelChoiceField(
label=_("Organizer"),
queryset=qs,
widget=Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizers.select2') + '?can_create=1',
'data-placeholder': _('Organizer')
}
queryset=Organizer.objects.filter(
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
),
widget=forms.RadioSelect,
empty_label=None,
required=True
)
self.fields['organizer'].widget.choices = self.fields['organizer'].choices
if len(self.fields['organizer'].choices) == 1:
self.fields['organizer'].initial = self.fields['organizer'].queryset.first()
@@ -131,7 +116,6 @@ class EventWizardBasicsForm(I18nModelForm):
self.locales = kwargs.get('locales')
self.has_subevents = kwargs.pop('has_subevents')
kwargs.pop('user')
kwargs.pop('session')
super().__init__(*args, **kwargs)
self.initial['timezone'] = get_current_timezone_name()
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
@@ -189,9 +173,7 @@ class EventChoiceField(forms.ModelChoiceField):
class EventWizardCopyForm(forms.Form):
@staticmethod
def copy_from_queryset(user, session):
if user.has_active_staff_session(session.session_key):
return Event.objects.all()
def copy_from_queryset(user):
return Event.objects.filter(
Q(organizer_id__in=user.teams.filter(
all_events=True, can_change_event_settings=True, can_change_items=True
@@ -203,25 +185,16 @@ class EventWizardCopyForm(forms.Form):
def __init__(self, *args, **kwargs):
kwargs.pop('organizer')
kwargs.pop('locales')
self.session = kwargs.pop('session')
kwargs.pop('has_subevents')
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['copy_from_event'] = EventChoiceField(
label=_("Copy configuration from"),
queryset=EventWizardCopyForm.copy_from_queryset(self.user, self.session),
widget=Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:events.typeahead') + '?can_copy=1',
'data-placeholder': _('Do not copy')
}
),
queryset=EventWizardCopyForm.copy_from_queryset(self.user),
widget=forms.RadioSelect,
empty_label=_('Do not copy'),
required=False
)
self.fields['copy_from_event'].widget.choices = self.fields['copy_from_event'].choices
class EventMetaValueForm(forms.ModelForm):
@@ -306,7 +279,7 @@ class EventSettingsForm(SettingsForm):
)
display_net_prices = forms.BooleanField(
label=_("Show net prices instead of gross prices in the product list (not recommended!)"),
help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be "
help_text=_("Independent of your choice, the cart will show gross prices as this the price that needs to be "
"paid"),
required=False
)
@@ -691,13 +664,6 @@ class InvoiceSettingsForm(SettingsForm):
),
help_text=_("Invoices will never be automatically generated for free orders.")
)
invoice_generate_sales_channels = forms.MultipleChoiceField(
label=_('Generate invoices for Sales channels'),
choices=[],
widget=forms.CheckboxSelectMultiple,
help_text=_("If you have enabled invoice generation in the previous setting, you can limit it here to specific "
"sales channels.")
)
invoice_attendee_name = forms.BooleanField(
label=_("Show attendee names on invoices"),
required=False
@@ -813,16 +779,6 @@ class InvoiceSettingsForm(SettingsForm):
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
locale_names = dict(settings.LANGUAGES)
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
self.fields['invoice_generate_sales_channels'].choices = (
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
)
def multimail_validate(val):
s = val.split(',')
for part in s:
validate_email(part.strip())
return s
class MailSettingsForm(SettingsForm):
@@ -834,20 +790,12 @@ class MailSettingsForm(SettingsForm):
)
mail_from = forms.EmailField(
label=_("Sender address"),
help_text=_("Sender address for outgoing emails"),
help_text=_("Sender address for outgoing emails")
)
mail_from_name = forms.CharField(
label=_("Sender name"),
help_text=_("Sender name used in conjunction with the sender address for outgoing emails. "
"Defaults to your event name."),
required=False
)
mail_bcc = forms.CharField(
mail_bcc = forms.EmailField(
label=_("Bcc address"),
help_text=_("All emails will be sent to this address as a Bcc copy"),
validators=[multimail_validate],
required=False,
max_length=255
required=False
)
mail_text_signature = I18nFormField(
@@ -870,7 +818,7 @@ class MailSettingsForm(SettingsForm):
)
mail_text_order_placed = I18nFormField(
label=_("Text sent to order contact address"),
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
@@ -878,62 +826,20 @@ class MailSettingsForm(SettingsForm):
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
'{payment_info}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_placed_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_placed_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_paid = I18nFormField(
label=_("Text sent to order contact address"),
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}', '{payment_info}'])]
)
mail_send_order_paid_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_paid_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_free = I18nFormField(
label=_("Text sent to order contact address"),
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_free_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
@@ -993,25 +899,12 @@ class MailSettingsForm(SettingsForm):
'{invoice_name}', '{invoice_company}'])]
)
mail_text_download_reminder = I18nFormField(
label=_("Text sent to order contact address"),
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}"),
validators=[PlaceholderValidator(['{event}', '{url}'])]
)
mail_send_download_reminder_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_download_reminder_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {attendee_name}, {event}, {url}"),
validators=[PlaceholderValidator(['{attendee_name}', '{event}', '{url}'])]
)
mail_days_download_reminder = forms.IntegerField(
label=_("Number of days"),
required=False,
@@ -1092,26 +985,13 @@ class MailSettingsForm(SettingsForm):
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
]
keys = list(event.meta_data.keys())
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
for k, v in list(self.fields.items()):
for k, v in self.fields.items():
if k.startswith('mail_text_'):
v.help_text = str(v.help_text) + ', ' + ', '.join({
'{meta_' + p + '}' for p in keys
})
v.validators[0].limit_value += ['{meta_' + p + '}' for p in keys]
if '{attendee_name}' in v.validators[0].limit_value:
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
v.help_text = str(v.help_text) + ', ' + '{attendee_name_%s}' % f
v.validators[0].limit_value += ['{attendee_name_' + f + '}']
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
# the user interface with it
del self.fields[k]
def clean(self):
data = self.cleaned_data
if not data.get('smtp_password') and data.get('smtp_username'):
@@ -1166,7 +1046,6 @@ class DisplaySettingsForm(SettingsForm):
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
frontpage_text = I18nFormField(

View File

@@ -6,9 +6,6 @@ from django.urls import reverse
from django.utils.translation import (
pgettext_lazy, ugettext as __, ugettext_lazy as _,
)
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.channels import get_all_sales_channels
@@ -85,7 +82,6 @@ class QuestionForm(I18nModelForm):
'type',
'required',
'ask_during_checkin',
'hidden',
'identifier',
'items',
'dependency_question',
@@ -97,10 +93,6 @@ class QuestionForm(I18nModelForm):
),
'dependency_value': forms.Select,
}
field_classes = {
'items': SafeModelMultipleChoiceField,
'dependency_question': SafeModelChoiceField,
}
class QuestionOptionForm(I18nModelForm):
@@ -166,9 +158,6 @@ class QuotaForm(I18nModelForm):
'size',
'subevent'
]
field_classes = {
'subevent': SafeModelChoiceField,
}
def save(self, *args, **kwargs):
creating = not self.instance.pk
@@ -216,8 +205,6 @@ class ItemCreateForm(I18nModelForm):
empty_label=_('Do not copy'),
required=False
)
if self.event.tax_rules.exists():
self.fields['tax_rule'].required = True
if not self.event.has_subevents:
choices = [
@@ -376,8 +363,6 @@ class ItemUpdateForm(I18nModelForm):
'over 65. This ticket includes access to all parts of the event, except the VIP '
'area.'
)
if self.event.tax_rules.exists():
self.fields['tax_rule'].required = True
self.fields['description'].widget.attrs['rows'] = '4'
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=_('Sales channels'),

View File

@@ -9,7 +9,6 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
@@ -117,12 +116,6 @@ class MarkPaidForm(ConfirmPaymentForm):
localize=True,
label=_('Payment amount'),
)
payment_date = forms.DateField(
required=True,
label=_('Payment date'),
widget=DatePickerWidget(),
initial=now
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -192,7 +185,7 @@ class OrderPositionAddForm(forms.Form):
label=_('Product')
)
addon_to = forms.ModelChoiceField(
OrderPosition.all.none(),
OrderPosition.objects.none(),
required=False,
label=_('Add-on to'),
)
@@ -292,7 +285,10 @@ class OrderPositionChangeForm(forms.Form):
instance = kwargs.pop('instance')
initial = kwargs.get('initial', {})
initial['price'] = instance.price
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
initial['price'] = instance.price - instance.tax_value
else:
initial['price'] = instance.price
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
@@ -344,7 +340,7 @@ class OrderContactForm(forms.ModelForm):
class Meta:
model = Order
fields = ['email', 'email_known_to_work']
fields = ['email']
class OrderLocaleForm(forms.ModelForm):

View File

@@ -6,16 +6,13 @@ from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.safestring import mark_safe
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Device, Organizer, Team
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget,
)
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
from pretix.multidomain.models import KnownDomain
from pretix.presale.style import get_fonts
@@ -150,9 +147,6 @@ class TeamForm(forms.ModelForm):
'data-inverse-dependency': '#id_all_events'
}),
}
field_classes = {
'limit_events': SafeModelMultipleChoiceField
}
def clean(self):
data = super().clean()
@@ -181,9 +175,6 @@ class DeviceForm(forms.ModelForm):
'data-inverse-dependency': '#id_all_events'
}),
}
field_classes = {
'limit_events': SafeModelMultipleChoiceField
}
class OrganizerSettingsForm(SettingsForm):
@@ -269,7 +260,6 @@ class OrganizerDisplaySettingsForm(SettingsForm):
choices=[
('Open Sans', 'Open Sans')
],
widget=FontSelect,
help_text=_('Only respected by modern browsers.')
)
favicon = ExtFileField(
@@ -314,6 +304,3 @@ class WebHookForm(forms.ModelForm):
'data-inverse-dependency': '#id_all_events'
}),
}
field_classes = {
'limit_events': SafeModelMultipleChoiceField
}

View File

@@ -29,7 +29,6 @@ class SubEventForm(I18nModelForm):
fields = [
'name',
'active',
'is_public',
'date_from',
'date_to',
'date_admission',

View File

@@ -48,14 +48,12 @@ class UserEditForm(forms.ModelForm):
'email',
'require_2fa',
'is_active',
'is_staff',
'last_login'
'is_staff'
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email'].required = True
self.fields['last_login'].disabled = True
def clean_email(self):
email = self.cleaned_data['email']

View File

@@ -3,7 +3,6 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models.functions import Lower
from django.urls import reverse
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes.forms import SafeModelChoiceField
from pretix.base.forms import I18nModelForm
from pretix.base.models import Item, Voucher
@@ -36,7 +35,6 @@ class VoucherForm(I18nModelForm):
]
field_classes = {
'valid_until': SplitDateTimeField,
'subevent': SafeModelChoiceField,
}
widgets = {
'valid_until': SplitDateTimePickerWidget(),
@@ -142,20 +140,12 @@ class VoucherForm(I18nModelForm):
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
cnt = len(data['codes']) * data.get('max_usages', 0)
else:
cnt = data.get('max_usages', 0)
cnt = data['max_usages']
Voucher.clean_item_properties(
data, self.instance.event,
self.instance.quota, self.instance.item, self.instance.variation
)
if self.instance.quota:
if all(i.hide_without_voucher for i in self.instance.quota.items.all()):
raise ValidationError({
'itemvar': [
_('The quota you selected only contains hidden products. Hidden products can currently only be '
'shown by using vouchers that directly apply to the product, not via a quota.')
]
})
Voucher.clean_subevent(
data, self.instance.event
)
@@ -201,7 +191,6 @@ class VoucherBulkForm(VoucherForm):
]
field_classes = {
'valid_until': SplitDateTimeField,
'subevent': SafeModelChoiceField,
}
widgets = {
'valid_until': SplitDateTimePickerWidget(),

View File

@@ -189,8 +189,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
'pretix.event.order.email.error': _('Sending of an email has failed.'),
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attachments since they '
'would have been too large to be likely to arrive.'),
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
'pretix.event.order.email.download_reminder_sent': _('An email has been sent with a reminder that the ticket '
'is available for download.'),
@@ -251,9 +249,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.item.addons.added': _('An add-on has been added to this product.'),
'pretix.event.item.addons.removed': _('An add-on has been removed from this product.'),
'pretix.event.item.addons.changed': _('An add-on has been changed on this product.'),
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
'pretix.event.quota.added': _('The quota has been added.'),
'pretix.event.quota.deleted': _('The quota has been deleted.'),
'pretix.event.quota.changed': _('The quota has been changed.'),

View File

@@ -4,11 +4,10 @@ from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.template.response import TemplateResponse
from django.urls import get_script_prefix, resolve, reverse
from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_str
from django.utils.translation import ugettext as _
from django_scopes import scope
from hijack.templatetags.hijack_tags import is_hijacked
from pretix.base.models import Event, Organizer
@@ -18,7 +17,7 @@ from pretix.helpers.security import (
)
class PermissionMiddleware:
class PermissionMiddleware(MiddlewareMixin):
"""
This middleware enforces all requests to the control app to require login.
Additionally, it enforces all requests to "control:event." URLs
@@ -35,10 +34,6 @@ class PermissionMiddleware:
"user.settings.notifications.off",
)
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def _login_redirect(self, request):
# Taken from django/contrib/auth/decorators.py
path = request.build_absolute_uri()
@@ -57,19 +52,19 @@ class PermissionMiddleware:
return redirect_to_login(
path, resolved_login_url, REDIRECT_FIELD_NAME)
def __call__(self, request):
def process_request(self, request):
url = resolve(request.path_info)
url_name = url.url_name
if not request.path.startswith(get_script_prefix() + 'control'):
# This middleware should only touch the /control subpath
return self.get_response(request)
return
if hasattr(request, 'organizer'):
# If the user is on a organizer's subdomain, he should be redirected to pretix
return redirect(urljoin(settings.SITE_URL, request.get_full_path()))
if url_name in self.EXCEPTIONS:
return self.get_response(request)
return
if not request.user.is_authenticated:
return self._login_redirect(request)
@@ -84,11 +79,10 @@ class PermissionMiddleware:
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
if 'event' in url.kwargs and 'organizer' in url.kwargs:
with scope(organizer=None):
request.event = Event.objects.filter(
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'],
).select_related('organizer').first()
request.event = Event.objects.filter(
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'],
).select_related('organizer').first()
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event,
request=request):
raise Http404(_("The selected event was not found or you "
@@ -110,12 +104,6 @@ class PermissionMiddleware:
else:
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
with scope(organizer=getattr(request, 'organizer', None)):
r = self.get_response(request)
if isinstance(r, TemplateResponse):
r = r.render()
return r
class AuditLogMiddleware:

View File

@@ -281,7 +281,7 @@ def get_event_navigation(request: HttpRequest):
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_event.send(request.event, request=request)), []),
key=lambda r: (1 if r.get('parent') else 0, r['label'])
key=lambda r: r['label']
))
return nav
@@ -391,7 +391,7 @@ def get_global_navigation(request):
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_global.send(request, request=request)), []),
key=lambda r: (1 if r.get('parent') else 0, r['label'])
key=lambda r: r['label']
))
return nav
@@ -464,7 +464,7 @@ def get_organizer_navigation(request):
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
[]),
key=lambda r: (1 if r.get('parent') else 0, r['label'])
key=lambda r: r['label']
))
return nav
@@ -474,8 +474,6 @@ def merge_in(nav, newnav):
if 'parent' in item:
parents = [n for n in nav if n['url'] == item['parent']]
if parents:
if 'children' not in parents[0]:
parents[0]['children'] = []
parents[0]['children'].append(item)
else:
nav.append(item)

View File

@@ -2,6 +2,16 @@ from django.dispatch import Signal
from pretix.base.signals import DeprecatedSignal, EventPluginSignal
restriction_formset = EventPluginSignal(
providing_args=["item"]
)
"""
This signal is sent out to build configuration forms for all restriction formsets
(see plugin API documentation for details).
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
html_page_start = Signal(
providing_args=[]
)
@@ -36,12 +46,6 @@ on the type of navigation. You should also return an ``active`` key with a boole
set to ``True``, when this item should be marked as active. The ``request`` object
will have an attribute ``event``.
You can optionally create sub-items to create hierarchical navigation. There are two
ways to achieve this: Either you specify a key ``children`` on your top navigation item
that contains a list of navigation items (as dictionaries), or you specify a ``parent``
key with the ``url`` value of the designated parent item.
The latter method also allows you to register navigation items as a sub-item of existing ones.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
@@ -79,12 +83,6 @@ a fontawesome icon name with the key ``icon``, it will be respected depending
on the type of navigation. You should also return an ``active`` key with a boolean
set to ``True``, when this item should be marked as active.
You can optionally create sub-items to create hierarchical navigation. There are two
ways to achieve this: Either you specify a key ``children`` on your top navigation item
that contains a list of navigation items (as dictionaries), or you specify a ``parent``
key with the ``url`` value of the designated parent item.
The latter method also allows you to register navigation items as a sub-item of existing ones.
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
in pretix.
@@ -185,12 +183,6 @@ should contain at least the keys ``label`` and ``url``. You should also return
an ``active`` key with a boolean set to ``True``, when this item should be marked
as active.
You can optionally create sub-items to create hierarchical navigation. There are two
ways to achieve this: Either you specify a key ``children`` on your top navigation item
that contains a list of navigation items (as dictionaries), or you specify a ``parent``
key with the ``url`` value of the designated parent item.
The latter method also allows you to register navigation items as a sub-item of existing ones.
If your linked view should stay in the tab-like context of this page, we recommend
that you use ``pretix.control.views.organizer.OrganizerDetailViewMixin`` for your view
and your template inherits from ``pretixcontrol/organizers/base.html``.
@@ -237,24 +229,6 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
A second keyword argument ``request`` will contain the request object.
"""
nav_item = EventPluginSignal(
providing_args=['request', 'item']
)
"""
This signal is sent out to include tab links on the settings page of an item.
Receivers are expected to return a list of dictionaries. The dictionaries
should contain at least the keys ``label`` and ``url``. You should also return
an ``active`` key with a boolean set to ``True``, when this item should be marked
as active.
If your linked view should stay in the tab-like context of this page, we recommend
that you use ``pretix.control.views.item.ItemDetailMixin`` for your view
and your template inherits from ``pretixcontrol/item/base.html``.
As with all plugin signals, the ``sender`` keyword argument will contain the event.
A second keyword argument ``request`` will contain the request object.
"""
event_settings_widget = EventPluginSignal(
providing_args=['request']
)

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