forked from CGM_Public/pretix_original
Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd81a2d20 | ||
|
|
4271245a4a | ||
|
|
b41139a143 | ||
|
|
a68b225529 | ||
|
|
c12ba88ad8 | ||
|
|
69a80f2540 | ||
|
|
867667cd12 | ||
|
|
5c06c41a5b | ||
|
|
b1db5dbb3e | ||
|
|
fed389b990 | ||
|
|
1ce613ff89 | ||
|
|
44bef85b66 | ||
|
|
49c4acefd0 | ||
|
|
61e111742d | ||
|
|
b2274039b3 | ||
|
|
4913190730 | ||
|
|
276a087fdb | ||
|
|
3639f2cea1 | ||
|
|
692e9c38f1 | ||
|
|
dd4075b2cc | ||
|
|
b549cb451a | ||
|
|
576132b2d0 | ||
|
|
e0c432d014 | ||
|
|
b66a35df7a | ||
|
|
2e1347cf9a | ||
|
|
8d1c9e44fc | ||
|
|
a187a02daa | ||
|
|
b9d100b5a8 | ||
|
|
f1e097c1b1 | ||
|
|
91a5b1546a | ||
|
|
3532f9c5a9 | ||
|
|
884e54180a | ||
|
|
e17ddb0cc8 | ||
|
|
55edc8a3d6 | ||
|
|
bd79a93737 | ||
|
|
12ab260eb1 | ||
|
|
30f0318de6 | ||
|
|
52e072e68f | ||
|
|
f25bb571b9 | ||
|
|
ae71492902 | ||
|
|
57375eb9b6 | ||
|
|
0657ef2e0c | ||
|
|
f63907fb16 | ||
|
|
e266d3808f | ||
|
|
180f9a356f | ||
|
|
480b71bd50 | ||
|
|
79839e3735 | ||
|
|
6ba5c58556 | ||
|
|
a5e3bab107 | ||
|
|
4dcce70ab3 | ||
|
|
8a5332f415 | ||
|
|
58ce1cbab7 | ||
|
|
27c3e5d875 | ||
|
|
caac517c0d | ||
|
|
58b9052164 | ||
|
|
2d223a9e11 | ||
|
|
fe37ab9286 | ||
|
|
95cc661a05 | ||
|
|
9a98d16949 | ||
|
|
50ba019a07 | ||
|
|
7d3e9b1777 | ||
|
|
f82640d763 | ||
|
|
d84cd71a5c | ||
|
|
74105ddd53 | ||
|
|
3a2f915ac9 | ||
|
|
9024a552a9 | ||
|
|
bae9fab2c4 | ||
|
|
ee3cd6d465 | ||
|
|
ccdcd380fa | ||
|
|
3c0f0434cd | ||
|
|
58dba57bef | ||
|
|
9178aef323 | ||
|
|
e9d696ea5e | ||
|
|
983ffdd8a8 | ||
|
|
294d47ccfc | ||
|
|
a14b1a5a14 | ||
|
|
a28c5f71c9 | ||
|
|
35bd9d1c22 | ||
|
|
b070fc0297 | ||
|
|
f7fd3596a6 | ||
|
|
3b4f758c82 | ||
|
|
df8c8f2063 | ||
|
|
ebb6b5b469 | ||
|
|
16ad39bb16 | ||
|
|
6ca65edde9 | ||
|
|
02684a0fcd | ||
|
|
141ba6e50d | ||
|
|
6681eb1a27 | ||
|
|
2b515ea30c | ||
|
|
7997882e24 | ||
|
|
a8190258a4 | ||
|
|
9376a26709 | ||
|
|
d8e2e0e217 | ||
|
|
f9c942bc6f | ||
|
|
f9b7696366 | ||
|
|
2143135285 | ||
|
|
54146bb9e8 | ||
|
|
d1f702cafd | ||
|
|
54e7b8da89 | ||
|
|
25af386d87 | ||
|
|
51fa9e78dd | ||
|
|
6cf244bb4b | ||
|
|
6a0e3b1b46 | ||
|
|
571b0e9aa8 | ||
|
|
8075d3e385 | ||
|
|
411f5c358f | ||
|
|
94d3eff799 | ||
|
|
a073d66213 | ||
|
|
7b2dda9cd9 | ||
|
|
e9c66f5bb1 | ||
|
|
24aa8fc033 | ||
|
|
ee74f75913 | ||
|
|
b484675aeb | ||
|
|
88379d7c25 | ||
|
|
4e8bdb4427 | ||
|
|
f68c29ca95 | ||
|
|
eb7154a55b | ||
|
|
ed19cc99f3 | ||
|
|
51ae1e5e33 | ||
|
|
132f8d8cb3 | ||
|
|
cd4b4b98b8 | ||
|
|
07e0ffd4f3 | ||
|
|
1d2a6d55b9 | ||
|
|
33b893b0ba | ||
|
|
03ebe0e528 | ||
|
|
28797b8cc6 | ||
|
|
691ba3a1a7 | ||
|
|
656673ccde | ||
|
|
7073622ab3 | ||
|
|
b8f71d2428 | ||
|
|
2a8bdc29f4 | ||
|
|
a6da1bb4e9 | ||
|
|
d1e67d38d9 | ||
|
|
a5214d459c | ||
|
|
9ad2891d17 | ||
|
|
4b2d55a2fb | ||
|
|
5cfed32d61 | ||
|
|
06ccd83921 | ||
|
|
ba417b6e3c | ||
|
|
f7ed0236f3 | ||
|
|
4491b80786 | ||
|
|
37bcb520cc | ||
|
|
782e957c3a | ||
|
|
953890c269 | ||
|
|
60d9c1080a | ||
|
|
364e7cefda | ||
|
|
33accf3250 | ||
|
|
be4d9ac00e | ||
|
|
8ca5e4dd54 | ||
|
|
1394cf3148 | ||
|
|
de58b35bf4 | ||
|
|
490b421d53 | ||
|
|
61d45f26dd | ||
|
|
a8b0475c6d | ||
|
|
31cf94eb02 | ||
|
|
dc0590ea91 | ||
|
|
bc5e5d0a27 | ||
|
|
0fc448fbd3 | ||
|
|
67d5c1ccad | ||
|
|
779ad11640 | ||
|
|
70e9d9faad | ||
|
|
51f5b0645a | ||
|
|
b3436c1a93 | ||
|
|
9eef5d5d6d | ||
|
|
e139de3c19 | ||
|
|
74f861bd48 | ||
|
|
35c02f35d7 | ||
|
|
d5c0b0f71d | ||
|
|
6c701d66b1 | ||
|
|
8d62b509a2 | ||
|
|
9e0b97e88e | ||
|
|
28a5519881 | ||
|
|
363826e294 | ||
|
|
eb8ea6d477 | ||
|
|
77be4d835b | ||
|
|
c6390520a7 | ||
|
|
594803ec17 | ||
|
|
32ce3a4319 | ||
|
|
d3f01832fe | ||
|
|
bba702489d | ||
|
|
85fe7e55be | ||
|
|
92c9216fbd | ||
|
|
db63e20708 | ||
|
|
e2ce35a85b | ||
|
|
d39964b021 | ||
|
|
59beba5069 | ||
|
|
1bd3a63959 | ||
|
|
1d644e90c9 | ||
|
|
e0e66c903e | ||
|
|
bc08bdebb5 | ||
|
|
edd92ac34d | ||
|
|
f1bce0c08b | ||
|
|
68c24ebea3 | ||
|
|
d22a7844ea | ||
|
|
6238e1df98 | ||
|
|
6acca4c4ba | ||
|
|
1a9f6e49d4 | ||
|
|
efa1d2683e | ||
|
|
9b39d34f81 | ||
|
|
96c5c8c4ff | ||
|
|
3254ac36a2 | ||
|
|
52d10957a1 | ||
|
|
f9d4669423 | ||
|
|
6e220cbbd8 | ||
|
|
036a555374 | ||
|
|
861a41c95f | ||
|
|
e2abc19fe3 | ||
|
|
97fc226e07 | ||
|
|
d73c98bff0 | ||
|
|
aa186f7a09 | ||
|
|
1b434b40d2 | ||
|
|
71475c5863 | ||
|
|
71b544d951 | ||
|
|
b0685437f1 | ||
|
|
2d99828eab | ||
|
|
2ffc1b8eaf | ||
|
|
893f47d365 | ||
|
|
7de1fca2f4 | ||
|
|
c6b18b31a1 | ||
|
|
ecc9c7f39f | ||
|
|
b9aba9cf56 | ||
|
|
33f0892052 | ||
|
|
4bf3d48549 | ||
|
|
32aa4b4f3e | ||
|
|
e1992bb99f | ||
|
|
45e98546d6 | ||
|
|
c7774dfdb8 | ||
|
|
6c582b8f8c | ||
|
|
5f82db3949 | ||
|
|
2b818f42cd | ||
|
|
b19df33dda | ||
|
|
dba8761bc5 | ||
|
|
0311c0251a | ||
|
|
5b99bf3623 | ||
|
|
4137e0fc1f | ||
|
|
b32c6033f1 | ||
|
|
de0e700fec | ||
|
|
00bc5f4fae | ||
|
|
6ef3603d9f | ||
|
|
2c7cefea35 | ||
|
|
a10b31cacb | ||
|
|
4e9e925b32 | ||
|
|
f4415cf906 | ||
|
|
bf4fcfd914 | ||
|
|
7021c178ab | ||
|
|
5d8e3e28d6 |
@@ -25,8 +25,6 @@ matrix:
|
|||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.5
|
- python: 3.5
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.7
|
|
||||||
env: JOB=plugins
|
|
||||||
- python: 3.7
|
- python: 3.7
|
||||||
env: JOB=doc-spelling
|
env: JOB=doc-spelling
|
||||||
- python: 3.7
|
- python: 3.7
|
||||||
|
|||||||
@@ -273,6 +273,24 @@ to speed up various operations::
|
|||||||
If redis is not configured, pretix will store sessions and locks in the database. If memcached
|
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.
|
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
|
Celery task queue
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|||||||
131
doc/api/resources/billing_invoices.rst
Normal file
131
doc/api/resources/billing_invoices.rst
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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.
|
||||||
@@ -50,6 +50,10 @@ plugins list A list of packa
|
|||||||
|
|
||||||
The ``testmode`` attribute has been added.
|
The ``testmode`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.8
|
||||||
|
|
||||||
|
When cloning events, the ``testmode`` attribute will now be cloned, too.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -112,6 +116,9 @@ 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_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 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 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
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -246,7 +253,7 @@ Endpoints
|
|||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
|
.. 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',
|
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
|
||||||
settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
`testmode`, 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
|
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.
|
their value will be copied from the existing event.
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ Resources and endpoints
|
|||||||
waitinglist
|
waitinglist
|
||||||
carts
|
carts
|
||||||
webhooks
|
webhooks
|
||||||
|
billing_invoices
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
: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 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ source_suffix = '.rst'
|
|||||||
#source_encoding = 'utf-8-sig'
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'contents'
|
master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'pretix'
|
project = 'pretix'
|
||||||
@@ -234,7 +234,7 @@ latex_elements = {
|
|||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
('contents', 'pretix.tex', 'pretix Documentation',
|
('index', 'pretix.tex', 'pretix Documentation',
|
||||||
'Raphael Michel', 'manual'),
|
'Raphael Michel', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
: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
|
: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
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. 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
|
: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
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -45,11 +45,11 @@ Backend
|
|||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
: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
|
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, nav_item
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: logentry_display, logentry_object_link, requiredaction_display
|
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ Your should install the following on your system:
|
|||||||
* Python 3.5 or newer
|
* Python 3.5 or newer
|
||||||
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
||||||
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
* ``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``)
|
* ``libffi`` (Debian package: ``libffi-dev``)
|
||||||
* ``libssl`` (Debian package: ``libssl-dev``)
|
* ``libssl`` (Debian package: ``libssl-dev``)
|
||||||
* ``libxml2`` (Debian package ``libxml2-dev``)
|
* ``libxml2`` (Debian package ``libxml2-dev``)
|
||||||
* ``libxslt`` (Debian package ``libxslt1-dev``)
|
* ``libxslt`` (Debian package ``libxslt1-dev``)
|
||||||
|
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
|
||||||
* ``msgfmt`` (Debian package ``gettext``)
|
* ``msgfmt`` (Debian package ``gettext``)
|
||||||
* ``git``
|
* ``git``
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "2.7.0"
|
__version__ = "2.9.0.dev0"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
def authenticate_credentials(self, key):
|
def authenticate_credentials(self, key):
|
||||||
model = self.get_model()
|
model = self.get_model()
|
||||||
try:
|
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:
|
except model.DoesNotExist:
|
||||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
|||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.base.models import Device, Event, User
|
from pretix.base.models import Device, Event, User
|
||||||
from pretix.base.models.auth import SuperuserPermissionSet
|
from pretix.base.models.auth import SuperuserPermissionSet
|
||||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
from pretix.base.models.organizer import TeamAPIToken
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||||
)
|
)
|
||||||
@@ -50,9 +50,6 @@ class EventPermission(BasePermission):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
elif 'organizer' in request.resolver_match.kwargs:
|
elif 'organizer' in request.resolver_match.kwargs:
|
||||||
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, request=request):
|
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
|
||||||
return False
|
return False
|
||||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ from hashlib import sha1
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
|
from django.urls import resolve
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scope
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from pretix.api.models import ApiCall
|
from pretix.api.models import ApiCall
|
||||||
|
from pretix.base.models import Organizer
|
||||||
|
|
||||||
|
|
||||||
class IdempotencyMiddleware:
|
class IdempotencyMiddleware:
|
||||||
@@ -89,3 +92,21 @@ class IdempotencyMiddleware:
|
|||||||
for k, v in json.loads(call.response_headers).values():
|
for k, v in json.loads(call.response_headers).values():
|
||||||
r[k] = v
|
r[k] = v
|
||||||
return r
|
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)
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
plugins = validated_data.pop('plugins', None)
|
plugins = validated_data.pop('plugins', None)
|
||||||
is_public = validated_data.pop('is_public', None)
|
is_public = validated_data.pop('is_public', None)
|
||||||
|
testmode = validated_data.pop('testmode', None)
|
||||||
new_event = super().create(validated_data)
|
new_event = super().create(validated_data)
|
||||||
|
|
||||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||||
@@ -173,6 +174,8 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
new_event.set_active_plugins(plugins)
|
new_event.set_active_plugins(plugins)
|
||||||
if is_public is not None:
|
if is_public is not None:
|
||||||
new_event.is_public = is_public
|
new_event.is_public = is_public
|
||||||
|
if testmode is not None:
|
||||||
|
new_event.testmode = testmode
|
||||||
new_event.save()
|
new_event.save()
|
||||||
|
|
||||||
return new_event
|
return new_event
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.dispatch import Signal, receiver
|
from django.dispatch import Signal, receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import ApiCall, WebHookCall
|
from pretix.api.models import ApiCall, WebHookCall
|
||||||
from pretix.base.signals import periodic_task
|
from pretix.base.signals import periodic_task
|
||||||
@@ -17,10 +18,12 @@ instances.
|
|||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task)
|
@receiver(periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def cleanup_webhook_logs(sender, **kwargs):
|
def cleanup_webhook_logs(sender, **kwargs):
|
||||||
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task)
|
@receiver(periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def cleanup_api_logs(sender, **kwargs):
|
def cleanup_api_logs(sender, **kwargs):
|
||||||
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
@@ -24,8 +25,8 @@ from pretix.base.services.checkin import (
|
|||||||
)
|
)
|
||||||
from pretix.helpers.database import FixedOrderBy
|
from pretix.helpers.database import FixedOrderBy
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
class CheckinListFilter(FilterSet):
|
class CheckinListFilter(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
fields = ['subevent']
|
fields = ['subevent']
|
||||||
@@ -146,7 +147,8 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
class CheckinOrderPositionFilter(OrderPositionFilter):
|
with scopes_disabled():
|
||||||
|
class CheckinOrderPositionFilter(OrderPositionFilter):
|
||||||
|
|
||||||
def has_checkin_qs(self, queryset, name, value):
|
def has_checkin_qs(self, queryset, name, value):
|
||||||
return queryset.filter(last_checked_in__isnull=not value)
|
return queryset.filter(last_checked_in__isnull=not value)
|
||||||
@@ -154,7 +156,7 @@ class CheckinOrderPositionFilter(OrderPositionFilter):
|
|||||||
|
|
||||||
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = CheckinListOrderPositionSerializer
|
serializer_class = CheckinListOrderPositionSerializer
|
||||||
queryset = OrderPosition.objects.none()
|
queryset = OrderPosition.all.none()
|
||||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||||
ordering = ('attendee_name_cached', 'positionid')
|
ordering = ('attendee_name_cached', 'positionid')
|
||||||
ordering_fields = (
|
ordering_fields = (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.db import transaction
|
|||||||
from django.db.models import ProtectedError, Q
|
from django.db.models import ProtectedError, Q
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import filters, viewsets
|
from rest_framework import filters, viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
@@ -18,8 +19,8 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
class EventFilter(FilterSet):
|
class EventFilter(FilterSet):
|
||||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||||
@@ -72,6 +73,8 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
lookup_url_kwarg = 'event'
|
lookup_url_kwarg = 'event'
|
||||||
permission_classes = (EventCRUDPermission,)
|
permission_classes = (EventCRUDPermission,)
|
||||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
|
ordering = ('slug',)
|
||||||
|
ordering_fields = ('date_from', 'slug')
|
||||||
filterset_class = EventFilter
|
filterset_class = EventFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -180,7 +183,8 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubEventFilter(FilterSet):
|
with scopes_disabled():
|
||||||
|
class SubEventFilter(FilterSet):
|
||||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||||
@@ -242,8 +246,14 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
super().perform_update(serializer)
|
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(
|
serializer.instance.log_action(
|
||||||
'pretix.subevent.changed',
|
'pretix.subevent.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.db.models import Q
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
@@ -21,8 +22,8 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
class ItemFilter(FilterSet):
|
class ItemFilter(FilterSet):
|
||||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||||
|
|
||||||
def tax_rate_qs(self, queryset, name, value):
|
def tax_rate_qs(self, queryset, name, value):
|
||||||
@@ -65,7 +66,14 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
|
|
||||||
serializer.save(event=self.request.event)
|
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(
|
serializer.instance.log_action(
|
||||||
'pretix.event.item.changed',
|
'pretix.event.item.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -312,7 +320,8 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
class QuestionFilter(FilterSet):
|
with scopes_disabled():
|
||||||
|
class QuestionFilter(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = ['ask_during_checkin', 'required', 'identifier']
|
fields = ['ask_during_checkin', 'required', 'identifier']
|
||||||
@@ -411,7 +420,8 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
class QuotaFilter(FilterSet):
|
with scopes_disabled():
|
||||||
|
class QuotaFilter(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Quota
|
model = Quota
|
||||||
fields = ['subevent']
|
fields = ['subevent']
|
||||||
@@ -452,9 +462,17 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
|
|
||||||
current_subevent = serializer.instance.subevent
|
current_subevent = serializer.instance.subevent
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
request_subevent = serializer.instance.subevent
|
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(
|
serializer.instance.log_action(
|
||||||
'pretix.event.quota.changed',
|
'pretix.event.quota.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
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 import mixins, serializers, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import (
|
from rest_framework.exceptions import (
|
||||||
@@ -50,8 +51,8 @@ from pretix.base.signals import (
|
|||||||
)
|
)
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
class OrderFilter(FilterSet):
|
class OrderFilter(FilterSet):
|
||||||
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
||||||
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
||||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||||
@@ -482,6 +483,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
|
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(
|
serializer.instance.log_action(
|
||||||
'pretix.event.order.contact.changed',
|
'pretix.event.order.contact.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -530,7 +532,8 @@ 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)
|
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
|
||||||
|
|
||||||
class OrderPositionFilter(FilterSet):
|
with scopes_disabled():
|
||||||
|
class OrderPositionFilter(FilterSet):
|
||||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||||
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
attendee_name = django_filters.CharFilter(method='attendee_name_qs')
|
||||||
@@ -571,7 +574,7 @@ class OrderPositionFilter(FilterSet):
|
|||||||
|
|
||||||
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrderPositionSerializer
|
serializer_class = OrderPositionSerializer
|
||||||
queryset = OrderPosition.objects.none()
|
queryset = OrderPosition.all.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('order__datetime', 'positionid')
|
ordering = ('order__datetime', 'positionid')
|
||||||
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
||||||
@@ -959,7 +962,8 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
class InvoiceFilter(FilterSet):
|
with scopes_disabled():
|
||||||
|
class InvoiceFilter(FilterSet):
|
||||||
refers = django_filters.CharFilter(method='refers_qs')
|
refers = django_filters.CharFilter(method='refers_qs')
|
||||||
number = django_filters.CharFilter(method='nr_qs')
|
number = django_filters.CharFilter(method='nr_qs')
|
||||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import filters, viewsets
|
||||||
|
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.organizer import OrganizerSerializer
|
from pretix.api.serializers.organizer import OrganizerSerializer
|
||||||
@@ -10,6 +10,9 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
queryset = Organizer.objects.none()
|
queryset = Organizer.objects.none()
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
lookup_url_kwarg = 'organizer'
|
lookup_url_kwarg = 'organizer'
|
||||||
|
filter_backends = (filters.OrderingFilter,)
|
||||||
|
ordering = ('slug',)
|
||||||
|
ordering_fields = ('name', 'slug')
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.utils.timezone import now
|
|||||||
from django_filters.rest_framework import (
|
from django_filters.rest_framework import (
|
||||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||||
)
|
)
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
@@ -15,8 +16,8 @@ from rest_framework.response import Response
|
|||||||
from pretix.api.serializers.voucher import VoucherSerializer
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
class VoucherFilter(FilterSet):
|
class VoucherFilter(FilterSet):
|
||||||
active = BooleanFilter(method='filter_active')
|
active = BooleanFilter(method='filter_active')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||||
@@ -10,8 +11,8 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
|
|||||||
from pretix.base.models import WaitingListEntry
|
from pretix.base.models import WaitingListEntry
|
||||||
from pretix.base.models.waitinglist import WaitingListException
|
from pretix.base.models.waitinglist import WaitingListException
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
class WaitingListFilter(FilterSet):
|
class WaitingListFilter(FilterSet):
|
||||||
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
||||||
|
|
||||||
def has_voucher_qs(self, queryset, name, value):
|
def has_voucher_qs(self, queryset, name, value):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from celery.exceptions import MaxRetriesExceededError
|
|||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
||||||
@@ -203,9 +204,10 @@ def notify_webhooks(logentry_id: int):
|
|||||||
@app.task(base=ProfiledTask, bind=True, max_retries=9)
|
@app.task(base=ProfiledTask, bind=True, max_retries=9)
|
||||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||||
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||||
logentry = LogEntry.all.get(id=logentry_id)
|
with scopes_disabled():
|
||||||
webhook = WebHook.objects.get(id=webhook_id)
|
webhook = WebHook.objects.get(id=webhook_id)
|
||||||
|
with scope(organizer=webhook.organizer):
|
||||||
|
logentry = LogEntry.all.get(id=logentry_id)
|
||||||
types = get_all_webhook_events()
|
types = get_all_webhook_events()
|
||||||
event_type = types.get(action_type)
|
event_type = types.get(action_type)
|
||||||
if not event_type or not webhook.enabled:
|
if not event_type or not webhook.enabled:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.template.loader import get_template
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from inlinestyler.utils import inline_css
|
from inlinestyler.utils import inline_css
|
||||||
|
|
||||||
from pretix.base.models import Event, Order
|
from pretix.base.models import Event, Order, OrderPosition
|
||||||
from pretix.base.signals import register_html_mail_renderers
|
from pretix.base.signals import register_html_mail_renderers
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
|
|
||||||
@@ -44,7 +44,8 @@ class BaseHTMLMailRenderer:
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.identifier
|
return self.identifier
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
|
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
|
||||||
|
position: OrderPosition=None) -> str:
|
||||||
"""
|
"""
|
||||||
This method should generate the HTML part of the email.
|
This method should generate the HTML part of the email.
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ class BaseHTMLMailRenderer:
|
|||||||
:param plain_signature: The signature with event organizer contact details in plain text.
|
:param plain_signature: The signature with event organizer contact details in plain text.
|
||||||
:param subject: The email subject.
|
:param subject: The email subject.
|
||||||
:param order: The order if this email is connected to one, otherwise ``None``.
|
: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
|
:return: An HTML string
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -95,7 +97,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
def template_name(self):
|
def template_name(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
|
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
|
||||||
body_md = markdown_compile_email(plain_body)
|
body_md = markdown_compile_email(plain_body)
|
||||||
htmlctx = {
|
htmlctx = {
|
||||||
'site': settings.PRETIX_INSTANCE_NAME,
|
'site': settings.PRETIX_INSTANCE_NAME,
|
||||||
@@ -116,6 +118,9 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
if order:
|
if order:
|
||||||
htmlctx['order'] = order
|
htmlctx['order'] = order
|
||||||
|
|
||||||
|
if position:
|
||||||
|
htmlctx['position'] = position
|
||||||
|
|
||||||
tpl = get_template(self.template_name)
|
tpl = get_template(self.template_name)
|
||||||
body_html = inline_css(tpl.render(htmlctx))
|
body_html = inline_css(tpl.render(htmlctx))
|
||||||
return body_html
|
return body_html
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class AnswerFilesExporter(BaseExporter):
|
|||||||
if form_data.get('questions'):
|
if form_data.get('questions'):
|
||||||
qs = qs.filter(question__in=form_data['questions'])
|
qs = qs.filter(question__in=form_data['questions'])
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
any = False
|
||||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||||
for i in qs:
|
for i in qs:
|
||||||
if i.file:
|
if i.file:
|
||||||
@@ -51,9 +52,12 @@ class AnswerFilesExporter(BaseExporter):
|
|||||||
i.question.pk,
|
i.question.pk,
|
||||||
os.path.basename(i.file.name).split('.', 1)[1]
|
os.path.basename(i.file.name).split('.', 1)[1]
|
||||||
)
|
)
|
||||||
|
any = True
|
||||||
zipf.writestr(fname, i.file.read())
|
zipf.writestr(fname, i.file.read())
|
||||||
i.file.close()
|
i.file.close()
|
||||||
|
|
||||||
|
if not any:
|
||||||
|
return None
|
||||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||||
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
'PTNo15': p.full_id or '',
|
'PTNo15': p.full_id or '',
|
||||||
})
|
})
|
||||||
elif p.provider.startswith('stripe'):
|
elif p.provider.startswith('stripe'):
|
||||||
src = p.info_data.get("source", "{}")
|
src = p.info_data.get("source", p.info_data)
|
||||||
payments.append({
|
payments.append({
|
||||||
'PTID': '81',
|
'PTID': '81',
|
||||||
'PTN': 'Stripe',
|
'PTN': 'Stripe',
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class InvoiceExporter(BaseExporter):
|
|||||||
qs = qs.filter(date__lte=date_value)
|
qs = qs.filter(date__lte=date_value)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
any = False
|
||||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||||
for i in qs:
|
for i in qs:
|
||||||
try:
|
try:
|
||||||
@@ -54,14 +55,19 @@ class InvoiceExporter(BaseExporter):
|
|||||||
i.refresh_from_db()
|
i.refresh_from_db()
|
||||||
i.file.open('rb')
|
i.file.open('rb')
|
||||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||||
|
any = True
|
||||||
i.file.close()
|
i.file.close()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
invoice_pdf_task.apply(args=(i.pk,))
|
invoice_pdf_task.apply(args=(i.pk,))
|
||||||
i.refresh_from_db()
|
i.refresh_from_db()
|
||||||
i.file.open('rb')
|
i.file.open('rb')
|
||||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||||
|
any = True
|
||||||
i.file.close()
|
i.file.close()
|
||||||
|
|
||||||
|
if not any:
|
||||||
|
return None
|
||||||
|
|
||||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||||
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ from django.contrib import messages
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import get_language, ugettext_lazy as _
|
||||||
from django_countries.fields import CountryField
|
from django_countries import countries
|
||||||
|
from django_countries.fields import Country, CountryField
|
||||||
|
|
||||||
from pretix.base.forms.widgets import (
|
from pretix.base.forms.widgets import (
|
||||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||||
@@ -351,6 +352,27 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
self.request = kwargs.pop('request', None)
|
self.request = kwargs.pop('request', None)
|
||||||
self.validate_vat_id = kwargs.pop('validate_vat_id')
|
self.validate_vat_id = kwargs.pop('validate_vat_id')
|
||||||
self.all_optional = kwargs.pop('all_optional', False)
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
if not event.settings.invoice_address_vatid:
|
if not event.settings.invoice_address_vatid:
|
||||||
del self.fields['vat_id']
|
del self.fields['vat_id']
|
||||||
@@ -402,6 +424,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
self.instance.name_parts = data.get('name_parts')
|
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:
|
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||||
pass
|
pass
|
||||||
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
||||||
|
|||||||
18
src/pretix/base/migrations/0121_order_email_known_to_work.py
Normal file
18
src/pretix/base/migrations/0121_order_email_known_to_work.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
src/pretix/base/migrations/0122_orderposition_web_secret.py
Normal file
20
src/pretix/base/migrations/0122_orderposition_web_secret.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -12,6 +12,7 @@ from django.utils.crypto import get_random_string
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.helpers.urls import build_absolute_uri
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
@@ -283,6 +284,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
def get_events_with_any_permission(self, request=None):
|
def get_events_with_any_permission(self, request=None):
|
||||||
"""
|
"""
|
||||||
Returns a queryset of events the user has any permissions to.
|
Returns a queryset of events the user has any permissions to.
|
||||||
@@ -300,6 +302,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
def get_events_with_permission(self, permission, request=None):
|
def get_events_with_permission(self, permission, request=None):
|
||||||
"""
|
"""
|
||||||
Returns a queryset of events the user has a specific permissions to.
|
Returns a queryset of events the user has a specific permissions to.
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ from django.db import models
|
|||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.signals import post_delete
|
from django.db.models.signals import post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from pretix.helpers.json import CustomJSONEncoder
|
from pretix.helpers.json import CustomJSONEncoder
|
||||||
|
|
||||||
@@ -113,6 +115,40 @@ class LoggedModel(models.Model, LoggingMixin):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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):
|
def all_logentries(self):
|
||||||
"""
|
"""
|
||||||
Returns all log entries that are attached to this object.
|
Returns all log entries that are attached to this object.
|
||||||
@@ -122,7 +158,7 @@ class LoggedModel(models.Model, LoggingMixin):
|
|||||||
from .log import LogEntry
|
from .log import LogEntry
|
||||||
|
|
||||||
return LogEntry.objects.filter(
|
return LogEntry.objects.filter(
|
||||||
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
|
content_type=self.logs_content_type, object_id=self.pk
|
||||||
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')
|
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
|
|||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ class CheckinList(LoggedModel):
|
|||||||
'order have not been paid. This only works with pretixdesk '
|
'order have not been paid. This only works with pretixdesk '
|
||||||
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
|
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('subevent__date_from', 'name')
|
ordering = ('subevent__date_from', 'name')
|
||||||
|
|
||||||
@@ -167,6 +170,8 @@ class Checkin(models.Model):
|
|||||||
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
|
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='position__order__event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (('list', 'position'),)
|
unique_together = (('list', 'position'),)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.db import models
|
|||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ class Device(LoggedModel):
|
|||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (('organizer', 'device_id'),)
|
unique_together = (('organizer', 'device_id'),)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from django.utils.crypto import get_random_string
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django_scopes import ScopedManager
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
@@ -336,6 +337,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Event")
|
verbose_name = _("Event")
|
||||||
verbose_name_plural = _("Events")
|
verbose_name_plural = _("Events")
|
||||||
@@ -445,6 +448,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
self.plugins = other.plugins
|
self.plugins = other.plugins
|
||||||
self.is_public = other.is_public
|
self.is_public = other.is_public
|
||||||
|
self.testmode = other.testmode
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
tax_map = {}
|
tax_map = {}
|
||||||
@@ -874,6 +878,8 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
items = models.ManyToManyField('Item', through='SubEventItem')
|
items = models.ManyToManyField('Item', through='SubEventItem')
|
||||||
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
|
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Date in event series")
|
verbose_name = _("Date in event series")
|
||||||
verbose_name_plural = _("Dates in event series")
|
verbose_name_plural = _("Dates in event series")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from django.utils.crypto import get_random_string
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import pgettext
|
from django.utils.translation import pgettext
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
|
|
||||||
def invoice_filename(instance, filename: str) -> str:
|
def invoice_filename(instance, filename: str) -> str:
|
||||||
@@ -107,6 +108,8 @@ class Invoice(models.Model):
|
|||||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
||||||
internal_reference = models.TextField(blank=True)
|
internal_reference = models.TextField(blank=True)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_numeric_invoice_number(number):
|
def _to_numeric_invoice_number(number):
|
||||||
return '{:05d}'.format(int(number))
|
return '{:05d}'.format(int(number))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.timezone import is_naive, make_aware, now
|
from django.utils.timezone import is_naive, make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
|
from django_scopes import ScopedManager
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
|
||||||
from pretix.base.models import fields
|
from pretix.base.models import fields
|
||||||
@@ -155,8 +156,7 @@ class SubEventItemVariation(models.Model):
|
|||||||
self.subevent.event.cache.clear()
|
self.subevent.event.cache.clear()
|
||||||
|
|
||||||
|
|
||||||
class ItemQuerySet(models.QuerySet):
|
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
|
||||||
q = (
|
q = (
|
||||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||||
# in models/event.py: EventMixin.annotated()
|
# in models/event.py: EventMixin.annotated()
|
||||||
@@ -167,7 +167,7 @@ class ItemQuerySet(models.QuerySet):
|
|||||||
)
|
)
|
||||||
if not allow_addons:
|
if not allow_addons:
|
||||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||||
qs = self.filter(q)
|
qs = qs.filter(q)
|
||||||
|
|
||||||
vouchq = Q(hide_without_voucher=False)
|
vouchq = Q(hide_without_voucher=False)
|
||||||
if voucher:
|
if voucher:
|
||||||
@@ -179,6 +179,20 @@ class ItemQuerySet(models.QuerySet):
|
|||||||
return qs.filter(vouchq)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class Item(LoggedModel):
|
class Item(LoggedModel):
|
||||||
"""
|
"""
|
||||||
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
|
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
|
||||||
@@ -226,7 +240,7 @@ class Item(LoggedModel):
|
|||||||
:type sales_channels: bool
|
:type sales_channels: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
objects = ItemQuerySet.as_manager()
|
objects = ItemQuerySetManager()
|
||||||
|
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
Event,
|
Event,
|
||||||
@@ -591,6 +605,8 @@ class ItemVariation(models.Model):
|
|||||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='item__event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Product variation")
|
verbose_name = _("Product variation")
|
||||||
verbose_name_plural = _("Product variations")
|
verbose_name_plural = _("Product variations")
|
||||||
@@ -985,6 +1001,8 @@ class Question(LoggedModel):
|
|||||||
)
|
)
|
||||||
dependency_value = models.TextField(null=True, blank=True)
|
dependency_value = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Question")
|
verbose_name = _("Question")
|
||||||
verbose_name_plural = _("Questions")
|
verbose_name_plural = _("Questions")
|
||||||
@@ -1234,6 +1252,8 @@ class Quota(LoggedModel):
|
|||||||
cached_availability_paid_orders = models.PositiveIntegerField(null=True, blank=True)
|
cached_availability_paid_orders = models.PositiveIntegerField(null=True, blank=True)
|
||||||
cached_availability_time = models.DateTimeField(null=True, blank=True)
|
cached_availability_time = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Quota")
|
verbose_name = _("Quota")
|
||||||
verbose_name_plural = _("Quotas")
|
verbose_name_plural = _("Quotas")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -14,7 +15,7 @@ from django.db import models, transaction
|
|||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
from django.db.models.signals import post_delete
|
from django.db.models.signals import post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -25,6 +26,7 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
from django_countries.fields import Country, CountryField
|
from django_countries.fields import Country, CountryField
|
||||||
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
from jsonfallback.fields import FallbackJSONField
|
from jsonfallback.fields import FallbackJSONField
|
||||||
|
|
||||||
@@ -180,6 +182,12 @@ class Order(LockModel, LoggedModel):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
sales_channel = models.CharField(max_length=190, default="web")
|
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:
|
class Meta:
|
||||||
verbose_name = _("Order")
|
verbose_name = _("Order")
|
||||||
@@ -190,6 +198,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
return self.full_code
|
return self.full_code
|
||||||
|
|
||||||
def gracefully_delete(self, user=None, auth=None):
|
def gracefully_delete(self, user=None, auth=None):
|
||||||
|
from . import Voucher
|
||||||
|
|
||||||
if not self.testmode:
|
if not self.testmode:
|
||||||
raise TypeError("Only test mode orders can be deleted.")
|
raise TypeError("Only test mode orders can be deleted.")
|
||||||
self.event.log_action(
|
self.event.log_action(
|
||||||
@@ -198,6 +208,12 @@ class Order(LockModel, LoggedModel):
|
|||||||
'code': self.code,
|
'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, addon_to__isnull=False).delete()
|
||||||
OrderPosition.all.filter(order=self).delete()
|
OrderPosition.all.filter(order=self).delete()
|
||||||
OrderFee.all.filter(order=self).delete()
|
OrderFee.all.filter(order=self).delete()
|
||||||
@@ -206,6 +222,9 @@ class Order(LockModel, LoggedModel):
|
|||||||
self.event.cache.delete('complain_testmode_orders')
|
self.event.cache.delete('complain_testmode_orders')
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
|
def email_confirm_hash(self):
|
||||||
|
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fees(self):
|
def fees(self):
|
||||||
"""
|
"""
|
||||||
@@ -215,6 +234,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
return self.all_fees(manager='objects')
|
return self.all_fees(manager='objects')
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
@scopes_disabled()
|
||||||
def count_positions(self):
|
def count_positions(self):
|
||||||
if hasattr(self, 'pcnt'):
|
if hasattr(self, 'pcnt'):
|
||||||
return self.pcnt or 0
|
return self.pcnt or 0
|
||||||
@@ -238,6 +258,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@scopes_disabled()
|
||||||
def payment_refund_sum(self):
|
def payment_refund_sum(self):
|
||||||
payment_sum = self.payments.filter(
|
payment_sum = self.payments.filter(
|
||||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED)
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED)
|
||||||
@@ -249,6 +270,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
return payment_sum - refund_sum
|
return payment_sum - refund_sum
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@scopes_disabled()
|
||||||
def pending_sum(self):
|
def pending_sum(self):
|
||||||
total = self.total
|
total = self.total
|
||||||
if self.status == Order.STATUS_CANCELED:
|
if self.status == Order.STATUS_CANCELED:
|
||||||
@@ -423,6 +445,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
return round_decimal(fee, self.event.currency)
|
return round_decimal(fee, self.event.currency)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@scopes_disabled()
|
||||||
def user_cancel_allowed(self) -> bool:
|
def user_cancel_allowed(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns whether or not this order can be canceled by the user.
|
Returns whether or not this order can be canceled by the user.
|
||||||
@@ -665,7 +688,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
|
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
|
||||||
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
|
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,
|
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
|
||||||
auth=None, attach_tickets=False):
|
auth=None, attach_tickets=False, position: 'OrderPosition'=None):
|
||||||
"""
|
"""
|
||||||
Sends an email to the user that placed this order. Basically, this method does two things:
|
Sends an email to the user that placed this order. Basically, this method does two things:
|
||||||
|
|
||||||
@@ -682,6 +705,9 @@ class Order(LockModel, LoggedModel):
|
|||||||
:param headers: Dictionary with additional mail headers
|
:param headers: Dictionary with additional mail headers
|
||||||
:param sender: Custom email sender.
|
:param sender: Custom email sender.
|
||||||
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
|
: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
|
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||||
|
|
||||||
@@ -693,12 +719,16 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
with language(self.locale):
|
with language(self.locale):
|
||||||
recipient = self.email
|
recipient = self.email
|
||||||
|
if position and position.attendee_email:
|
||||||
|
recipient = position.attendee_email
|
||||||
|
|
||||||
try:
|
try:
|
||||||
email_content = render_mail(template, context)
|
email_content = render_mail(template, context)
|
||||||
mail(
|
mail(
|
||||||
recipient, subject, template, context,
|
recipient, subject, template, context,
|
||||||
self.event, self.locale, self, headers, sender,
|
self.event, self.locale, self, headers=headers, sender=sender,
|
||||||
invoices=invoices, attach_tickets=attach_tickets
|
invoices=invoices, attach_tickets=attach_tickets,
|
||||||
|
position=position
|
||||||
)
|
)
|
||||||
except SendMailException:
|
except SendMailException:
|
||||||
raise
|
raise
|
||||||
@@ -710,6 +740,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
data={
|
data={
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
'message': email_content,
|
'message': email_content,
|
||||||
|
'position': position.positionid if position else None,
|
||||||
'recipient': recipient,
|
'recipient': recipient,
|
||||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||||
'attach_tickets': attach_tickets,
|
'attach_tickets': attach_tickets,
|
||||||
@@ -729,9 +760,10 @@ class Order(LockModel, LoggedModel):
|
|||||||
email_template = self.event.settings.mail_text_resend_link
|
email_template = self.event.settings.mail_text_resend_link
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': self.event.name,
|
'event': self.event.name,
|
||||||
'url': build_absolute_uri(self.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(self.event, 'presale:event.order.open', kwargs={
|
||||||
'order': self.code,
|
'order': self.code,
|
||||||
'secret': self.secret
|
'secret': self.secret,
|
||||||
|
'hash': self.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'invoice_company': invoice_company,
|
||||||
@@ -797,6 +829,8 @@ class QuestionAnswer(models.Model):
|
|||||||
max_length=255
|
max_length=255
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='question__event__organizer')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend_file_url(self):
|
def backend_file_url(self):
|
||||||
if self.file:
|
if self.file:
|
||||||
@@ -961,6 +995,10 @@ class AbstractPosition(models.Model):
|
|||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@meta_info_data.setter
|
||||||
|
def meta_info_data(self, d):
|
||||||
|
self.meta_info = json.dumps(d)
|
||||||
|
|
||||||
def cache_answers(self, all=True):
|
def cache_answers(self, all=True):
|
||||||
"""
|
"""
|
||||||
Creates two properties on the object.
|
Creates two properties on the object.
|
||||||
@@ -1116,6 +1154,8 @@ class OrderPayment(models.Model):
|
|||||||
)
|
)
|
||||||
migrated = models.BooleanField(default=False)
|
migrated = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='order__event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('local_id',)
|
ordering = ('local_id',)
|
||||||
|
|
||||||
@@ -1163,7 +1203,8 @@ class OrderPayment(models.Model):
|
|||||||
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
|
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
|
||||||
order_paid.send(self.order.event, order=self.order)
|
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):
|
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):
|
||||||
"""
|
"""
|
||||||
Marks the payment as complete. If possible, this also marks the order as paid if no further
|
Marks the payment as complete. If possible, this also marks the order as paid if no further
|
||||||
payment is required
|
payment is required
|
||||||
@@ -1184,8 +1225,6 @@ class OrderPayment(models.Model):
|
|||||||
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
|
: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.invoices import generate_invoice, invoice_qualified
|
||||||
from pretix.base.services.mail import SendMailException
|
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
|
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
|
||||||
@@ -1194,7 +1233,7 @@ class OrderPayment(models.Model):
|
|||||||
return
|
return
|
||||||
|
|
||||||
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
|
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
|
||||||
locked_instance.payment_date = now()
|
locked_instance.payment_date = payment_date or now()
|
||||||
locked_instance.info = self.info # required for backwards compatibility
|
locked_instance.info = self.info # required for backwards compatibility
|
||||||
locked_instance.save(update_fields=['state', 'payment_date', 'info'])
|
locked_instance.save(update_fields=['state', 'payment_date', 'info'])
|
||||||
|
|
||||||
@@ -1249,6 +1288,47 @@ class OrderPayment(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if send_mail:
|
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):
|
with language(self.order.locale):
|
||||||
try:
|
try:
|
||||||
invoice_name = self.order.invoice_address.name
|
invoice_name = self.order.invoice_address.name
|
||||||
@@ -1259,9 +1339,10 @@ class OrderPayment(models.Model):
|
|||||||
email_template = self.order.event.settings.mail_text_order_paid
|
email_template = self.order.event.settings.mail_text_order_paid
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': self.order.event.name,
|
'event': self.order.event.name,
|
||||||
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': self.order.code,
|
'order': self.order.code,
|
||||||
'secret': self.order.secret
|
'secret': self.order.secret,
|
||||||
|
'hash': self.order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
|
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
@@ -1431,6 +1512,8 @@ class OrderRefund(models.Model):
|
|||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='order__event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('local_id',)
|
ordering = ('local_id',)
|
||||||
|
|
||||||
@@ -1492,7 +1575,7 @@ class OrderRefund(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ActivePositionManager(models.Manager):
|
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(canceled=False)
|
return super().get_queryset().filter(canceled=False)
|
||||||
|
|
||||||
@@ -1569,7 +1652,7 @@ class OrderFee(models.Model):
|
|||||||
)
|
)
|
||||||
canceled = models.BooleanField(default=False)
|
canceled = models.BooleanField(default=False)
|
||||||
|
|
||||||
all = models.Manager()
|
all = ScopedManager(organizer='order__event__organizer')
|
||||||
objects = ActivePositionManager()
|
objects = ActivePositionManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1666,6 +1749,7 @@ class OrderPosition(AbstractPosition):
|
|||||||
verbose_name=_('Tax value')
|
verbose_name=_('Tax value')
|
||||||
)
|
)
|
||||||
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
|
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(
|
pseudonymization_id = models.CharField(
|
||||||
max_length=16,
|
max_length=16,
|
||||||
unique=True,
|
unique=True,
|
||||||
@@ -1673,7 +1757,7 @@ class OrderPosition(AbstractPosition):
|
|||||||
)
|
)
|
||||||
canceled = models.BooleanField(default=False)
|
canceled = models.BooleanField(default=False)
|
||||||
|
|
||||||
all = models.Manager()
|
all = ScopedManager(organizer='order__event__organizer')
|
||||||
objects = ActivePositionManager()
|
objects = ActivePositionManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -1789,6 +1873,60 @@ class OrderPosition(AbstractPosition):
|
|||||||
def event(self):
|
def event(self):
|
||||||
return self.order.event
|
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):
|
class CartPosition(AbstractPosition):
|
||||||
"""
|
"""
|
||||||
@@ -1826,6 +1964,8 @@ class CartPosition(AbstractPosition):
|
|||||||
)
|
)
|
||||||
is_bundled = models.BooleanField(default=False)
|
is_bundled = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Cart position")
|
verbose_name = _("Cart position")
|
||||||
verbose_name_plural = _("Cart positions")
|
verbose_name_plural = _("Cart positions")
|
||||||
@@ -1875,6 +2015,8 @@ class InvoiceAddress(models.Model):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='order__event__organizer')
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
if self.order:
|
if self.order:
|
||||||
self.order.touch()
|
self.order.touch()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django.db.models import Q
|
|||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from ..decimal import round_decimal
|
from ..decimal import round_decimal
|
||||||
from .base import LoggedModel
|
from .base import LoggedModel
|
||||||
@@ -173,6 +174,8 @@ class Voucher(LoggedModel):
|
|||||||
"convenience.")
|
"convenience.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Voucher")
|
verbose_name = _("Voucher")
|
||||||
verbose_name_plural = _("Vouchers")
|
verbose_name_plural = _("Vouchers")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
@@ -67,6 +68,8 @@ class WaitingListEntry(LoggedModel):
|
|||||||
)
|
)
|
||||||
priority = models.IntegerField(default=0)
|
priority = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Waiting list entry")
|
verbose_name = _("Waiting list entry")
|
||||||
verbose_name_plural = _("Waiting list entries")
|
verbose_name_plural = _("Waiting list entries")
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class RelativeDateWrapper:
|
|||||||
else:
|
else:
|
||||||
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
base_date = getattr(event, self.data.base_date_name) or event.date_from
|
||||||
|
|
||||||
oldoffset = base_date.utcoffset()
|
oldoffset = base_date.astimezone(tz).utcoffset()
|
||||||
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
|
new_date = base_date.astimezone(tz) - datetime.timedelta(days=self.data.days_before)
|
||||||
if self.data.time:
|
if self.data.time:
|
||||||
new_date = new_date.replace(
|
new_date = new_date.replace(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.db.models import Q
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext as _
|
from django.utils.translation import pgettext_lazy, ugettext as _
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -23,7 +24,7 @@ from pretix.base.reldate import RelativeDateWrapper
|
|||||||
from pretix.base.services.checkin import _save_answers
|
from pretix.base.services.checkin import _save_answers
|
||||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.base.services.tasks import ProfiledTask
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
@@ -634,7 +635,7 @@ class CartManager:
|
|||||||
Q(voucher=voucher) & Q(event=self.event) &
|
Q(voucher=voucher) & Q(event=self.event) &
|
||||||
Q(expires__gte=self.now_dt)
|
Q(expires__gte=self.now_dt)
|
||||||
).exclude(pk__in=[
|
).exclude(pk__in=[
|
||||||
op.position.voucher_id for op in self._operations if isinstance(op, self.ExtendOperation)
|
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
|
||||||
])
|
])
|
||||||
cart_count = redeemed_in_carts.count()
|
cart_count = redeemed_in_carts.count()
|
||||||
v_avail = voucher.max_usages - voucher.redeemed - cart_count
|
v_avail = voucher.max_usages - voucher.redeemed - cart_count
|
||||||
@@ -902,7 +903,7 @@ def get_fees(event, request, total, invoice_address, provider):
|
|||||||
return fees
|
return fees
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, 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',
|
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:
|
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
|
||||||
"""
|
"""
|
||||||
@@ -913,11 +914,10 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
|||||||
:raises CartError: On any error that occured
|
:raises CartError: On any error that occured
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale):
|
||||||
event = Event.objects.get(id=event)
|
|
||||||
|
|
||||||
ia = False
|
ia = False
|
||||||
if invoice_address:
|
if invoice_address:
|
||||||
try:
|
try:
|
||||||
|
with scopes_disabled():
|
||||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
@@ -934,8 +934,8 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
|||||||
raise CartError(error_messages['busy'])
|
raise CartError(error_messages['busy'])
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, 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:
|
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en') -> None:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
@@ -943,7 +943,6 @@ def remove_cart_position(self, event: int, position: int, cart_id: str=None, loc
|
|||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale):
|
||||||
event = Event.objects.get(id=event)
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id)
|
cm = CartManager(event=event, cart_id=cart_id)
|
||||||
@@ -955,15 +954,14 @@ def remove_cart_position(self, event: int, position: int, cart_id: str=None, loc
|
|||||||
raise CartError(error_messages['busy'])
|
raise CartError(error_messages['busy'])
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
|
def clear_cart(self, event: Event, cart_id: str=None, locale='en') -> None:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale):
|
||||||
event = Event.objects.get(id=event)
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id)
|
cm = CartManager(event=event, cart_id=cart_id)
|
||||||
@@ -975,8 +973,8 @@ def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
|
|||||||
raise CartError(error_messages['busy'])
|
raise CartError(error_messages['busy'])
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, 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',
|
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
||||||
invoice_address: int=None, sales_channel='web') -> None:
|
invoice_address: int=None, sales_channel='web') -> None:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
@@ -985,11 +983,10 @@ def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, loc
|
|||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale):
|
||||||
event = Event.objects.get(id=event)
|
|
||||||
|
|
||||||
ia = False
|
ia = False
|
||||||
if invoice_address:
|
if invoice_address:
|
||||||
try:
|
try:
|
||||||
|
with scopes_disabled():
|
||||||
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
ia = InvoiceAddress.objects.get(pk=invoice_address)
|
||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import CachedCombinedTicket, CachedTicket
|
from pretix.base.models import CachedCombinedTicket, CachedTicket
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ from ..signals import periodic_task
|
|||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def clean_cart_positions(sender, **kwargs):
|
def clean_cart_positions(sender, **kwargs):
|
||||||
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=False):
|
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=False):
|
||||||
cp.delete()
|
cp.delete()
|
||||||
@@ -20,12 +22,14 @@ def clean_cart_positions(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def clean_cached_files(sender, **kwargs):
|
def clean_cached_files(sender, **kwargs):
|
||||||
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()):
|
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()):
|
||||||
cf.delete()
|
cf.delete()
|
||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def clean_cached_tickets(sender, **kwargs):
|
def clean_cached_tickets(sender, **kwargs):
|
||||||
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
|
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
|
||||||
cf.delete()
|
cf.delete()
|
||||||
|
|||||||
@@ -2,24 +2,33 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.timezone import override
|
from django.utils.timezone import override
|
||||||
|
from django.utils.translation import ugettext
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import LazyLocaleException, language
|
||||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||||
from pretix.base.services.tasks import ProfiledTask
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
from pretix.base.signals import register_data_exporters
|
from pretix.base.signals import register_data_exporters
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask)
|
class ExportError(LazyLocaleException):
|
||||||
def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
pass
|
||||||
event = Event.objects.get(id=event)
|
|
||||||
|
|
||||||
|
@app.task(base=ProfiledEventTask, throws=(ExportError,))
|
||||||
|
def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
||||||
file = CachedFile.objects.get(id=fileid)
|
file = CachedFile.objects.get(id=fileid)
|
||||||
with language(event.settings.locale), override(event.settings.timezone):
|
with language(event.settings.locale), override(event.settings.timezone):
|
||||||
responses = register_data_exporters.send(event)
|
responses = register_data_exporters.send(event)
|
||||||
for receiver, response in responses:
|
for receiver, response in responses:
|
||||||
ex = response(event)
|
ex = response(event)
|
||||||
if ex.identifier == provider:
|
if ex.identifier == provider:
|
||||||
file.filename, file.type, data = ex.render(form_data)
|
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.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
||||||
file.save()
|
file.save()
|
||||||
return file.pk
|
return file.pk
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from django.utils import timezone
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext, ugettext as _
|
from django.utils.translation import pgettext, ugettext as _
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
@@ -244,7 +245,9 @@ def generate_invoice(order: Order, trigger_pdf=True):
|
|||||||
|
|
||||||
@app.task(base=TransactionAwareTask)
|
@app.task(base=TransactionAwareTask)
|
||||||
def invoice_pdf_task(invoice: int):
|
def invoice_pdf_task(invoice: int):
|
||||||
|
with scopes_disabled():
|
||||||
i = Invoice.objects.get(pk=invoice)
|
i = Invoice.objects.get(pk=invoice)
|
||||||
|
with scope(organizer=i.order.event.organizer):
|
||||||
if i.shredded:
|
if i.shredded:
|
||||||
return None
|
return None
|
||||||
if i.file:
|
if i.file:
|
||||||
@@ -257,7 +260,8 @@ def invoice_pdf_task(invoice: int):
|
|||||||
|
|
||||||
|
|
||||||
def invoice_qualified(order: Order):
|
def invoice_qualified(order: Order):
|
||||||
if order.total == Decimal('0.00') or order.require_approval:
|
if order.total == Decimal('0.00') or order.require_approval or \
|
||||||
|
order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import warnings
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
@@ -9,11 +10,14 @@ from django.conf import settings
|
|||||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.base.email import ClassicMailRenderer
|
from pretix.base.email import ClassicMailRenderer
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
|
from pretix.base.models import (
|
||||||
|
Event, Invoice, InvoiceAddress, Order, OrderPosition,
|
||||||
|
)
|
||||||
from pretix.base.services.invoices import invoice_pdf_task
|
from pretix.base.services.invoices import invoice_pdf_task
|
||||||
from pretix.base.services.tasks import TransactionAwareTask
|
from pretix.base.services.tasks import TransactionAwareTask
|
||||||
from pretix.base.services.tickets import get_tickets_for_order
|
from pretix.base.services.tickets import get_tickets_for_order
|
||||||
@@ -38,8 +42,8 @@ class SendMailException(Exception):
|
|||||||
|
|
||||||
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||||
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
|
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
|
||||||
order: Order=None, headers: dict=None, sender: str=None, invoices: list=None,
|
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
|
||||||
attach_tickets=False):
|
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.
|
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
|
||||||
|
|
||||||
@@ -60,6 +64,9 @@ 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
|
: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.
|
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 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
|
:param locale: The locale to be used while evaluating the subject and the template
|
||||||
@@ -100,7 +107,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
subject = str(subject).format_map(context)
|
subject = str(subject).format_map(context)
|
||||||
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
|
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
|
||||||
if event:
|
if event:
|
||||||
sender = formataddr((str(event.name), sender))
|
sender_name = event.settings.mail_from_name or str(event.name)
|
||||||
|
sender = formataddr((sender_name, sender))
|
||||||
else:
|
else:
|
||||||
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
|
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
|
||||||
|
|
||||||
@@ -111,7 +119,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
if event:
|
if event:
|
||||||
renderer = event.get_html_mail_renderer()
|
renderer = event.get_html_mail_renderer()
|
||||||
if event.settings.mail_bcc:
|
if event.settings.mail_bcc:
|
||||||
bcc.append(event.settings.mail_bcc)
|
for bcc_mail in event.settings.mail_bcc.split(','):
|
||||||
|
bcc.append(bcc_mail.strip())
|
||||||
|
|
||||||
if event.settings.mail_from == settings.DEFAULT_FROM_EMAIL and event.settings.contact_mail and not headers.get('Reply-To'):
|
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
|
headers['Reply-To'] = event.settings.contact_mail
|
||||||
@@ -130,9 +139,26 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
body_plain += signature
|
body_plain += signature
|
||||||
body_plain += "\r\n\r\n-- \r\n"
|
body_plain += "\r\n\r\n-- \r\n"
|
||||||
|
|
||||||
if order:
|
if order and order.testmode:
|
||||||
if order.testmode:
|
|
||||||
subject = "[TESTMODE] " + subject
|
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:
|
||||||
body_plain += _(
|
body_plain += _(
|
||||||
"You are receiving this email because you placed an order for {event}."
|
"You are receiving this email because you placed an order for {event}."
|
||||||
).format(event=event.name)
|
).format(event=event.name)
|
||||||
@@ -141,15 +167,23 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
"You can view your order details at the following URL:\n{orderurl}."
|
"You can view your order details at the following URL:\n{orderurl}."
|
||||||
).replace("\n", "\r\n").format(
|
).replace("\n", "\r\n").format(
|
||||||
event=event.name, orderurl=build_absolute_uri(
|
event=event.name, orderurl=build_absolute_uri(
|
||||||
order.event, 'presale:event.order', kwargs={
|
order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
body_plain += "\r\n"
|
body_plain += "\r\n"
|
||||||
|
|
||||||
try:
|
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:
|
except:
|
||||||
logger.exception('Could not render HTML body')
|
logger.exception('Could not render HTML body')
|
||||||
@@ -164,8 +198,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
sender=sender,
|
sender=sender,
|
||||||
event=event.id if event else None,
|
event=event.id if event else None,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
invoices=[i.pk for i in invoices] if invoices else [],
|
invoices=[i.pk for i in invoices] if invoices and not position else [],
|
||||||
order=order.pk if order else None,
|
order=order.pk if order else None,
|
||||||
|
position=position.pk if position else None,
|
||||||
attach_tickets=attach_tickets
|
attach_tickets=attach_tickets
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -180,8 +215,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
|||||||
|
|
||||||
@app.task(base=TransactionAwareTask, bind=True)
|
@app.task(base=TransactionAwareTask, bind=True)
|
||||||
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
|
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
|
||||||
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
|
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
|
||||||
order: int=None, attach_tickets=False) -> bool:
|
invoices: List[int]=None, order: int=None, attach_tickets=False) -> bool:
|
||||||
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
|
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
|
||||||
if html is not None:
|
if html is not None:
|
||||||
email.attach_alternative(html, "text/html")
|
email.attach_alternative(html, "text/html")
|
||||||
@@ -200,11 +235,15 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if event:
|
if event:
|
||||||
|
with scopes_disabled():
|
||||||
event = Event.objects.get(id=event)
|
event = Event.objects.get(id=event)
|
||||||
backend = event.get_mail_backend()
|
backend = event.get_mail_backend()
|
||||||
|
cm = lambda: scope(organizer=event.organizer) # noqa
|
||||||
else:
|
else:
|
||||||
backend = get_connection(fail_silently=False)
|
backend = get_connection(fail_silently=False)
|
||||||
|
cm = lambda: scopes_disabled() # noqa
|
||||||
|
|
||||||
|
with cm():
|
||||||
if event:
|
if event:
|
||||||
if order:
|
if order:
|
||||||
try:
|
try:
|
||||||
@@ -212,10 +251,15 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
except Order.DoesNotExist:
|
except Order.DoesNotExist:
|
||||||
order = None
|
order = None
|
||||||
else:
|
else:
|
||||||
|
if position:
|
||||||
|
try:
|
||||||
|
position = order.positions.get(pk=position)
|
||||||
|
except OrderPosition.DoesNotExist:
|
||||||
|
attach_tickets = False
|
||||||
if attach_tickets:
|
if attach_tickets:
|
||||||
args = []
|
args = []
|
||||||
attach_size = 0
|
attach_size = 0
|
||||||
for name, ct in get_tickets_for_order(order):
|
for name, ct in get_tickets_for_order(order, base_position=position):
|
||||||
content = ct.file.read()
|
content = ct.file.read()
|
||||||
args.append((name, content, ct.type))
|
args.append((name, content, ct.type))
|
||||||
attach_size += len(content)
|
attach_size += len(content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
from inlinestyler.utils import inline_css
|
from inlinestyler.utils import inline_css
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
@@ -12,6 +13,7 @@ from pretix.helpers.urls import build_absolute_uri
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=TransactionAwareTask)
|
@app.task(base=TransactionAwareTask)
|
||||||
|
@scopes_disabled()
|
||||||
def notify(logentry_id: int):
|
def notify(logentry_id: int):
|
||||||
logentry = LogEntry.all.get(id=logentry_id)
|
logentry = LogEntry.all.get(id=logentry_id)
|
||||||
if not logentry.event:
|
if not logentry.event:
|
||||||
@@ -66,6 +68,11 @@ def notify(logentry_id: int):
|
|||||||
@app.task(base=ProfiledTask)
|
@app.task(base=ProfiledTask)
|
||||||
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
|
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
|
||||||
logentry = LogEntry.all.get(id=logentry_id)
|
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)
|
user = User.objects.get(id=user_id)
|
||||||
types = get_all_notification_types(logentry.event)
|
types = get_all_notification_types(logentry.event)
|
||||||
notification_type = types.get(action_type)
|
notification_type = types.get(action_type)
|
||||||
@@ -104,7 +111,11 @@ def send_notification_mail(notification: Notification, user: User):
|
|||||||
|
|
||||||
mail_send_task.apply_async(kwargs={
|
mail_send_task.apply_async(kwargs={
|
||||||
'to': [user.email],
|
'to': [user.email],
|
||||||
'subject': '[{}] {}'.format(settings.PRETIX_INSTANCE_NAME, notification.title),
|
'subject': '[{}] {}: {}'.format(
|
||||||
|
settings.PRETIX_INSTANCE_NAME,
|
||||||
|
notification.event.settings.mail_prefix or notification.event.slug.upper(),
|
||||||
|
notification.title
|
||||||
|
),
|
||||||
'body': body_plain,
|
'body': body_plain,
|
||||||
'html': body_html,
|
'html': body_html,
|
||||||
'sender': settings.MAIL_FROM,
|
'sender': settings.MAIL_FROM,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from django.utils.formats import date_format
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import OAuthApplication
|
from pretix.api.models import OAuthApplication
|
||||||
from pretix.base.i18n import (
|
from pretix.base.i18n import (
|
||||||
@@ -42,11 +43,12 @@ from pretix.base.services.invoices import (
|
|||||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.base.services.tasks import ProfiledTask
|
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||||
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import (
|
from pretix.base.signals import (
|
||||||
allow_ticket_download, order_approved, order_canceled, order_changed,
|
allow_ticket_download, order_approved, order_canceled, order_changed,
|
||||||
order_denied, order_expired, order_fee_calculation, order_placed,
|
order_denied, order_expired, order_fee_calculation, order_placed,
|
||||||
periodic_task,
|
periodic_task, validate_order,
|
||||||
)
|
)
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
@@ -222,9 +224,10 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
|
|||||||
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
||||||
'date': LazyDate(order.expires),
|
'date': LazyDate(order.expires),
|
||||||
'event': order.event.name,
|
'event': order.event.name,
|
||||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'invoice_company': invoice_company,
|
||||||
@@ -282,9 +285,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
|||||||
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
'total_with_currency': LazyCurrencyNumber(order.total, order.event.currency),
|
||||||
'date': LazyDate(order.expires),
|
'date': LazyDate(order.expires),
|
||||||
'event': order.event.name,
|
'event': order.event.name,
|
||||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'comment': comment,
|
'comment': comment,
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
@@ -375,9 +379,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
email_context = {
|
email_context = {
|
||||||
'event': order.event.name,
|
'event': order.event.name,
|
||||||
'code': order.code,
|
'code': order.code,
|
||||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
with language(order.locale):
|
with language(order.locale):
|
||||||
@@ -531,7 +536,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
|
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
|
||||||
positions[i] = cp
|
|
||||||
cp.price = price.gross
|
cp.price = price.gross
|
||||||
cp.includes_tax = bool(price.rate)
|
cp.includes_tax = bool(price.rate)
|
||||||
cp.save()
|
cp.save()
|
||||||
@@ -555,7 +559,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
break
|
break
|
||||||
|
|
||||||
if quota_ok:
|
if quota_ok:
|
||||||
positions[i] = cp
|
|
||||||
cp.expires = now_dt + timedelta(
|
cp.expires = now_dt + timedelta(
|
||||||
minutes=event.settings.get('reservation_time', as_type=int))
|
minutes=event.settings.get('reservation_time', as_type=int))
|
||||||
cp.save()
|
cp.save()
|
||||||
@@ -644,10 +647,77 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
return order, p
|
return order, p
|
||||||
|
|
||||||
|
|
||||||
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
|
||||||
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
|
invoice):
|
||||||
|
try:
|
||||||
|
invoice_name = order.invoice_address.name
|
||||||
|
invoice_company = order.invoice_address.company
|
||||||
|
except InvoiceAddress.DoesNotExist:
|
||||||
|
invoice_name = ""
|
||||||
|
invoice_company = ""
|
||||||
|
|
||||||
event = Event.objects.get(id=event)
|
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],
|
||||||
|
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
|
||||||
if payment_provider:
|
if payment_provider:
|
||||||
pprov = event.get_payment_providers().get(payment_provider)
|
pprov = event.get_payment_providers().get(payment_provider)
|
||||||
if not pprov:
|
if not pprov:
|
||||||
@@ -661,12 +731,16 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
|||||||
addr = None
|
addr = None
|
||||||
if address is not None:
|
if address is not None:
|
||||||
try:
|
try:
|
||||||
|
with scopes_disabled():
|
||||||
addr = InvoiceAddress.objects.get(pk=address)
|
addr = InvoiceAddress.objects.get(pk=address)
|
||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
positions = CartPosition.objects.filter(id__in=position_ids, event=event)
|
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
|
lockfn = NoLockManager
|
||||||
locked = False
|
locked = False
|
||||||
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2))).exists():
|
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2))).exists():
|
||||||
@@ -705,54 +779,32 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
|||||||
if order.require_approval:
|
if order.require_approval:
|
||||||
email_template = event.settings.mail_text_order_placed_require_approval
|
email_template = event.settings.mail_text_order_placed_require_approval
|
||||||
log_entry = 'pretix.event.order.email.order_placed_require_approval'
|
log_entry = 'pretix.event.order.email.order_placed_require_approval'
|
||||||
|
|
||||||
|
email_attendees = False
|
||||||
elif free_order_flow:
|
elif free_order_flow:
|
||||||
email_template = event.settings.mail_text_order_free
|
email_template = event.settings.mail_text_order_free
|
||||||
log_entry = 'pretix.event.order.email.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:
|
else:
|
||||||
email_template = event.settings.mail_text_order_placed
|
email_template = event.settings.mail_text_order_placed
|
||||||
log_entry = 'pretix.event.order.email.order_placed'
|
log_entry = 'pretix.event.order.email.order_placed'
|
||||||
|
|
||||||
try:
|
email_attendees = event.settings.mail_send_order_placed_attendee
|
||||||
invoice_name = order.invoice_address.name
|
email_attendees_template = event.settings.mail_text_order_placed_attendee
|
||||||
invoice_company = order.invoice_address.company
|
|
||||||
except InvoiceAddress.DoesNotExist:
|
|
||||||
invoice_name = ""
|
|
||||||
invoice_company = ""
|
|
||||||
|
|
||||||
if pprov:
|
_order_placed_email(event, order, pprov, email_template, log_entry, invoice)
|
||||||
payment_info = str(pprov.order_pending_mail_render(order))
|
if email_attendees:
|
||||||
else:
|
for p in order.positions.all():
|
||||||
payment_info = None
|
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)
|
||||||
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
|
return order.id
|
||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def expire_orders(sender, **kwargs):
|
def expire_orders(sender, **kwargs):
|
||||||
eventcache = {}
|
eventcache = {}
|
||||||
|
|
||||||
@@ -767,6 +819,7 @@ def expire_orders(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def send_expiry_warnings(sender, **kwargs):
|
def send_expiry_warnings(sender, **kwargs):
|
||||||
eventcache = {}
|
eventcache = {}
|
||||||
today = now().replace(hour=0, minute=0, second=0)
|
today = now().replace(hour=0, minute=0, second=0)
|
||||||
@@ -800,9 +853,10 @@ def send_expiry_warnings(sender, **kwargs):
|
|||||||
email_template = eventsettings.mail_text_order_expire_warning
|
email_template = eventsettings.mail_text_order_expire_warning
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': o.event.name,
|
'event': o.event.name,
|
||||||
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
|
||||||
'order': o.code,
|
'order': o.code,
|
||||||
'secret': o.secret
|
'secret': o.secret,
|
||||||
|
'hash': o.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
|
'expire_date': date_format(o.expires.astimezone(tz), 'SHORT_DATE_FORMAT'),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
@@ -823,6 +877,7 @@ def send_expiry_warnings(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def send_download_reminders(sender, **kwargs):
|
def send_download_reminders(sender, **kwargs):
|
||||||
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
|
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
@@ -851,9 +906,10 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
email_template = e.settings.mail_text_download_reminder
|
email_template = e.settings.mail_text_download_reminder
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': o.event.name,
|
'event': o.event.name,
|
||||||
'url': build_absolute_uri(o.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(o.event, 'presale:event.order.open', kwargs={
|
||||||
'order': o.code,
|
'order': o.code,
|
||||||
'secret': o.secret
|
'secret': o.secret,
|
||||||
|
'hash': o.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
|
email_subject = _('Your ticket is ready for download: %(code)s') % {'code': o.code}
|
||||||
@@ -866,6 +922,31 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
except SendMailException:
|
except SendMailException:
|
||||||
logger.exception('Reminder email could not be sent')
|
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:
|
class OrderChangeManager:
|
||||||
error_messages = {
|
error_messages = {
|
||||||
@@ -1350,9 +1431,10 @@ class OrderChangeManager:
|
|||||||
email_template = order.event.settings.mail_text_order_changed
|
email_template = order.event.settings.mail_text_order_changed
|
||||||
email_context = {
|
email_context = {
|
||||||
'event': order.event.name,
|
'event': order.event.name,
|
||||||
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'invoice_company': invoice_company,
|
||||||
@@ -1418,8 +1500,8 @@ class OrderChangeManager:
|
|||||||
return pprov
|
return pprov
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
def perform_order(self, event: Event, payment_provider: str, positions: List[str],
|
||||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||||
sales_channel: str='web'):
|
sales_channel: str='web'):
|
||||||
with language(locale):
|
with language(locale):
|
||||||
@@ -1434,6 +1516,7 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@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,
|
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):
|
device=None, cancellation_fee=None, try_auto_refund=False):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.db import models
|
from django.conf import settings
|
||||||
from django.db.models import F, Max, OuterRef, Q, Subquery
|
from django.db.models import Max, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import LogEntry, Quota
|
from pretix.base.models import Event, LogEntry
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
from ..signals import periodic_task
|
from ..signals import periodic_task
|
||||||
@@ -17,20 +18,27 @@ def build_all_quota_caches(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
|
@scopes_disabled()
|
||||||
def refresh_quota_caches():
|
def refresh_quota_caches():
|
||||||
last_activity = LogEntry.objects.filter(
|
# Active events
|
||||||
event=OuterRef('event_id'),
|
active = LogEntry.objects.using(settings.DATABASE_REPLICA).filter(
|
||||||
|
datetime__gt=now() - timedelta(days=7)
|
||||||
).order_by().values('event').annotate(
|
).order_by().values('event').annotate(
|
||||||
m=Max('datetime')
|
last_activity=Max('datetime')
|
||||||
).values(
|
|
||||||
'm'
|
|
||||||
)
|
)
|
||||||
quotas = Quota.objects.annotate(
|
for a in active:
|
||||||
last_activity=Subquery(last_activity, output_field=models.DateTimeField())
|
try:
|
||||||
).filter(
|
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__isnull=True) |
|
||||||
Q(cached_availability_time__lt=F('last_activity')) |
|
Q(cached_availability_time__lt=a['last_activity']) |
|
||||||
Q(cached_availability_time__lt=now() - timedelta(hours=2), last_activity__gt=now() - timedelta(days=7))
|
Q(cached_availability_time__lt=now() - timedelta(hours=2))
|
||||||
).select_related('subevent')
|
).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:
|
for q in quotas:
|
||||||
q.availability()
|
q.availability()
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||||
from pretix.base.services.tasks import ProfiledTask
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
from pretix.base.shredder import ShredError
|
from pretix.base.shredder import ShredError
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask)
|
@app.task(base=ProfiledEventTask)
|
||||||
def export(event: str, shredders: List[str]) -> None:
|
def export(event: Event, shredders: List[str]) -> None:
|
||||||
event = Event.objects.get(id=event)
|
|
||||||
known_shredders = event.get_data_shredders()
|
known_shredders = event.get_data_shredders()
|
||||||
|
|
||||||
with NamedTemporaryFile() as rawfile:
|
with NamedTemporaryFile() as rawfile:
|
||||||
@@ -63,9 +62,8 @@ def export(event: str, shredders: List[str]) -> None:
|
|||||||
return cf.pk
|
return cf.pk
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, throws=(ShredError,))
|
@app.task(base=ProfiledEventTask, throws=(ShredError,))
|
||||||
def shred(event: str, fileid: str, confirm_code: str) -> None:
|
def shred(event: Event, fileid: str, confirm_code: str) -> None:
|
||||||
event = Event.objects.get(id=event)
|
|
||||||
known_shredders = event.get_data_shredders()
|
known_shredders = event.get_data_shredders()
|
||||||
try:
|
try:
|
||||||
cf = CachedFile.objects.get(pk=fileid)
|
cf = CachedFile.objects.get(pk=fileid)
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import time
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
|
|
||||||
from pretix.base.metrics import (
|
from pretix.base.metrics import (
|
||||||
pretix_task_duration_seconds, pretix_task_runs_total,
|
pretix_task_duration_seconds, pretix_task_runs_total,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models import Event
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +63,35 @@ class ProfiledTask(app.Task):
|
|||||||
return super().on_success(retval, task_id, args, kwargs)
|
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):
|
class TransactionAwareTask(ProfiledTask):
|
||||||
"""
|
"""
|
||||||
Task class which is aware of django db transactions and only executes tasks
|
Task class which is aware of django db transactions and only executes tasks
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import os
|
|||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
|
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
|
||||||
OrderPosition,
|
OrderPosition,
|
||||||
)
|
)
|
||||||
from pretix.base.services.tasks import ProfiledTask
|
from pretix.base.services.tasks import EventTask, ProfiledTask
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
|
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
@@ -57,6 +58,7 @@ def generate_order(order: int, provider: str):
|
|||||||
|
|
||||||
@app.task(base=ProfiledTask)
|
@app.task(base=ProfiledTask)
|
||||||
def generate(model: str, pk: int, provider: str):
|
def generate(model: str, pk: int, provider: str):
|
||||||
|
with scopes_disabled():
|
||||||
if model == 'order':
|
if model == 'order':
|
||||||
return generate_order(pk, provider)
|
return generate_order(pk, provider)
|
||||||
elif model == 'orderposition':
|
elif model == 'orderposition':
|
||||||
@@ -96,7 +98,7 @@ def preview(event: int, provider: str):
|
|||||||
return prov.generate(p)
|
return prov.generate(p)
|
||||||
|
|
||||||
|
|
||||||
def get_tickets_for_order(order):
|
def get_tickets_for_order(order, base_position=None):
|
||||||
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
|
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
|
||||||
if not can_download:
|
if not can_download:
|
||||||
return []
|
return []
|
||||||
@@ -111,13 +113,20 @@ def get_tickets_for_order(order):
|
|||||||
|
|
||||||
tickets = []
|
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:
|
for p in providers:
|
||||||
if not p.is_enabled:
|
if not p.is_enabled:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if p.multi_download_enabled:
|
if p.multi_download_enabled and not base_position:
|
||||||
try:
|
try:
|
||||||
if len(list(order.positions_with_tickets)) == 0:
|
if len(positions) == 0:
|
||||||
continue
|
continue
|
||||||
ct = CachedCombinedTicket.objects.filter(
|
ct = CachedCombinedTicket.objects.filter(
|
||||||
order=order, provider=p.identifier, file__isnull=False
|
order=order, provider=p.identifier, file__isnull=False
|
||||||
@@ -136,7 +145,7 @@ def get_tickets_for_order(order):
|
|||||||
except:
|
except:
|
||||||
logger.exception('Failed to generate ticket.')
|
logger.exception('Failed to generate ticket.')
|
||||||
else:
|
else:
|
||||||
for pos in order.positions_with_tickets:
|
for pos in positions:
|
||||||
try:
|
try:
|
||||||
ct = CachedTicket.objects.filter(
|
ct = CachedTicket.objects.filter(
|
||||||
order_position=pos, provider=p.identifier, file__isnull=False
|
order_position=pos, provider=p.identifier, file__isnull=False
|
||||||
@@ -145,7 +154,7 @@ def get_tickets_for_order(order):
|
|||||||
retval = generate_orderposition(pos.pk, p.identifier)
|
retval = generate_orderposition(pos.pk, p.identifier)
|
||||||
if not retval:
|
if not retval:
|
||||||
continue
|
continue
|
||||||
ct = CachedCombinedTicket.objects.get(pk=retval)
|
ct = CachedTicket.objects.get(pk=retval)
|
||||||
tickets.append((
|
tickets.append((
|
||||||
"{}-{}-{}-{}{}".format(
|
"{}-{}-{}-{}{}".format(
|
||||||
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,
|
order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension,
|
||||||
@@ -158,9 +167,8 @@ def get_tickets_for_order(order):
|
|||||||
return tickets
|
return tickets
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask)
|
@app.task(base=EventTask)
|
||||||
def invalidate_cache(event: int, item: int=None, provider: str=None, order: int=None, **kwargs):
|
def invalidate_cache(event: Event, 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)
|
qs = CachedTicket.objects.filter(order_position__order__event=event)
|
||||||
qsc = CachedCombinedTicket.objects.filter(order__event=event)
|
qsc = CachedCombinedTicket.objects.filter(order__event=event)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import requests
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _, ugettext_noop
|
from django.utils.translation import ugettext_lazy as _, ugettext_noop
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix import __version__
|
from pretix import __version__
|
||||||
@@ -29,6 +30,7 @@ def run_update_check(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
|
@scopes_disabled()
|
||||||
def update_check():
|
def update_check():
|
||||||
gs = GlobalSettingsObject()
|
gs = GlobalSettingsObject()
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import Event, User, WaitingListEntry
|
from pretix.base.models import Event, User, WaitingListEntry
|
||||||
from pretix.base.models.waitinglist import WaitingListException
|
from pretix.base.models.waitinglist import WaitingListException
|
||||||
from pretix.base.services.tasks import ProfiledTask
|
from pretix.base.services.tasks import EventTask
|
||||||
from pretix.base.signals import periodic_task
|
from pretix.base.signals import periodic_task
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask)
|
@app.task(base=EventTask)
|
||||||
def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None):
|
def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None):
|
||||||
event = Event.objects.get(id=event_id)
|
|
||||||
if user_id:
|
if user_id:
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
else:
|
else:
|
||||||
@@ -69,6 +69,7 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
|
|||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
@receiver(signal=periodic_task)
|
||||||
|
@scopes_disabled()
|
||||||
def process_waitinglist(sender, **kwargs):
|
def process_waitinglist(sender, **kwargs):
|
||||||
qs = Event.objects.filter(
|
qs = Event.objects.filter(
|
||||||
live=True
|
live=True
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ DEFAULTS = {
|
|||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': str
|
'type': str
|
||||||
},
|
},
|
||||||
|
'invoice_generate_sales_channels': {
|
||||||
|
'default': json.dumps(['web']),
|
||||||
|
'type': list
|
||||||
|
},
|
||||||
'invoice_address_from': {
|
'invoice_address_from': {
|
||||||
'default': '',
|
'default': '',
|
||||||
'type': str
|
'type': str
|
||||||
@@ -297,6 +301,10 @@ DEFAULTS = {
|
|||||||
'default': settings.MAIL_FROM,
|
'default': settings.MAIL_FROM,
|
||||||
'type': str
|
'type': str
|
||||||
},
|
},
|
||||||
|
'mail_from_name': {
|
||||||
|
'default': None,
|
||||||
|
'type': str
|
||||||
|
},
|
||||||
'mail_text_signature': {
|
'mail_text_signature': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': ""
|
'default': ""
|
||||||
@@ -323,6 +331,18 @@ The list is as follows:
|
|||||||
|
|
||||||
{orders}
|
{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,
|
Best regards,
|
||||||
Your {event} team"""))
|
Your {event} team"""))
|
||||||
},
|
},
|
||||||
@@ -339,6 +359,10 @@ You can change your order details and view the status of your order at
|
|||||||
Best regards,
|
Best regards,
|
||||||
Your {event} team"""))
|
Your {event} team"""))
|
||||||
},
|
},
|
||||||
|
'mail_send_order_free_attendee': {
|
||||||
|
'type': bool,
|
||||||
|
'default': 'False'
|
||||||
|
},
|
||||||
'mail_text_order_placed_require_approval': {
|
'mail_text_order_placed_require_approval': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||||
@@ -365,6 +389,22 @@ of {total_with_currency}. Please complete your payment before {date}.
|
|||||||
You can change your order details and view the status of your order at
|
You can change your order details and view the status of your order at
|
||||||
{url}
|
{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,
|
Best regards,
|
||||||
Your {event} team"""))
|
Your {event} team"""))
|
||||||
},
|
},
|
||||||
@@ -391,6 +431,22 @@ we successfully received your payment for {event}. Thank you!
|
|||||||
You can change your order details and view the status of your order at
|
You can change your order details and view the status of your order at
|
||||||
{url}
|
{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,
|
Best regards,
|
||||||
Your {event} team"""))
|
Your {event} team"""))
|
||||||
},
|
},
|
||||||
@@ -492,6 +548,22 @@ Your {event} team"""))
|
|||||||
'type': int,
|
'type': int,
|
||||||
'default': None
|
'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': {
|
'mail_text_download_reminder': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||||
|
|||||||
@@ -240,6 +240,19 @@ subclass of pretix.base.exporter.BaseExporter
|
|||||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
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(
|
validate_cart = EventPluginSignal(
|
||||||
providing_args=["positions"]
|
providing_args=["positions"]
|
||||||
)
|
)
|
||||||
@@ -502,3 +515,12 @@ 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
|
The evaluate member will be called with the order position, order and event as arguments. The event might
|
||||||
also be a subevent, if applicable.
|
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``.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -23,13 +23,23 @@
|
|||||||
<table cellpadding="20"><tr><td>
|
<table cellpadding="20"><tr><td>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div class="content">
|
<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>
|
{% 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 "Event:" %}</strong> {{ event.name }}<br>
|
||||||
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
||||||
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<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 %}">
|
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
|
||||||
{% trans "View order details" %}
|
{% trans "View order details" %}
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<!--[if gte mso 9]>
|
<!--[if gte mso 9]>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ ALLOWED_TAGS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
ALLOWED_ATTRIBUTES = {
|
ALLOWED_ATTRIBUTES = {
|
||||||
'a': ['href', 'title'],
|
'a': ['href', 'title', 'class'],
|
||||||
'abbr': ['title'],
|
'abbr': ['title'],
|
||||||
'acronym': ['title'],
|
'acronym': ['title'],
|
||||||
'table': ['width'],
|
'table': ['width'],
|
||||||
|
|||||||
200
src/pretix/base/timeline.py
Normal file
200
src/pretix/base/timeline.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
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)
|
||||||
@@ -3,6 +3,7 @@ import hmac
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from .. import metrics
|
from .. import metrics
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ def unauthed_response():
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
def serve_metrics(request):
|
def serve_metrics(request):
|
||||||
if not settings.METRICS_ENABLED:
|
if not settings.METRICS_ENABLED:
|
||||||
return unauthed_response()
|
return unauthed_response()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.files.uploadedfile import UploadedFile
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch, QuerySet
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from pretix.base.forms.questions import (
|
from pretix.base.forms.questions import (
|
||||||
@@ -89,19 +89,20 @@ class BaseQuestionsViewMixin:
|
|||||||
elif k == 'attendee_email':
|
elif k == 'attendee_email':
|
||||||
form.pos.attendee_email = v if v != '' else None
|
form.pos.attendee_email = v if v != '' else None
|
||||||
form.pos.save()
|
form.pos.save()
|
||||||
elif k.startswith('question_') and v is not None:
|
elif k.startswith('question_'):
|
||||||
field = form.fields[k]
|
field = form.fields[k]
|
||||||
if hasattr(field, 'answer'):
|
if hasattr(field, 'answer'):
|
||||||
# We already have a cached answer object, so we don't
|
# We already have a cached answer object, so we don't
|
||||||
# have to create a new one
|
# have to create a new one
|
||||||
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False):
|
if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \
|
||||||
|
or (isinstance(v, QuerySet) and not v.exists()):
|
||||||
if field.answer.file:
|
if field.answer.file:
|
||||||
field.answer.file.delete()
|
field.answer.file.delete()
|
||||||
field.answer.delete()
|
field.answer.delete()
|
||||||
else:
|
else:
|
||||||
self._save_to_answer(field, field.answer, v)
|
self._save_to_answer(field, field.answer, v)
|
||||||
field.answer.save()
|
field.answer.save()
|
||||||
elif v != '':
|
elif v != '' and v is not None:
|
||||||
answer = QuestionAnswer(
|
answer = QuestionAnswer(
|
||||||
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None),
|
cartposition=(form.pos if isinstance(form.pos, CartPosition) else None),
|
||||||
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None),
|
orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.conf import settings
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.urls import Resolver404, get_script_prefix, resolve
|
from django.urls import Resolver404, get_script_prefix, resolve
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
|
from django_scopes import scope
|
||||||
|
|
||||||
from pretix.base.models.auth import StaffSession
|
from pretix.base.models.auth import StaffSession
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
@@ -53,6 +54,7 @@ def contextprocessor(request):
|
|||||||
ctx['has_domain'] = request.event.organizer.domains.exists()
|
ctx['has_domain'] = request.event.organizer.domains.exists()
|
||||||
|
|
||||||
if not request.event.testmode:
|
if not request.event.testmode:
|
||||||
|
with scope(organizer=request.organizer):
|
||||||
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
|
complain_testmode_orders = request.event.cache.get('complain_testmode_orders')
|
||||||
if complain_testmode_orders is None:
|
if complain_testmode_orders is None:
|
||||||
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
|
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
|
||||||
|
|||||||
@@ -200,3 +200,7 @@ class SplitDateTimeField(forms.SplitDateTimeField):
|
|||||||
result = datetime.datetime.combine(*data_list)
|
result = datetime.datetime.combine(*data_list)
|
||||||
return from_current_timezone(result)
|
return from_current_timezone(result)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class FontSelect(forms.RadioSelect):
|
||||||
|
option_template_name = 'pretixcontrol/font_option.html'
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import pgettext_lazy
|
from django.utils.translation import pgettext_lazy
|
||||||
|
from django_scopes.forms import (
|
||||||
|
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||||
|
)
|
||||||
|
|
||||||
from pretix.base.models.checkin import CheckinList
|
from pretix.base.models.checkin import CheckinList
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
@@ -44,3 +47,7 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'data-inverse-dependency': '<[name$=all_products]'
|
'data-inverse-dependency': '<[name$=all_products]'
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'limit_products': SafeModelMultipleChoiceField,
|
||||||
|
'subevent': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator, validate_email
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import get_current_timezone_name
|
from django.utils.timezone import get_current_timezone_name
|
||||||
@@ -18,15 +19,17 @@ from i18nfield.forms import (
|
|||||||
)
|
)
|
||||||
from pytz import common_timezones, timezone
|
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.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||||
from pretix.base.models import Event, Organizer, TaxRule
|
from pretix.base.models import Event, Organizer, TaxRule
|
||||||
from pretix.base.models.event import EventMetaValue, SubEvent
|
from pretix.base.models.event import EventMetaValue, SubEvent
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ExtFileField, MultipleLanguagesWidget, SingleLanguageWidget, SlugWidget,
|
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
|
||||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
|
||||||
)
|
)
|
||||||
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.plugins.banktransfer.payment import BankTransfer
|
from pretix.plugins.banktransfer.payment import BankTransfer
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
@@ -51,16 +54,28 @@ class EventWizardFoundationForm(forms.Form):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
|
self.session = kwargs.pop('session')
|
||||||
super().__init__(*args, **kwargs)
|
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(
|
self.fields['organizer'] = forms.ModelChoiceField(
|
||||||
label=_("Organizer"),
|
label=_("Organizer"),
|
||||||
queryset=Organizer.objects.filter(
|
queryset=qs,
|
||||||
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
|
widget=Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'generic',
|
||||||
|
'data-select2-url': reverse('control:organizers.select2') + '?can_create=1',
|
||||||
|
'data-placeholder': _('Organizer')
|
||||||
|
}
|
||||||
),
|
),
|
||||||
widget=forms.RadioSelect,
|
|
||||||
empty_label=None,
|
empty_label=None,
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
|
self.fields['organizer'].widget.choices = self.fields['organizer'].choices
|
||||||
|
|
||||||
if len(self.fields['organizer'].choices) == 1:
|
if len(self.fields['organizer'].choices) == 1:
|
||||||
self.fields['organizer'].initial = self.fields['organizer'].queryset.first()
|
self.fields['organizer'].initial = self.fields['organizer'].queryset.first()
|
||||||
|
|
||||||
@@ -116,6 +131,7 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
self.locales = kwargs.get('locales')
|
self.locales = kwargs.get('locales')
|
||||||
self.has_subevents = kwargs.pop('has_subevents')
|
self.has_subevents = kwargs.pop('has_subevents')
|
||||||
kwargs.pop('user')
|
kwargs.pop('user')
|
||||||
|
kwargs.pop('session')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.initial['timezone'] = get_current_timezone_name()
|
self.initial['timezone'] = get_current_timezone_name()
|
||||||
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
|
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
|
||||||
@@ -173,7 +189,9 @@ class EventChoiceField(forms.ModelChoiceField):
|
|||||||
class EventWizardCopyForm(forms.Form):
|
class EventWizardCopyForm(forms.Form):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy_from_queryset(user):
|
def copy_from_queryset(user, session):
|
||||||
|
if user.has_active_staff_session(session.session_key):
|
||||||
|
return Event.objects.all()
|
||||||
return Event.objects.filter(
|
return Event.objects.filter(
|
||||||
Q(organizer_id__in=user.teams.filter(
|
Q(organizer_id__in=user.teams.filter(
|
||||||
all_events=True, can_change_event_settings=True, can_change_items=True
|
all_events=True, can_change_event_settings=True, can_change_items=True
|
||||||
@@ -185,16 +203,25 @@ class EventWizardCopyForm(forms.Form):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs.pop('organizer')
|
kwargs.pop('organizer')
|
||||||
kwargs.pop('locales')
|
kwargs.pop('locales')
|
||||||
|
self.session = kwargs.pop('session')
|
||||||
kwargs.pop('has_subevents')
|
kwargs.pop('has_subevents')
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['copy_from_event'] = EventChoiceField(
|
self.fields['copy_from_event'] = EventChoiceField(
|
||||||
label=_("Copy configuration from"),
|
label=_("Copy configuration from"),
|
||||||
queryset=EventWizardCopyForm.copy_from_queryset(self.user),
|
queryset=EventWizardCopyForm.copy_from_queryset(self.user, self.session),
|
||||||
widget=forms.RadioSelect,
|
widget=Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'event',
|
||||||
|
'data-select2-url': reverse('control:events.typeahead') + '?can_copy=1',
|
||||||
|
'data-placeholder': _('Do not copy')
|
||||||
|
}
|
||||||
|
),
|
||||||
empty_label=_('Do not copy'),
|
empty_label=_('Do not copy'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
self.fields['copy_from_event'].widget.choices = self.fields['copy_from_event'].choices
|
||||||
|
|
||||||
|
|
||||||
class EventMetaValueForm(forms.ModelForm):
|
class EventMetaValueForm(forms.ModelForm):
|
||||||
@@ -664,6 +691,13 @@ class InvoiceSettingsForm(SettingsForm):
|
|||||||
),
|
),
|
||||||
help_text=_("Invoices will never be automatically generated for free orders.")
|
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(
|
invoice_attendee_name = forms.BooleanField(
|
||||||
label=_("Show attendee names on invoices"),
|
label=_("Show attendee names on invoices"),
|
||||||
required=False
|
required=False
|
||||||
@@ -779,6 +813,16 @@ class InvoiceSettingsForm(SettingsForm):
|
|||||||
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
|
self.fields['invoice_numbers_prefix'].widget.attrs['placeholder'] = event.slug.upper() + '-'
|
||||||
locale_names = dict(settings.LANGUAGES)
|
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_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):
|
class MailSettingsForm(SettingsForm):
|
||||||
@@ -790,12 +834,20 @@ class MailSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
mail_from = forms.EmailField(
|
mail_from = forms.EmailField(
|
||||||
label=_("Sender address"),
|
label=_("Sender address"),
|
||||||
help_text=_("Sender address for outgoing emails")
|
help_text=_("Sender address for outgoing emails"),
|
||||||
)
|
)
|
||||||
mail_bcc = forms.EmailField(
|
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(
|
||||||
label=_("Bcc address"),
|
label=_("Bcc address"),
|
||||||
help_text=_("All emails will be sent to this address as a Bcc copy"),
|
help_text=_("All emails will be sent to this address as a Bcc copy"),
|
||||||
required=False
|
validators=[multimail_validate],
|
||||||
|
required=False,
|
||||||
|
max_length=255
|
||||||
)
|
)
|
||||||
|
|
||||||
mail_text_signature = I18nFormField(
|
mail_text_signature = I18nFormField(
|
||||||
@@ -818,7 +870,7 @@ class MailSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
mail_text_order_placed = I18nFormField(
|
mail_text_order_placed = I18nFormField(
|
||||||
label=_("Text"),
|
label=_("Text sent to order contact address"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=I18nTextarea,
|
widget=I18nTextarea,
|
||||||
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
|
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
|
||||||
@@ -826,20 +878,62 @@ class MailSettingsForm(SettingsForm):
|
|||||||
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
|
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
|
||||||
'{payment_info}', '{url}', '{invoice_name}', '{invoice_company}'])]
|
'{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(
|
mail_text_order_paid = I18nFormField(
|
||||||
label=_("Text"),
|
label=_("Text sent to order contact address"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=I18nTextarea,
|
widget=I18nTextarea,
|
||||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
|
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
|
||||||
validators=[PlaceholderValidator(['{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(
|
mail_text_order_free = I18nFormField(
|
||||||
label=_("Text"),
|
label=_("Text sent to order contact address"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=I18nTextarea,
|
widget=I18nTextarea,
|
||||||
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
|
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
|
||||||
validators=[PlaceholderValidator(['{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(
|
mail_text_order_changed = I18nFormField(
|
||||||
label=_("Text"),
|
label=_("Text"),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -899,12 +993,25 @@ class MailSettingsForm(SettingsForm):
|
|||||||
'{invoice_name}', '{invoice_company}'])]
|
'{invoice_name}', '{invoice_company}'])]
|
||||||
)
|
)
|
||||||
mail_text_download_reminder = I18nFormField(
|
mail_text_download_reminder = I18nFormField(
|
||||||
label=_("Text"),
|
label=_("Text sent to order contact address"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=I18nTextarea,
|
widget=I18nTextarea,
|
||||||
help_text=_("Available placeholders: {event}, {url}"),
|
help_text=_("Available placeholders: {event}, {url}"),
|
||||||
validators=[PlaceholderValidator(['{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(
|
mail_days_download_reminder = forms.IntegerField(
|
||||||
label=_("Number of days"),
|
label=_("Number of days"),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -985,13 +1092,26 @@ class MailSettingsForm(SettingsForm):
|
|||||||
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
|
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
|
||||||
]
|
]
|
||||||
keys = list(event.meta_data.keys())
|
keys = list(event.meta_data.keys())
|
||||||
for k, v in self.fields.items():
|
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||||
|
for k, v in list(self.fields.items()):
|
||||||
if k.startswith('mail_text_'):
|
if k.startswith('mail_text_'):
|
||||||
v.help_text = str(v.help_text) + ', ' + ', '.join({
|
v.help_text = str(v.help_text) + ', ' + ', '.join({
|
||||||
'{meta_' + p + '}' for p in keys
|
'{meta_' + p + '}' for p in keys
|
||||||
})
|
})
|
||||||
v.validators[0].limit_value += ['{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):
|
def clean(self):
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
if not data.get('smtp_password') and data.get('smtp_username'):
|
if not data.get('smtp_password') and data.get('smtp_username'):
|
||||||
@@ -1046,6 +1166,7 @@ class DisplaySettingsForm(SettingsForm):
|
|||||||
choices=[
|
choices=[
|
||||||
('Open Sans', 'Open Sans')
|
('Open Sans', 'Open Sans')
|
||||||
],
|
],
|
||||||
|
widget=FontSelect,
|
||||||
help_text=_('Only respected by modern browsers.')
|
help_text=_('Only respected by modern browsers.')
|
||||||
)
|
)
|
||||||
frontpage_text = I18nFormField(
|
frontpage_text = I18nFormField(
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ from django.urls import reverse
|
|||||||
from django.utils.translation import (
|
from django.utils.translation import (
|
||||||
pgettext_lazy, ugettext as __, ugettext_lazy as _,
|
pgettext_lazy, ugettext as __, ugettext_lazy as _,
|
||||||
)
|
)
|
||||||
|
from django_scopes.forms import (
|
||||||
|
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||||
|
)
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
@@ -94,6 +97,10 @@ class QuestionForm(I18nModelForm):
|
|||||||
),
|
),
|
||||||
'dependency_value': forms.Select,
|
'dependency_value': forms.Select,
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'items': SafeModelMultipleChoiceField,
|
||||||
|
'dependency_question': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionForm(I18nModelForm):
|
class QuestionOptionForm(I18nModelForm):
|
||||||
@@ -159,6 +166,9 @@ class QuotaForm(I18nModelForm):
|
|||||||
'size',
|
'size',
|
||||||
'subevent'
|
'subevent'
|
||||||
]
|
]
|
||||||
|
field_classes = {
|
||||||
|
'subevent': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
creating = not self.instance.pk
|
creating = not self.instance.pk
|
||||||
@@ -206,6 +216,8 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
empty_label=_('Do not copy'),
|
empty_label=_('Do not copy'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
if self.event.tax_rules.exists():
|
||||||
|
self.fields['tax_rule'].required = True
|
||||||
|
|
||||||
if not self.event.has_subevents:
|
if not self.event.has_subevents:
|
||||||
choices = [
|
choices = [
|
||||||
@@ -364,6 +376,8 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
'over 65. This ticket includes access to all parts of the event, except the VIP '
|
'over 65. This ticket includes access to all parts of the event, except the VIP '
|
||||||
'area.'
|
'area.'
|
||||||
)
|
)
|
||||||
|
if self.event.tax_rules.exists():
|
||||||
|
self.fields['tax_rule'].required = True
|
||||||
self.fields['description'].widget.attrs['rows'] = '4'
|
self.fields['description'].widget.attrs['rows'] = '4'
|
||||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
||||||
label=_('Sales channels'),
|
label=_('Sales channels'),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
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 import InvoiceAddress, ItemAddOn, Order, OrderPosition
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
@@ -116,6 +117,12 @@ class MarkPaidForm(ConfirmPaymentForm):
|
|||||||
localize=True,
|
localize=True,
|
||||||
label=_('Payment amount'),
|
label=_('Payment amount'),
|
||||||
)
|
)
|
||||||
|
payment_date = forms.DateField(
|
||||||
|
required=True,
|
||||||
|
label=_('Payment date'),
|
||||||
|
widget=DatePickerWidget(),
|
||||||
|
initial=now
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -185,7 +192,7 @@ class OrderPositionAddForm(forms.Form):
|
|||||||
label=_('Product')
|
label=_('Product')
|
||||||
)
|
)
|
||||||
addon_to = forms.ModelChoiceField(
|
addon_to = forms.ModelChoiceField(
|
||||||
OrderPosition.objects.none(),
|
OrderPosition.all.none(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Add-on to'),
|
label=_('Add-on to'),
|
||||||
)
|
)
|
||||||
@@ -285,9 +292,6 @@ class OrderPositionChangeForm(forms.Form):
|
|||||||
instance = kwargs.pop('instance')
|
instance = kwargs.pop('instance')
|
||||||
initial = kwargs.get('initial', {})
|
initial = kwargs.get('initial', {})
|
||||||
|
|
||||||
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
|
initial['price'] = instance.price
|
||||||
|
|
||||||
kwargs['initial'] = initial
|
kwargs['initial'] = initial
|
||||||
@@ -340,7 +344,7 @@ class OrderContactForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ['email']
|
fields = ['email', 'email_known_to_work']
|
||||||
|
|
||||||
|
|
||||||
class OrderLocaleForm(forms.ModelForm):
|
class OrderLocaleForm(forms.ModelForm):
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||||
|
|
||||||
from pretix.api.models import WebHook
|
from pretix.api.models import WebHook
|
||||||
from pretix.api.webhooks import get_all_webhook_events
|
from pretix.api.webhooks import get_all_webhook_events
|
||||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||||
from pretix.base.models import Device, Organizer, Team
|
from pretix.base.models import Device, Organizer, Team
|
||||||
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
|
from pretix.control.forms import (
|
||||||
|
ExtFileField, FontSelect, MultipleLanguagesWidget,
|
||||||
|
)
|
||||||
from pretix.multidomain.models import KnownDomain
|
from pretix.multidomain.models import KnownDomain
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
@@ -147,6 +150,9 @@ class TeamForm(forms.ModelForm):
|
|||||||
'data-inverse-dependency': '#id_all_events'
|
'data-inverse-dependency': '#id_all_events'
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'limit_events': SafeModelMultipleChoiceField
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
@@ -175,6 +181,9 @@ class DeviceForm(forms.ModelForm):
|
|||||||
'data-inverse-dependency': '#id_all_events'
|
'data-inverse-dependency': '#id_all_events'
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'limit_events': SafeModelMultipleChoiceField
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OrganizerSettingsForm(SettingsForm):
|
class OrganizerSettingsForm(SettingsForm):
|
||||||
@@ -260,6 +269,7 @@ class OrganizerDisplaySettingsForm(SettingsForm):
|
|||||||
choices=[
|
choices=[
|
||||||
('Open Sans', 'Open Sans')
|
('Open Sans', 'Open Sans')
|
||||||
],
|
],
|
||||||
|
widget=FontSelect,
|
||||||
help_text=_('Only respected by modern browsers.')
|
help_text=_('Only respected by modern browsers.')
|
||||||
)
|
)
|
||||||
favicon = ExtFileField(
|
favicon = ExtFileField(
|
||||||
@@ -304,3 +314,6 @@ class WebHookForm(forms.ModelForm):
|
|||||||
'data-inverse-dependency': '#id_all_events'
|
'data-inverse-dependency': '#id_all_events'
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'limit_events': SafeModelMultipleChoiceField
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,12 +48,14 @@ class UserEditForm(forms.ModelForm):
|
|||||||
'email',
|
'email',
|
||||||
'require_2fa',
|
'require_2fa',
|
||||||
'is_active',
|
'is_active',
|
||||||
'is_staff'
|
'is_staff',
|
||||||
|
'last_login'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['email'].required = True
|
self.fields['email'].required = True
|
||||||
|
self.fields['last_login'].disabled = True
|
||||||
|
|
||||||
def clean_email(self):
|
def clean_email(self):
|
||||||
email = self.cleaned_data['email']
|
email = self.cleaned_data['email']
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
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.forms import I18nModelForm
|
||||||
from pretix.base.models import Item, Voucher
|
from pretix.base.models import Item, Voucher
|
||||||
@@ -35,6 +36,7 @@ class VoucherForm(I18nModelForm):
|
|||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'valid_until': SplitDateTimeField,
|
'valid_until': SplitDateTimeField,
|
||||||
|
'subevent': SafeModelChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'valid_until': SplitDateTimePickerWidget(),
|
'valid_until': SplitDateTimePickerWidget(),
|
||||||
@@ -146,6 +148,14 @@ class VoucherForm(I18nModelForm):
|
|||||||
data, self.instance.event,
|
data, self.instance.event,
|
||||||
self.instance.quota, self.instance.item, self.instance.variation
|
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(
|
Voucher.clean_subevent(
|
||||||
data, self.instance.event
|
data, self.instance.event
|
||||||
)
|
)
|
||||||
@@ -191,6 +201,7 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'valid_until': SplitDateTimeField,
|
'valid_until': SplitDateTimeField,
|
||||||
|
'subevent': SafeModelChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'valid_until': SplitDateTimePickerWidget(),
|
'valid_until': SplitDateTimePickerWidget(),
|
||||||
|
|||||||
@@ -251,6 +251,9 @@ 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.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.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.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.added': _('The quota has been added.'),
|
||||||
'pretix.event.quota.deleted': _('The quota has been deleted.'),
|
'pretix.event.quota.deleted': _('The quota has been deleted.'),
|
||||||
'pretix.event.quota.changed': _('The quota has been changed.'),
|
'pretix.event.quota.changed': _('The quota has been changed.'),
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
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.urls import get_script_prefix, resolve, reverse
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django_scopes import scope
|
||||||
from hijack.templatetags.hijack_tags import is_hijacked
|
from hijack.templatetags.hijack_tags import is_hijacked
|
||||||
|
|
||||||
from pretix.base.models import Event, Organizer
|
from pretix.base.models import Event, Organizer
|
||||||
@@ -17,7 +18,7 @@ from pretix.helpers.security import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PermissionMiddleware(MiddlewareMixin):
|
class PermissionMiddleware:
|
||||||
"""
|
"""
|
||||||
This middleware enforces all requests to the control app to require login.
|
This middleware enforces all requests to the control app to require login.
|
||||||
Additionally, it enforces all requests to "control:event." URLs
|
Additionally, it enforces all requests to "control:event." URLs
|
||||||
@@ -34,6 +35,10 @@ class PermissionMiddleware(MiddlewareMixin):
|
|||||||
"user.settings.notifications.off",
|
"user.settings.notifications.off",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, get_response=None):
|
||||||
|
self.get_response = get_response
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def _login_redirect(self, request):
|
def _login_redirect(self, request):
|
||||||
# Taken from django/contrib/auth/decorators.py
|
# Taken from django/contrib/auth/decorators.py
|
||||||
path = request.build_absolute_uri()
|
path = request.build_absolute_uri()
|
||||||
@@ -52,19 +57,19 @@ class PermissionMiddleware(MiddlewareMixin):
|
|||||||
return redirect_to_login(
|
return redirect_to_login(
|
||||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||||
|
|
||||||
def process_request(self, request):
|
def __call__(self, request):
|
||||||
url = resolve(request.path_info)
|
url = resolve(request.path_info)
|
||||||
url_name = url.url_name
|
url_name = url.url_name
|
||||||
|
|
||||||
if not request.path.startswith(get_script_prefix() + 'control'):
|
if not request.path.startswith(get_script_prefix() + 'control'):
|
||||||
# This middleware should only touch the /control subpath
|
# This middleware should only touch the /control subpath
|
||||||
return
|
return self.get_response(request)
|
||||||
|
|
||||||
if hasattr(request, 'organizer'):
|
if hasattr(request, 'organizer'):
|
||||||
# If the user is on a organizer's subdomain, he should be redirected to pretix
|
# 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()))
|
return redirect(urljoin(settings.SITE_URL, request.get_full_path()))
|
||||||
if url_name in self.EXCEPTIONS:
|
if url_name in self.EXCEPTIONS:
|
||||||
return
|
return self.get_response(request)
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return self._login_redirect(request)
|
return self._login_redirect(request)
|
||||||
|
|
||||||
@@ -79,6 +84,7 @@ class PermissionMiddleware(MiddlewareMixin):
|
|||||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||||
|
|
||||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||||
|
with scope(organizer=None):
|
||||||
request.event = Event.objects.filter(
|
request.event = Event.objects.filter(
|
||||||
slug=url.kwargs['event'],
|
slug=url.kwargs['event'],
|
||||||
organizer__slug=url.kwargs['organizer'],
|
organizer__slug=url.kwargs['organizer'],
|
||||||
@@ -104,6 +110,12 @@ class PermissionMiddleware(MiddlewareMixin):
|
|||||||
else:
|
else:
|
||||||
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
|
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:
|
class AuditLogMiddleware:
|
||||||
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ def get_event_navigation(request: HttpRequest):
|
|||||||
|
|
||||||
merge_in(nav, sorted(
|
merge_in(nav, sorted(
|
||||||
sum((list(a[1]) for a in nav_event.send(request.event, request=request)), []),
|
sum((list(a[1]) for a in nav_event.send(request.event, request=request)), []),
|
||||||
key=lambda r: r['label']
|
key=lambda r: (1 if r.get('parent') else 0, r['label'])
|
||||||
))
|
))
|
||||||
|
|
||||||
return nav
|
return nav
|
||||||
@@ -391,7 +391,7 @@ def get_global_navigation(request):
|
|||||||
|
|
||||||
merge_in(nav, sorted(
|
merge_in(nav, sorted(
|
||||||
sum((list(a[1]) for a in nav_global.send(request, request=request)), []),
|
sum((list(a[1]) for a in nav_global.send(request, request=request)), []),
|
||||||
key=lambda r: r['label']
|
key=lambda r: (1 if r.get('parent') else 0, r['label'])
|
||||||
))
|
))
|
||||||
return nav
|
return nav
|
||||||
|
|
||||||
@@ -464,7 +464,7 @@ def get_organizer_navigation(request):
|
|||||||
merge_in(nav, sorted(
|
merge_in(nav, sorted(
|
||||||
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
|
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
|
||||||
[]),
|
[]),
|
||||||
key=lambda r: r['label']
|
key=lambda r: (1 if r.get('parent') else 0, r['label'])
|
||||||
))
|
))
|
||||||
return nav
|
return nav
|
||||||
|
|
||||||
@@ -474,6 +474,8 @@ def merge_in(nav, newnav):
|
|||||||
if 'parent' in item:
|
if 'parent' in item:
|
||||||
parents = [n for n in nav if n['url'] == item['parent']]
|
parents = [n for n in nav if n['url'] == item['parent']]
|
||||||
if parents:
|
if parents:
|
||||||
|
if 'children' not in parents[0]:
|
||||||
|
parents[0]['children'] = []
|
||||||
parents[0]['children'].append(item)
|
parents[0]['children'].append(item)
|
||||||
else:
|
else:
|
||||||
nav.append(item)
|
nav.append(item)
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ 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
|
set to ``True``, when this item should be marked as active. The ``request`` object
|
||||||
will have an attribute ``event``.
|
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>`
|
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
|
||||||
in pretix.
|
in pretix.
|
||||||
|
|
||||||
@@ -73,6 +79,12 @@ 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
|
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.
|
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>`
|
If you use this, you should read the documentation on :ref:`how to deal with URLs <urlconf>`
|
||||||
in pretix.
|
in pretix.
|
||||||
|
|
||||||
@@ -173,6 +185,12 @@ 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
|
an ``active`` key with a boolean set to ``True``, when this item should be marked
|
||||||
as active.
|
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
|
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
|
that you use ``pretix.control.views.organizer.OrganizerDetailViewMixin`` for your view
|
||||||
and your template inherits from ``pretixcontrol/organizers/base.html``.
|
and your template inherits from ``pretixcontrol/organizers/base.html``.
|
||||||
@@ -219,6 +237,24 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
|||||||
A second keyword argument ``request`` will contain the request object.
|
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(
|
event_settings_widget = EventPluginSignal(
|
||||||
providing_args=['request']
|
providing_args=['request']
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -242,7 +242,10 @@
|
|||||||
<span class="caret"></span></a>
|
<span class="caret"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul class="dropdown-menu event-dropdown" role="menu" data-event-typeahead
|
<ul class="dropdown-menu event-dropdown" role="menu" data-event-typeahead
|
||||||
data-source="{% url "control:nav.typeahead" %}">
|
data-source="{% url "control:nav.typeahead" %}"
|
||||||
|
{% if request.event %}
|
||||||
|
data-organizer="{{ request.organizer.id }}"
|
||||||
|
{% endif %}>
|
||||||
<li class="query-holder">
|
<li class="query-holder">
|
||||||
<div class="form-box">
|
<div class="form-box">
|
||||||
<input type="text" class="form-control" id="event-dropdown-field"
|
<input type="text" class="form-control" id="event-dropdown-field"
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load hierarkey_form %}
|
{% load hierarkey_form %}
|
||||||
|
{% block custom_header %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
|
||||||
|
{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Display settings" %}</h1>
|
<h1>{% trans "Display settings" %}</h1>
|
||||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<div class="panel panel-default items">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
{% trans "Your timeline" %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body timeline">
|
||||||
|
{% regroup timeline by date as tl_list %}
|
||||||
|
{% for day in tl_list %}
|
||||||
|
<div class="row {% if day.grouper < today %}text-muted{% endif %}">
|
||||||
|
<div class="col-date">
|
||||||
|
<strong>{{ day.grouper|date:"SHORT_DATE_FORMAT" }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-event">
|
||||||
|
{% for e in day.list %}
|
||||||
|
<strong class="">{{ e.time|date:"TIME_FORMAT" }}</strong>
|
||||||
|
|
||||||
|
<span class="{% if e.time < nearly_now %}text-muted{% endif %}">
|
||||||
|
{{ e.entry.description }}
|
||||||
|
</span>
|
||||||
|
{% if e.entry.edit_url %}
|
||||||
|
|
||||||
|
<a href="{{ e.entry.edit_url }}" class="text-muted">
|
||||||
|
<span class="fa fa-edit"></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if forloop.revcounter > 1 %}
|
||||||
|
<br/>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -90,6 +90,9 @@
|
|||||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not request.event.has_subevents or subevent %}
|
||||||
|
{% include "pretixcontrol/event/fragment_timeline.html" %}
|
||||||
|
{% endif %}
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
{% for w in widgets %}
|
{% for w in widgets %}
|
||||||
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
|
<div class="widget-container widget-{{ w.display_size|default:"small" }}">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "General settings" %}</legend>
|
<legend>{% trans "General settings" %}</legend>
|
||||||
{% bootstrap_field form.invoice_generate layout="control" %}
|
{% bootstrap_field form.invoice_generate layout="control" %}
|
||||||
|
{% bootstrap_field form.invoice_generate_sales_channels layout="control" %}
|
||||||
{% bootstrap_field form.invoice_email_attachment layout="control" %}
|
{% bootstrap_field form.invoice_email_attachment layout="control" %}
|
||||||
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}
|
{% bootstrap_field form.invoice_numbers_prefix layout="control" %}
|
||||||
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %}
|
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Event logs" %}</h1>
|
<h1>{% trans "Event logs" %}</h1>
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
<form class="form-inline helper-display-inline" action="" method="get">
|
||||||
|
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
||||||
|
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
||||||
<p>
|
<p>
|
||||||
<select name="user" class="form-control">
|
<select name="user" class="form-control">
|
||||||
<option value="">{% trans "All actions" %}</option>
|
<option value="">{% trans "All actions" %}</option>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<legend>{% trans "General settings" %}</legend>
|
<legend>{% trans "General settings" %}</legend>
|
||||||
{% bootstrap_field form.mail_prefix layout="control" %}
|
{% bootstrap_field form.mail_prefix layout="control" %}
|
||||||
{% bootstrap_field form.mail_from layout="control" %}
|
{% bootstrap_field form.mail_from layout="control" %}
|
||||||
|
{% bootstrap_field form.mail_from_name layout="control" %}
|
||||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||||
{% bootstrap_field form.mail_bcc layout="control" %}
|
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -40,13 +41,13 @@
|
|||||||
<legend>{% trans "E-mail content" %}</legend>
|
<legend>{% trans "E-mail content" %}</legend>
|
||||||
<div class="panel-group" id="questions_group">
|
<div class="panel-group" id="questions_group">
|
||||||
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
|
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed,mail_send_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
|
||||||
|
|
||||||
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
|
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid,mail_send_order_paid_attendee,mail_text_order_paid_attendee" exclude="mail_send_order_paid_attendee" %}
|
||||||
|
|
||||||
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
|
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free,mail_send_order_free_attendee,mail_text_order_free_attendee" exclude="mail_send_order_free_attendee" %}
|
||||||
|
|
||||||
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
|
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
|
||||||
|
|
||||||
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
|
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder" exclude="mail_days_download_reminder" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee" %}
|
||||||
|
|
||||||
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
|
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
{% with exclude|split as exclusion %}
|
{% with exclude|split as exclusion %}
|
||||||
{% with items|split as item_list %}
|
{% with items|split as item_list %}
|
||||||
{% for item in item_list %}
|
{% for item in item_list %}
|
||||||
{% if item in exclusion %}
|
{% if item in exclusion and form|hasattr:item %}
|
||||||
{% with form|getattr:item as field %}
|
{% with form|getattr:item as field %}
|
||||||
{% bootstrap_field field layout="horizontal" %}
|
{% bootstrap_field field layout="horizontal" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% elif form|hasattr:item %}
|
||||||
<div id="{{ item }}_panel" class="preview-panel form-group" for="{{ item }}">
|
<div id="{{ item }}_panel" class="preview-panel form-group" for="{{ item }}">
|
||||||
{% with form|getattr:item as field %}
|
{% with form|getattr:item as field %}
|
||||||
<label class="col-md-3 control-label">{{ field.label }}</label>
|
<label class="col-md-3 control-label">{{ field.label }}</label>
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{% load i18n %}{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} class="preload-font"
|
||||||
|
data-family="{{ widget.label }}" data-style="regular">{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} <strong>{{ widget.label }}</strong><br>{% trans "The quick brown fox jumps over the lazy dog." context "typography" %}</label>{% endif %}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for log in obj.all_logentries %}
|
{% for log in obj.top_logentries %}
|
||||||
<li class="list-group-item logentry">
|
<li class="list-group-item logentry">
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
@@ -45,4 +45,11 @@
|
|||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if obj.all_logentries_link and obj.top_logentries_has_more %}
|
||||||
|
<li class="list-group-item logentry">
|
||||||
|
<a href="{{ obj.all_logentries_link }}">
|
||||||
|
{% trans "View full log" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -27,6 +27,13 @@
|
|||||||
{% trans "Bundled products" %}
|
{% trans "Bundled products" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% for n in extra_nav %}
|
||||||
|
<li {% if n.active %}class="active"{% endif %}>
|
||||||
|
<a href="{{ n.url }}">
|
||||||
|
{{ n.label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h1>{% trans "Create product" %}</h1>
|
<h1>{% trans "Create product" %}</h1>
|
||||||
|
|||||||
@@ -60,7 +60,13 @@
|
|||||||
<td>
|
<td>
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in q.items.all %}
|
{% for item in q.items.all %}
|
||||||
|
{% if not item.has_variations %}
|
||||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
|
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for v in q.variations.all %}
|
||||||
|
<li><a href="{% url "control:event.item.variations" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}">
|
||||||
|
{{ v.item }} – {{ v }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -121,11 +121,13 @@
|
|||||||
<strong>{% trans "Price" %}</strong>
|
<strong>{% trans "Price" %}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-5">
|
<div class="col-sm-5">
|
||||||
{{ position.price|money:request.event.currency }}<br>
|
{{ position.price|money:request.event.currency }}
|
||||||
{% if position.tax_rate %}
|
{% if position.tax_rate %}
|
||||||
<small>{% blocktrans trimmed with rate=position.tax_rate name=position.tax_rule.name %}
|
<br>
|
||||||
<strong>incl.</strong> {{ rate }}% {{ name }}
|
<small>
|
||||||
{% endblocktrans %}</small>
|
({{ position.net_price|money:request.event.currency }}
|
||||||
|
+ {{ position.tax_rate }}%)
|
||||||
|
</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 field-container">
|
<div class="col-sm-4 field-container">
|
||||||
|
|||||||
@@ -136,7 +136,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<dt>{% trans "User" %}</dt>
|
<dt>{% trans "User" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ order.email|default_if_none:"" }}
|
{{ order.email|default_if_none:"" }}
|
||||||
|
{% if order.email and order.email_known_to_work %}
|
||||||
|
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||||
<span class="fa fa-edit"></span>
|
<span class="fa fa-edit"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<input type="hidden" name="status" value="p" />
|
<input type="hidden" name="status" value="p" />
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
{% bootstrap_field form.amount layout='horizontal' %}
|
{% bootstrap_field form.amount layout='horizontal' %}
|
||||||
|
{% bootstrap_field form.payment_date layout='horizontal' %}
|
||||||
{% if form.force %}
|
{% if form.force %}
|
||||||
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{% extends "pretixcontrol/organizers/base.html" %}
|
{% extends "pretixcontrol/organizers/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
{% block custom_header %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
|
||||||
|
{% endblock %}
|
||||||
{% block inner %}
|
{% block inner %}
|
||||||
<h1>{% trans "Display settings" %}</h1>
|
<h1>{% trans "Display settings" %}</h1>
|
||||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% compress css %}
|
{% compress css %}
|
||||||
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
|
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
|
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>
|
<h1>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
{% bootstrap_field form.email layout='control' %}
|
{% bootstrap_field form.email layout='control' %}
|
||||||
{% bootstrap_field form.new_pw layout='control' %}
|
{% bootstrap_field form.new_pw layout='control' %}
|
||||||
{% bootstrap_field form.new_pw_repeat layout='control' %}
|
{% bootstrap_field form.new_pw_repeat layout='control' %}
|
||||||
|
{% bootstrap_field form.last_login layout='control' %}
|
||||||
{% bootstrap_field form.require_2fa layout='control' %}
|
{% bootstrap_field form.require_2fa layout='control' %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -11,3 +11,12 @@ def split(value, delimiter=","):
|
|||||||
@register.filter(name="getattr")
|
@register.filter(name="getattr")
|
||||||
def get_attribute(value, key):
|
def get_attribute(value, key):
|
||||||
return value[key]
|
return value[key]
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="hasattr")
|
||||||
|
def has_attribute(value, key):
|
||||||
|
try:
|
||||||
|
value[key]
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ urlpatterns = [
|
|||||||
url(r'^users/(?P<id>\d+)/reset$', users.UserResetView.as_view(), name='users.reset'),
|
url(r'^users/(?P<id>\d+)/reset$', users.UserResetView.as_view(), name='users.reset'),
|
||||||
url(r'^users/(?P<id>\d+)/impersonate', users.UserImpersonateView.as_view(), name='users.impersonate'),
|
url(r'^users/(?P<id>\d+)/impersonate', users.UserImpersonateView.as_view(), name='users.impersonate'),
|
||||||
url(r'^users/(?P<id>\d+)/anonymize', users.UserAnonymizeView.as_view(), name='users.anonymize'),
|
url(r'^users/(?P<id>\d+)/anonymize', users.UserAnonymizeView.as_view(), name='users.anonymize'),
|
||||||
|
url(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
|
||||||
url(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
|
url(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
|
||||||
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
|
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
|
||||||
url(r'^settings/notifications/$', user.UserNotificationsEditView.as_view(), name='user.settings.notifications'),
|
url(r'^settings/notifications/$', user.UserNotificationsEditView.as_view(), name='user.settings.notifications'),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@@ -22,6 +23,7 @@ from pretix.base.models import (
|
|||||||
WaitingListEntry,
|
WaitingListEntry,
|
||||||
)
|
)
|
||||||
from pretix.base.models.checkin import CheckinList
|
from pretix.base.models.checkin import CheckinList
|
||||||
|
from pretix.base.timeline import timeline_for_event
|
||||||
from pretix.control.forms.event import CommentForm
|
from pretix.control.forms.event import CommentForm
|
||||||
from pretix.control.signals import (
|
from pretix.control.signals import (
|
||||||
event_dashboard_widgets, user_dashboard_widgets,
|
event_dashboard_widgets, user_dashboard_widgets,
|
||||||
@@ -279,6 +281,7 @@ def event_index(request, organizer, event):
|
|||||||
ctx = {
|
ctx = {
|
||||||
'widgets': rearrange(widgets),
|
'widgets': rearrange(widgets),
|
||||||
'logs': qs[:5],
|
'logs': qs[:5],
|
||||||
|
'subevent': subevent,
|
||||||
'actions': a_qs[:5] if can_change_orders else [],
|
'actions': a_qs[:5] if can_change_orders else [],
|
||||||
'comment_form': CommentForm(initial={'comment': request.event.comment})
|
'comment_form': CommentForm(initial={'comment': request.event.comment})
|
||||||
}
|
}
|
||||||
@@ -302,7 +305,19 @@ def event_index(request, organizer, event):
|
|||||||
for a in ctx['actions']:
|
for a in ctx['actions']:
|
||||||
a.display = a.display(request)
|
a.display = a.display(request)
|
||||||
|
|
||||||
return render(request, 'pretixcontrol/event/index.html', ctx)
|
ctx['timeline'] = [
|
||||||
|
{
|
||||||
|
'date': t.datetime.astimezone(request.event.timezone).date(),
|
||||||
|
'entry': t,
|
||||||
|
'time': t.datetime.astimezone(request.event.timezone)
|
||||||
|
}
|
||||||
|
for t in timeline_for_event(request.event, subevent)
|
||||||
|
]
|
||||||
|
ctx['today'] = now().astimezone(request.event.timezone).date()
|
||||||
|
ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
|
||||||
|
resp = render(request, 'pretixcontrol/event/index.html', ctx)
|
||||||
|
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def annotated_event_query(request):
|
def annotated_event_query(request):
|
||||||
|
|||||||
@@ -689,13 +689,14 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
|||||||
expires=now(), code="PREVIEW", total=119)
|
expires=now(), code="PREVIEW", total=119)
|
||||||
item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23,
|
item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23,
|
||||||
description=ugettext("Sample product description"))
|
description=ugettext("Sample product description"))
|
||||||
order.positions.create(item=item, attendee_name_parts={'_legacy': ugettext("John Doe")},
|
p = order.positions.create(item=item, attendee_name_parts={'_legacy': ugettext("John Doe")},
|
||||||
price=item.default_price)
|
price=item.default_price)
|
||||||
v = renderers[request.GET.get('renderer')].render(
|
v = renderers[request.GET.get('renderer')].render(
|
||||||
v,
|
v,
|
||||||
str(request.event.settings.mail_text_signature),
|
str(request.event.settings.mail_text_signature),
|
||||||
ugettext('Your order: %(code)s') % {'code': order.code},
|
ugettext('Your order: %(code)s') % {'code': order.code},
|
||||||
order
|
order,
|
||||||
|
position=p
|
||||||
)
|
)
|
||||||
r = HttpResponse(v, content_type='text/html')
|
r = HttpResponse(v, content_type='text/html')
|
||||||
r._csp_ignore = True
|
r._csp_ignore = True
|
||||||
@@ -974,6 +975,12 @@ class EventLog(EventPermissionRequiredMixin, ListView):
|
|||||||
elif self.request.GET.get('user'):
|
elif self.request.GET.get('user'):
|
||||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||||
|
|
||||||
|
if self.request.GET.get('content_type'):
|
||||||
|
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
|
||||||
|
|
||||||
|
if self.request.GET.get('object'):
|
||||||
|
qs = qs.filter(object_id=self.request.GET.get('object'))
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, F, Q
|
from django.db.models import Count, F, Prefetch, Q
|
||||||
from django.forms.models import inlineformset_factory
|
from django.forms.models import inlineformset_factory
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
@@ -33,7 +33,7 @@ from pretix.control.forms.item import (
|
|||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
EventPermissionRequiredMixin, event_permission_required,
|
EventPermissionRequiredMixin, event_permission_required,
|
||||||
)
|
)
|
||||||
from pretix.control.signals import item_forms
|
from pretix.control.signals import item_forms, nav_item
|
||||||
|
|
||||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||||
|
|
||||||
@@ -565,7 +565,14 @@ class QuotaList(PaginationMixin, ListView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Quota.objects.filter(
|
qs = Quota.objects.filter(
|
||||||
event=self.request.event
|
event=self.request.event
|
||||||
).prefetch_related("items")
|
).prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"items",
|
||||||
|
queryset=Item.objects.annotate(has_variations=Count('variations'))
|
||||||
|
),
|
||||||
|
"variations",
|
||||||
|
"variations__item"
|
||||||
|
)
|
||||||
if self.request.GET.get("subevent", "") != "":
|
if self.request.GET.get("subevent", "") != "":
|
||||||
s = self.request.GET.get("subevent", "")
|
s = self.request.GET.get("subevent", "")
|
||||||
qs = qs.filter(subevent_id=s)
|
qs = qs.filter(subevent_id=s)
|
||||||
@@ -775,6 +782,18 @@ class ItemDetailMixin(SingleObjectMixin):
|
|||||||
model = Item
|
model = Item
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
nav = sorted(
|
||||||
|
sum(
|
||||||
|
(list(a[1]) for a in nav_item.send(self.request.event, request=self.request, item=self.get_object())),
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
key=lambda r: str(r['label'])
|
||||||
|
)
|
||||||
|
ctx['extra_nav'] = nav
|
||||||
|
return ctx
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> Item:
|
def get_object(self, queryset=None) -> Item:
|
||||||
try:
|
try:
|
||||||
if not hasattr(self, 'object') or not self.object:
|
if not hasattr(self, 'object') or not self.object:
|
||||||
@@ -893,6 +912,12 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['plugin_forms'] = self.plugin_forms
|
ctx['plugin_forms'] = self.plugin_forms
|
||||||
|
|
||||||
|
if not ctx['item'].active and ctx['item'].bundled_with.count() > 0:
|
||||||
|
messages.info(self.request, _("You disabled this item, but it is still part of a product bundle. "
|
||||||
|
"Your participants won't be able to buy the bundle unless you remove this "
|
||||||
|
"item from it."))
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class EventList(PaginationMixin, ListView):
|
|||||||
def condition_copy(wizard):
|
def condition_copy(wizard):
|
||||||
return (
|
return (
|
||||||
not wizard.clone_from and
|
not wizard.clone_from and
|
||||||
EventWizardCopyForm.copy_from_queryset(wizard.request.user).exists()
|
EventWizardCopyForm.copy_from_queryset(wizard.request.user, wizard.request.session).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -176,7 +176,8 @@ class EventWizard(SafeSessionWizardView):
|
|||||||
|
|
||||||
def get_form_kwargs(self, step=None):
|
def get_form_kwargs(self, step=None):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'user': self.request.user
|
'user': self.request.user,
|
||||||
|
'session': self.request.session,
|
||||||
}
|
}
|
||||||
if step != 'foundation':
|
if step != 'foundation':
|
||||||
fdata = self.get_cleaned_data_for_step('foundation')
|
fdata = self.get_cleaned_data_for_step('foundation')
|
||||||
@@ -203,6 +204,7 @@ class EventWizard(SafeSessionWizardView):
|
|||||||
event.organizer = foundation_data['organizer']
|
event.organizer = foundation_data['organizer']
|
||||||
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
event.plugins = settings.PRETIX_PLUGINS_DEFAULT
|
||||||
event.has_subevents = foundation_data['has_subevents']
|
event.has_subevents = foundation_data['has_subevents']
|
||||||
|
event.testmode = True
|
||||||
form_dict['basics'].save()
|
form_dict['basics'].save()
|
||||||
|
|
||||||
has_control_rights = self.request.user.teams.filter(
|
has_control_rights = self.request.user.teams.filter(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal, DecimalException
|
from decimal import Decimal, DecimalException
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@@ -24,7 +24,7 @@ from django.utils import formats
|
|||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
DetailView, FormView, ListView, TemplateView, View,
|
DetailView, FormView, ListView, TemplateView, View,
|
||||||
@@ -862,8 +862,15 @@ class OrderTransition(OrderView):
|
|||||||
fee=None
|
fee=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
payment_date = None
|
||||||
|
if self.mark_paid_form.cleaned_data['payment_date'] != now().date():
|
||||||
|
payment_date = make_aware(datetime.combine(
|
||||||
|
self.mark_paid_form.cleaned_data['payment_date'],
|
||||||
|
time(hour=0, minute=0, second=0)
|
||||||
|
), self.order.event.timezone)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p.confirm(user=self.request.user, count_waitinglist=False,
|
p.confirm(user=self.request.user, count_waitinglist=False, payment_date=payment_date,
|
||||||
force=self.mark_paid_form.cleaned_data.get('force', False))
|
force=self.mark_paid_form.cleaned_data.get('force', False))
|
||||||
except Quota.QuotaExceededException as e:
|
except Quota.QuotaExceededException as e:
|
||||||
p.state = OrderPayment.PAYMENT_STATE_FAILED
|
p.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||||
@@ -1365,6 +1372,7 @@ class OrderContactChange(OrderView):
|
|||||||
},
|
},
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.form.cleaned_data['regenerate_secrets']:
|
if self.form.cleaned_data['regenerate_secrets']:
|
||||||
changed = True
|
changed = True
|
||||||
self.order.secret = generate_secret()
|
self.order.secret = generate_secret()
|
||||||
@@ -1474,9 +1482,10 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
|||||||
'code': order.code,
|
'code': order.code,
|
||||||
'date': date_format(order.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
'date': date_format(order.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||||
'expire_date': date_format(order.expires, 'SHORT_DATE_FORMAT'),
|
'expire_date': date_format(order.expires, 'SHORT_DATE_FORMAT'),
|
||||||
'url': build_absolute_uri(order.event, 'presale:event.order', kwargs={
|
'url': build_absolute_uri(order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
}),
|
}),
|
||||||
'invoice_name': invoice_name,
|
'invoice_name': invoice_name,
|
||||||
'invoice_company': invoice_company,
|
'invoice_company': invoice_company,
|
||||||
@@ -1646,6 +1655,7 @@ class ExportMixin:
|
|||||||
|
|
||||||
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View):
|
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View):
|
||||||
permission = 'can_view_orders'
|
permission = 'can_view_orders'
|
||||||
|
known_errortypes = ['ExportError']
|
||||||
task = export
|
task = export
|
||||||
|
|
||||||
def get_success_message(self, value):
|
def get_success_message(self, value):
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user