mirror of
https://github.com/pretix/pretix.git
synced 2026-01-17 23:32:26 +00:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d1fc31cd | ||
|
|
597211d83a | ||
|
|
17679d4304 | ||
|
|
0fb70c78a9 | ||
|
|
e254e90e49 | ||
|
|
9c6e5f025d | ||
|
|
3c86532218 | ||
|
|
3834ae566f | ||
|
|
6766b2b19e | ||
|
|
b6d2f67c7c | ||
|
|
e70f593a94 | ||
|
|
ed5726fc0c | ||
|
|
5400d26c60 | ||
|
|
0bb6104532 | ||
|
|
16aa403735 | ||
|
|
1c279a92a7 | ||
|
|
35985dcb11 | ||
|
|
b0dcbe31fa | ||
|
|
b3c3ee3b22 | ||
|
|
aab340fd87 | ||
|
|
1871324ef4 | ||
|
|
d799d560b7 | ||
|
|
01e2851a76 | ||
|
|
ef2a4244ed | ||
|
|
55539dc8e5 | ||
|
|
ef303bfcc4 | ||
|
|
fff9ac04a9 | ||
|
|
76d27fbfaa | ||
|
|
2b1123b487 | ||
|
|
3607d8706d | ||
|
|
31fdf8721b | ||
|
|
128a1f349a | ||
|
|
7d432f0639 | ||
|
|
1ffc799c4d | ||
|
|
25dd8f2e2f | ||
|
|
b121596e4b | ||
|
|
cf835df62e | ||
|
|
7a3b7d4f02 | ||
|
|
b151d8f455 | ||
|
|
06de74d877 | ||
|
|
2ae9e3e0d9 | ||
|
|
0c0fe58bbf | ||
|
|
7b1e1a48ef | ||
|
|
c7dd50de0d | ||
|
|
a1caa65776 | ||
|
|
260973345d | ||
|
|
2c9b2620ea | ||
|
|
909c80e710 | ||
|
|
5a218ae6a9 | ||
|
|
b498d45621 | ||
|
|
b02196434b | ||
|
|
c0edce7760 | ||
|
|
cc46d55f5e | ||
|
|
ea8abb8dab | ||
|
|
f765d094b4 | ||
|
|
86f222870d | ||
|
|
19b5270d76 | ||
|
|
db76b9b0ef | ||
|
|
d23e53873f | ||
|
|
c116a4b998 | ||
|
|
2471d4bca5 | ||
|
|
8e04dbdcca | ||
|
|
0928358396 | ||
|
|
23f783c15c | ||
|
|
edae96c84f | ||
|
|
242ebdfae9 | ||
|
|
0ee502abec | ||
|
|
29cb1e93d8 | ||
|
|
c89242855c | ||
|
|
61a1368ed2 | ||
|
|
ac3e00fa03 | ||
|
|
d9d0f7b6f3 | ||
|
|
ad5e2df3be | ||
|
|
ec34561815 | ||
|
|
e1540b1648 | ||
|
|
a6b265455d | ||
|
|
8a6334bd86 | ||
|
|
173a23722a | ||
|
|
ab8eb2a34d | ||
|
|
30dcda616b | ||
|
|
3eafec9d6e | ||
|
|
a5910016fd | ||
|
|
0a49b93b26 | ||
|
|
7449bea836 | ||
|
|
0fc4478332 | ||
|
|
0df4a6e7ed | ||
|
|
a37cd380c8 | ||
|
|
11b2bd8887 | ||
|
|
8986db0975 | ||
|
|
2921611cb1 | ||
|
|
785fb29513 | ||
|
|
81c3d7fa17 | ||
|
|
8ff963698d | ||
|
|
6da63e0169 | ||
|
|
f84903ae27 | ||
|
|
a0a7859b33 | ||
|
|
af23d6e4bf | ||
|
|
7e9c9beace | ||
|
|
ac2fc2de5c | ||
|
|
45e548873e | ||
|
|
f484eb65df | ||
|
|
027a785ab5 | ||
|
|
25b80cbb57 | ||
|
|
589fa0f9de | ||
|
|
6d2989d15a | ||
|
|
5bb27b29ae | ||
|
|
d17f8a71e6 | ||
|
|
b664cc712a | ||
|
|
d61e8a9204 | ||
|
|
f00012a63e | ||
|
|
bd238f76ce | ||
|
|
703ae97820 | ||
|
|
1a60c5ea64 | ||
|
|
1d3ac5f02f | ||
|
|
8d23d75dfd | ||
|
|
9a32668ee1 | ||
|
|
ca0407a133 | ||
|
|
1de77b0784 | ||
|
|
d0907d3dcf | ||
|
|
81cc4bd768 | ||
|
|
262639e063 | ||
|
|
dedd93fb89 | ||
|
|
45f94aee03 | ||
|
|
d36e7d033f | ||
|
|
b94bd277bf | ||
|
|
e5095185d9 | ||
|
|
d76ce47597 | ||
|
|
58717850c2 | ||
|
|
29d52d4fe5 | ||
|
|
34c9c40ddc | ||
|
|
39d05a6c40 | ||
|
|
b664222c62 | ||
|
|
1ee48a10b5 | ||
|
|
2431a8b767 | ||
|
|
af84354e51 | ||
|
|
b04de880fc | ||
|
|
11f3057f76 | ||
|
|
ba164c16f6 | ||
|
|
7ef766ddfa | ||
|
|
bcafcc7dd8 | ||
|
|
e5b7102abc | ||
|
|
3601dd6bee | ||
|
|
a1d5854fbf | ||
|
|
09544a688d | ||
|
|
58a5892cc0 | ||
|
|
c9af76b46e | ||
|
|
91753935cf |
18
.travis.yml
18
.travis.yml
@@ -13,24 +13,24 @@ services:
|
||||
- postgresql
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.7
|
||||
- python: 3.8
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.7
|
||||
- python: 3.8
|
||||
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.7
|
||||
- python: 3.8
|
||||
env: JOB=style
|
||||
- python: 3.7
|
||||
- python: 3.8
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.5
|
||||
- python: 3.8
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.7
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.8
|
||||
env: JOB=doc-spelling
|
||||
- python: 3.7
|
||||
- python: 3.8
|
||||
env: JOB=translation-spelling
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
postgresql: "10"
|
||||
mariadb: '10.3'
|
||||
apt:
|
||||
packages:
|
||||
|
||||
@@ -182,7 +182,7 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
-v /var/pretix-data:/data \
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
--sysctl net.core.somaxconn=4096
|
||||
--sysctl net.core.somaxconn=4096 \
|
||||
pretix/standalone:stable all
|
||||
ExecStop=/usr/bin/docker stop %n
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ solution with many things readily set-up, look at :ref:`dockersmallscale`.
|
||||
get it right. If you're not feeling comfortable managing a Linux server, check out our hosting and service
|
||||
offers at `pretix.eu`_.
|
||||
|
||||
We tested this guide on the Linux distribution **Debian 8.0** but it should work very similar on other
|
||||
We tested this guide on the Linux distribution **Debian 10.0** but it should work very similar on other
|
||||
modern distributions, especially on all systemd-based ones.
|
||||
|
||||
Requirements
|
||||
@@ -133,7 +133,7 @@ command if you're running MySQL::
|
||||
|
||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||
|
||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
||||
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
We also need to create a data directory::
|
||||
|
||||
|
||||
@@ -151,6 +151,10 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``order.fees.canceled`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The ``reactivate`` operation has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -173,6 +177,13 @@ price money (string) Price of this p
|
||||
attendee_name string Specified attendee name for this position (or ``null``)
|
||||
attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name)
|
||||
attendee_email string Specified attendee email address for this position (or ``null``)
|
||||
company string Attendee company name (or ``null``)
|
||||
street string Attendee street (or ``null``)
|
||||
zipcode string Attendee ZIP code (or ``null``)
|
||||
city string Attendee city (or ``null``)
|
||||
country string Attendee country code (or ``null``)
|
||||
state string Attendee state (ISO 3166-2 code). Only supported in
|
||||
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
tax_rate decimal (string) VAT rate applied for this position
|
||||
tax_value money (string) VAT included in this position
|
||||
@@ -236,6 +247,10 @@ pdf_data object Data object req
|
||||
|
||||
The attribute ``canceled`` has been added.
|
||||
|
||||
.. versionchanged:: 3.8
|
||||
|
||||
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -380,6 +395,12 @@ List of all orders
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"company": "Sample company",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
@@ -536,6 +557,12 @@ Fetching individual orders
|
||||
"full_name": "Peter",
|
||||
},
|
||||
"attendee_email": null,
|
||||
"company": "Sample company",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "DE",
|
||||
"state": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
@@ -816,9 +843,9 @@ Creating orders
|
||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
||||
creation.
|
||||
* ``email``
|
||||
* ``email`` (optional)
|
||||
* ``locale``
|
||||
* ``sales_channel``
|
||||
* ``sales_channel`` (optional)
|
||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
||||
@@ -851,15 +878,21 @@ Creating orders
|
||||
|
||||
* ``positionid`` (optional, see below)
|
||||
* ``item``
|
||||
* ``variation``
|
||||
* ``variation`` (optional)
|
||||
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
|
||||
* ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.)
|
||||
* ``attendee_name`` **or** ``attendee_name_parts``
|
||||
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
||||
* ``attendee_email``
|
||||
* ``attendee_email`` (optional)
|
||||
* ``company`` (optional)
|
||||
* ``street`` (optional)
|
||||
* ``zipcode`` (optional)
|
||||
* ``city`` (optional)
|
||||
* ``country`` (optional)
|
||||
* ``state`` (optional)
|
||||
* ``secret`` (optional)
|
||||
* ``addon_to`` (optional, see below)
|
||||
* ``subevent``
|
||||
* ``subevent`` (optional)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
@@ -1057,6 +1090,42 @@ Order state operations
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/reactivate/
|
||||
|
||||
Reactivates a canceled order. This will set the order to pending or paid state. Only possible if all products are
|
||||
still available.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/reactivate/ 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
|
||||
|
||||
{
|
||||
"code": "ABC12",
|
||||
"status": "n",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be reactivated
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_pending/
|
||||
|
||||
Marks a paid order as unpaid.
|
||||
|
||||
@@ -66,7 +66,7 @@ event-related views, there is also a signal that allows you to add the view to t
|
||||
|
||||
from django.urls import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from pretix.control.signals import nav_event
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: validate_cart, validate_cart_addons, 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, order_split, order_gracefully_delete, invoice_line_text
|
||||
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||
|
||||
Check-ins
|
||||
"""""""""
|
||||
@@ -33,11 +33,11 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, 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, render_seating_plan, checkout_flow_steps, position_info, item_description
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, 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, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: order_info, order_meta_from_request
|
||||
:members: order_info, order_info_top, order_meta_from_request
|
||||
|
||||
Request flow
|
||||
""""""""""""
|
||||
|
||||
@@ -61,7 +61,7 @@ A working example would be::
|
||||
from pretix.base.plugins import PluginConfig
|
||||
except ImportError:
|
||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class PaypalApp(PluginConfig):
|
||||
|
||||
@@ -69,7 +69,7 @@ We now need a way to translate the action codes like ``pretix.event.changed`` in
|
||||
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
||||
implementation could look like::
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from pretix.base.signals import logentry_display
|
||||
|
||||
@receiver(signal=logentry_display)
|
||||
|
||||
277
doc/plugins/digital.rst
Normal file
277
doc/plugins/digital.rst
Normal file
@@ -0,0 +1,277 @@
|
||||
Digital content
|
||||
===============
|
||||
|
||||
The digital content plugin provides a HTTP API that allows you to create new digital content for your ticket holders,
|
||||
such as live streams, videos, or material downloads.
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The digital content resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal content ID
|
||||
title multi-lingual string The content title (required)
|
||||
content_type string The type of content, valid values are ``webinar``, ``video``, ``livestream``, ``link``, ``file``
|
||||
url string The location of the digital content
|
||||
description multi-lingual string A public description of the item. May contain Markdown
|
||||
syntax and is not required.
|
||||
available_from datetime The first date time at which this content will be shown
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this content will b e shown
|
||||
(or ``null``).
|
||||
all_products boolean If ``true``, the content is available to all buyers of tickets for this event. The ``limit_products`` field is ignored in this case.
|
||||
limit_products list of integers List of product/item IDs. This content is only shown to buyers of these ticket types.
|
||||
position integer An integer, used for sorting
|
||||
subevent integer Date in an event series this content should be shown for. Should be ``null`` if this is not an event series or if this should be shown to all customers.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||
|
||||
Returns a list of all digital content configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ 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": [
|
||||
{
|
||||
"id": 1,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Returns information on one content item, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ 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
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the content to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/
|
||||
|
||||
Create a new digital content.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 166
|
||||
|
||||
{
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new content for
|
||||
:param event: The ``slug`` field of the event to create new content for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The content could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create digital contents.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Update a content. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
|
||||
{
|
||||
"url": "https://mywebsite.com"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 2,
|
||||
"subevent": null,
|
||||
"title": {
|
||||
"en": "Concert livestream"
|
||||
},
|
||||
"content_type": "link",
|
||||
"url": "https://mywebsite.com",
|
||||
"description": {
|
||||
"en": "Watch our event live here on YouTube!"
|
||||
},
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"available_from": "2020-03-22T23:00:00Z",
|
||||
"available_until": null,
|
||||
"position": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the content to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The content could not be modified due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it.
|
||||
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/digitalcontents/(id)/
|
||||
|
||||
Delete a digital content.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/digitalcontents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the content to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/content does not exist **or** you have no permission to change it
|
||||
@@ -15,3 +15,4 @@ If you want to **create** a plugin, please go to the
|
||||
ticketoutputpdf
|
||||
badges
|
||||
campaigns
|
||||
digital
|
||||
|
||||
@@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps
|
||||
With the pretix.eu hosted service
|
||||
---------------------------------
|
||||
|
||||
Step 1: DNS Configuration
|
||||
#########################
|
||||
Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain".
|
||||
|
||||
This page will show you instructions on how to set up your own domain. Basically, it works like this:
|
||||
|
||||
Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their
|
||||
interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every
|
||||
domain provider.
|
||||
|
||||
Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias").
|
||||
The value of the record should be ``www.pretix.eu``.
|
||||
|
||||
Step 2: Wait for the DNS entry to propagate
|
||||
###########################################
|
||||
The value of the record should be the one shown on the "Custom Domain" page in pretix' backend.
|
||||
|
||||
Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches
|
||||
of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``.
|
||||
If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page
|
||||
anyways, you should get a pretix-themed error page with the headline "Unknown domain".
|
||||
|
||||
Step 3: Tell us
|
||||
###############
|
||||
|
||||
Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL
|
||||
certificate for you (for free!) and configure the domain.
|
||||
|
||||
Now, tell us about your domain on the "Custom Domain" page to get started.
|
||||
|
||||
With a custom pretix installation
|
||||
---------------------------------
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.7.0.dev0"
|
||||
__version__ = "3.8.0"
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from oauth2_provider.generators import (
|
||||
generate_client_id, generate_client_secret,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import timedelta
|
||||
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import gettext_lazy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@@ -56,7 +56,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
else validated_data.get('item').quotas.filter(subevent=validated_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
raise ValidationError(
|
||||
ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(validated_data.get('item'))
|
||||
)
|
||||
)
|
||||
@@ -64,8 +64,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
avail = quota.availability()
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < 1):
|
||||
raise ValidationError(
|
||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform '
|
||||
'the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
)
|
||||
@@ -88,7 +88,7 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from pytz import common_timezones
|
||||
@@ -532,6 +532,9 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'voucher_explanation_text',
|
||||
'banner_text',
|
||||
'banner_text_bottom',
|
||||
'show_dates_on_frontpage',
|
||||
'show_date_to',
|
||||
'show_times',
|
||||
'show_items_outside_presale_period',
|
||||
@@ -557,6 +560,10 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'confirm_text',
|
||||
'order_email_asked_twice',
|
||||
'payment_term_days',
|
||||
@@ -610,6 +617,10 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'cancel_allow_user_paid_keep',
|
||||
'cancel_allow_user_paid_keep_fees',
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
29
src/pretix/api/serializers/fields.py
Normal file
29
src/pretix/api/serializers/fields.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def remove_duplicates_from_list(data):
|
||||
return list(OrderedDict.fromkeys(data))
|
||||
|
||||
|
||||
class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, str) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
||||
internal_value_data = [
|
||||
super(serializers.MultipleChoiceField, self).to_internal_value(item)
|
||||
for item in data
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(internal_value_data)
|
||||
|
||||
def to_representation(self, value):
|
||||
representation_data = [
|
||||
self.choice_strings_to_values.get(str(item), item) for item in value
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(representation_data)
|
||||
@@ -3,7 +3,7 @@ from decimal import Decimal
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
@@ -287,8 +287,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
@@ -5,7 +5,7 @@ from decimal import Decimal
|
||||
import pycountry
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -39,7 +39,7 @@ class CompatibleCountryField(serializers.Field):
|
||||
def to_representation(self, instance: InvoiceAddress):
|
||||
if instance.country:
|
||||
return str(instance.country)
|
||||
else:
|
||||
elif hasattr(instance, 'country_old'):
|
||||
return instance.country_old
|
||||
|
||||
|
||||
@@ -211,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
|
||||
@@ -516,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
max_digits=10)
|
||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||
required=False, allow_null=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, v in self.fields.items():
|
||||
if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'):
|
||||
v.required = False
|
||||
v.allow_blank = True
|
||||
v.allow_null = True
|
||||
|
||||
def validate_secret(self, secret):
|
||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||
raise ValidationError(
|
||||
@@ -576,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country')):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -862,7 +892,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
pos_data['seat'] = seat
|
||||
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
seats_seen.add(seat)
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
@@ -877,7 +907,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if pos_data.get('variation')
|
||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||
if len(new_quotas) == 0:
|
||||
errs[i]['item'] = [ugettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format(
|
||||
str(pos_data.get('item'))
|
||||
)]
|
||||
else:
|
||||
@@ -889,7 +919,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
quota_avail_cache[quota][1] -= 1
|
||||
if quota_avail_cache[quota][1] < 0:
|
||||
errs[i]['item'] = [
|
||||
ugettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||
gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format(
|
||||
quota.name
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.exceptions import OAuthToolkitError
|
||||
from oauth2_provider.forms import AllowForm
|
||||
from oauth2_provider.views import (
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.db.models.functions import Coalesce, Concat
|
||||
from django.http import FileResponse, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
@@ -44,7 +44,7 @@ from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _order_placed_email,
|
||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
extend_order, mark_order_expired, mark_order_refunded, reactivate_order,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tickets import generate
|
||||
@@ -261,6 +261,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def reactivate(self, request, **kwargs):
|
||||
|
||||
order = self.get_object()
|
||||
if order.status != Order.STATUS_CANCELED:
|
||||
return Response(
|
||||
{'detail': 'The order is not allowed to be reactivated.'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
reactivate_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||
)
|
||||
except OrderError as e:
|
||||
return Response(
|
||||
{'detail': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def approve(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
@@ -58,7 +58,7 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.seating_plans.all()
|
||||
return self.request.organizer.seating_plans.order_by('name')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -195,7 +195,7 @@ class TeamViewSet(viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_teams'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.teams.all()
|
||||
return self.request.organizer.teams.order_by('pk')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -268,7 +268,7 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
|
||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.team.invites.all()
|
||||
return self.team.invites.order_by('email')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -305,7 +305,7 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
return get_object_or_404(self.request.organizer.teams, pk=self.kwargs.get('team'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.team.tokens.all()
|
||||
return self.team.tokens.order_by('name')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
@@ -7,7 +7,7 @@ import requests
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from requests import RequestException
|
||||
|
||||
@@ -125,6 +125,10 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.order.canceled',
|
||||
_('Order canceled'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.reactivated',
|
||||
_('Order reactivated'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.expired',
|
||||
_('Order expired'),
|
||||
|
||||
@@ -85,6 +85,16 @@ class BaseAuthBackend:
|
||||
"""
|
||||
return
|
||||
|
||||
def get_next_url(self, request):
|
||||
"""
|
||||
This method will be called after a successful login to determine the next URL. Pretix in general uses the
|
||||
``'next'`` query parameter. However, external authentication methods could use custom attributes with hardcoded
|
||||
names for security purposes. For example, OAuth uses ``'state'`` for keeping track of application state.
|
||||
"""
|
||||
if "next" in request.GET:
|
||||
return request.GET.get("next")
|
||||
return None
|
||||
|
||||
|
||||
class NativeAuthBackend(BaseAuthBackend):
|
||||
identifier = 'native'
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import register_sales_channels
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Tuple
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell.cell import KNOWN_TYPES
|
||||
|
||||
@@ -180,9 +180,9 @@ class MultiSheetListExporter(ListExporter):
|
||||
]
|
||||
for s, l in self.sheets:
|
||||
choices += [
|
||||
(s + ':default', str(l) + ' – ' + ugettext('CSV (with commas)')),
|
||||
(s + ':excel', str(l) + ' – ' + ugettext('CSV (Excel-style)')),
|
||||
(s + ':semicolon', str(l) + ' – ' + ugettext('CSV (with semicolons)')),
|
||||
(s + ':default', str(l) + ' – ' + gettext('CSV (with commas)')),
|
||||
(s + ':excel', str(l) + ' – ' + gettext('CSV (Excel-style)')),
|
||||
(s + ':semicolon', str(l) + ' – ' + gettext('CSV (with semicolons)')),
|
||||
]
|
||||
ff = OrderedDict(
|
||||
[
|
||||
|
||||
@@ -5,7 +5,7 @@ from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import QuestionAnswer
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import dateutil
|
||||
from django import forms
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext, ugettext_lazy
|
||||
from django.utils.translation import gettext, gettext_lazy
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Invoice, OrderPayment
|
||||
@@ -79,7 +79,7 @@ class DekodiNREIExporter(BaseExporter):
|
||||
payments.append({
|
||||
'PTID': '5',
|
||||
'PTN': 'Lastschrift',
|
||||
'PTNo4': ugettext('Event ticket {event}-{code}').format(
|
||||
'PTNo4': gettext('Event ticket {event}-{code}').format(
|
||||
event=self.event.slug.upper(),
|
||||
code=invoice.order.code
|
||||
),
|
||||
@@ -199,19 +199,19 @@ class DekodiNREIExporter(BaseExporter):
|
||||
[
|
||||
('date_from',
|
||||
forms.DateField(
|
||||
label=ugettext_lazy('Start date'),
|
||||
label=gettext_lazy('Start date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
help_text=ugettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
||||
'not always correspond to the order or payment date.')
|
||||
help_text=gettext_lazy('Only include invoices issued on or after this date. Note that the invoice date does '
|
||||
'not always correspond to the order or payment date.')
|
||||
)),
|
||||
('date_to',
|
||||
forms.DateField(
|
||||
label=ugettext_lazy('End date'),
|
||||
label=gettext_lazy('End date'),
|
||||
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
|
||||
required=False,
|
||||
help_text=ugettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
||||
'does not always correspond to the order or payment date.')
|
||||
help_text=gettext_lazy('Only include invoices issued on or before this date. Note that the invoice date '
|
||||
'does not always correspond to the order or payment date.')
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import dateutil.parser
|
||||
from django import forms
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import OrderPayment
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import OrderPosition
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ from django.db.models import (
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
@@ -22,7 +22,7 @@ from ..signals import register_data_exporters
|
||||
|
||||
class OrderListExporter(MultiSheetListExporter):
|
||||
identifier = 'orderlist'
|
||||
verbose_name = ugettext_lazy('Order data')
|
||||
verbose_name = gettext_lazy('Order data')
|
||||
|
||||
@property
|
||||
def sheets(self):
|
||||
@@ -305,6 +305,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Attendee name') + ': ' + str(label))
|
||||
headers += [
|
||||
_('Attendee email'),
|
||||
_('Company'),
|
||||
_('Address'),
|
||||
_('ZIP code'),
|
||||
_('City'),
|
||||
_('Country'),
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Pseudonymization ID'),
|
||||
]
|
||||
@@ -364,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
)
|
||||
row += [
|
||||
op.attendee_email,
|
||||
op.company or '',
|
||||
op.street or '',
|
||||
op.zipcode or '',
|
||||
op.city or '',
|
||||
op.country if op.country else '',
|
||||
op.state or '',
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
]
|
||||
@@ -414,7 +426,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
class PaymentListExporter(ListExporter):
|
||||
identifier = 'paymentlist'
|
||||
verbose_name = ugettext_lazy('Order payments and refunds')
|
||||
verbose_name = gettext_lazy('Order payments and refunds')
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
@@ -485,7 +497,7 @@ class PaymentListExporter(ListExporter):
|
||||
|
||||
class QuotaListExporter(ListExporter):
|
||||
identifier = 'quotalist'
|
||||
verbose_name = ugettext_lazy('Quota availabilities')
|
||||
verbose_name = gettext_lazy('Quota availabilities')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
headers = [
|
||||
@@ -514,7 +526,7 @@ class QuotaListExporter(ListExporter):
|
||||
|
||||
class InvoiceDataExporter(MultiSheetListExporter):
|
||||
identifier = 'invoicedata'
|
||||
verbose_name = ugettext_lazy('Invoice data')
|
||||
verbose_name = gettext_lazy('Invoice data')
|
||||
|
||||
@property
|
||||
def sheets(self):
|
||||
@@ -698,6 +710,45 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
||||
return '{}_invoices'.format(self.event.slug)
|
||||
|
||||
|
||||
class GiftcardRedemptionListExporter(ListExporter):
|
||||
identifier = 'giftcardredemptionlist'
|
||||
verbose_name = gettext_lazy('Giftcard Redemptions')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
tz = pytz.timezone(self.event.settings.timezone)
|
||||
|
||||
payments = OrderPayment.objects.filter(
|
||||
order__event=self.event,
|
||||
provider='giftcard'
|
||||
).order_by('created')
|
||||
refunds = OrderRefund.objects.filter(
|
||||
order__event=self.event,
|
||||
provider='giftcard'
|
||||
).order_by('created')
|
||||
|
||||
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
|
||||
|
||||
headers = [
|
||||
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
|
||||
]
|
||||
yield headers
|
||||
|
||||
for obj in objs:
|
||||
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
|
||||
row = [
|
||||
obj.order.code,
|
||||
obj.full_id,
|
||||
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
|
||||
gc.secret,
|
||||
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||
gc.issuer
|
||||
]
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_giftcardredemptions'.format(self.event.slug)
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
|
||||
def register_orderlist_exporter(sender, **kwargs):
|
||||
return OrderListExporter
|
||||
@@ -716,3 +767,8 @@ def register_quotalist_exporter(sender, **kwargs):
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_invoicedata")
|
||||
def register_invoicedata_exporter(sender, **kwargs):
|
||||
return InvoiceDataExporter
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
|
||||
def register_giftcardredemptionlist_exporter(sender, **kwargs):
|
||||
return GiftcardRedemptionListExporter
|
||||
|
||||
@@ -3,7 +3,6 @@ import logging
|
||||
import i18nfield.forms
|
||||
from django import forms
|
||||
from django.forms.models import ModelFormMetaclass
|
||||
from django.utils import six
|
||||
from django.utils.crypto import get_random_string
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from hierarkey.forms import HierarkeyForm
|
||||
@@ -25,7 +24,7 @@ class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)):
|
||||
class I18nModelForm(BaseI18nModelForm, metaclass=ModelFormMetaclass):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ from django.conf import settings
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
@@ -36,7 +37,7 @@ class LoginForm(forms.Form):
|
||||
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
||||
del self.fields['keep_logged_in']
|
||||
else:
|
||||
self.fields.move_to_end('keep_logged_in')
|
||||
move_to_end(self.fields, 'keep_logged_in')
|
||||
|
||||
def clean(self):
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.forms import Select
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import (
|
||||
get_language, pgettext_lazy, ugettext_lazy as _,
|
||||
get_language, gettext_lazy as _, pgettext_lazy,
|
||||
)
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
@@ -41,6 +41,7 @@ from pretix.base.settings import (
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.control.forms import SplitDateTimeField
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.escapejson import escapejson_attr
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
from pretix.presale.signals import question_form_fields
|
||||
@@ -214,6 +215,10 @@ def guess_country(event):
|
||||
return country
|
||||
|
||||
|
||||
class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
@@ -241,7 +246,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
if item.admission and event.settings.attendee_names_asked:
|
||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=event.settings.attendee_names_required,
|
||||
required=event.settings.attendee_names_required and not self.all_optional,
|
||||
scheme=event.settings.name_scheme,
|
||||
titles=event.settings.name_scheme_titles,
|
||||
label=_('Attendee name'),
|
||||
@@ -249,7 +254,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
)
|
||||
if item.admission and event.settings.attendee_emails_asked:
|
||||
self.fields['attendee_email'] = forms.EmailField(
|
||||
required=event.settings.attendee_emails_required,
|
||||
required=event.settings.attendee_emails_required and not self.all_optional,
|
||||
label=_('Attendee email'),
|
||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
||||
widget=forms.EmailInput(
|
||||
@@ -258,6 +263,75 @@ class BaseQuestionsForm(forms.Form):
|
||||
}
|
||||
)
|
||||
)
|
||||
if item.admission and event.settings.attendee_company_asked:
|
||||
self.fields['company'] = forms.CharField(
|
||||
required=event.settings.attendee_company_required and not self.all_optional,
|
||||
label=_('Company'),
|
||||
initial=(cartpos.company if cartpos else orderpos.company),
|
||||
)
|
||||
|
||||
if item.admission and event.settings.attendee_addresses_asked:
|
||||
self.fields['street'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Address'),
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
'placeholder': _('Street and Number'),
|
||||
'autocomplete': 'street-address'
|
||||
}),
|
||||
initial=(cartpos.street if cartpos else orderpos.street),
|
||||
)
|
||||
self.fields['zipcode'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('ZIP code'),
|
||||
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'postal-code',
|
||||
}),
|
||||
)
|
||||
self.fields['city'] = forms.CharField(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('City'),
|
||||
initial=(cartpos.city if cartpos else orderpos.city),
|
||||
widget=forms.TextInput(attrs={
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
)
|
||||
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
|
||||
self.fields['country'] = CountryField(
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Country'),
|
||||
initial=country,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'country',
|
||||
}),
|
||||
)
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||
cc = None
|
||||
if fprefix + 'country' in self.data:
|
||||
cc = str(self.data[fprefix + 'country'])
|
||||
elif country:
|
||||
cc = str(country)
|
||||
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
self.fields['state'] = forms.ChoiceField(
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
)
|
||||
self.fields['state'].widget.is_required = True
|
||||
|
||||
for q in questions:
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
@@ -309,12 +383,14 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||
field = CountryField().formfield(
|
||||
field = CountryField(
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Select,
|
||||
empty_label='',
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else guess_country(event),
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
@@ -332,7 +408,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
to_field_name='identifier',
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
widget=QuestionCheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
@@ -419,6 +495,10 @@ class BaseQuestionsForm(forms.Form):
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
if not d.get('state'):
|
||||
self.add_error('state', _('This field is required.'))
|
||||
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
@@ -500,6 +580,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
if not event.settings.invoice_address_vatid:
|
||||
del self.fields['vat_id']
|
||||
|
||||
self.fields['country'].choices = CachedCountries()
|
||||
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = self.prefix + '-' if self.prefix else ''
|
||||
cc = None
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from pytz import common_timezones
|
||||
|
||||
from pretix.base.models import User
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from django import forms
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class DatePickerWidget(forms.DateInput):
|
||||
|
||||
@@ -3,7 +3,7 @@ from contextlib import contextmanager
|
||||
from django.conf import settings
|
||||
from django.utils import translation
|
||||
from django.utils.formats import date_format, number_format
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.translation import gettext
|
||||
from i18nfield.fields import ( # noqa
|
||||
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
||||
)
|
||||
@@ -69,6 +69,6 @@ class LazyLocaleException(Exception):
|
||||
|
||||
def __str__(self):
|
||||
if self.msgargs:
|
||||
return ugettext(self.msg) % self.msgargs
|
||||
return gettext(self.msg) % self.msgargs
|
||||
else:
|
||||
return ugettext(self.msg)
|
||||
return gettext(self.msg)
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.contrib.staticfiles import finders
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import (
|
||||
get_language, pgettext, ugettext, ugettext_lazy,
|
||||
get_language, gettext, gettext_lazy, pgettext,
|
||||
)
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
@@ -264,7 +264,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_to_top = 52 * mm
|
||||
|
||||
def _draw_invoice_to(self, canvas):
|
||||
p = Paragraph(self.invoice.address_invoice_to.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p = Paragraph(bleach.clean(self.invoice.address_invoice_to, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||
@@ -422,7 +423,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
canvas.saveState()
|
||||
canvas.setFont('OpenSansBd', 30)
|
||||
canvas.setFillColorRGB(32, 0, 0)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, ugettext('TEST MODE'))
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
|
||||
canvas.restoreState()
|
||||
|
||||
def _on_first_page(self, canvas: Canvas, doc):
|
||||
@@ -687,7 +688,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
identifier = 'modern1'
|
||||
verbose_name = ugettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
|
||||
bottom_margin = 16.9 * mm
|
||||
top_margin = 16.9 * mm
|
||||
right_margin = 20 * mm
|
||||
|
||||
@@ -15,7 +15,9 @@ from django.utils.translation.trans_real import (
|
||||
)
|
||||
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.multidomain.urlreverse import get_domain
|
||||
from pretix.multidomain.urlreverse import (
|
||||
get_event_domain, get_organizer_domain,
|
||||
)
|
||||
|
||||
_supported = None
|
||||
|
||||
@@ -231,7 +233,10 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
dynamicdomain += " " + settings.SITE_URL
|
||||
|
||||
if hasattr(request, 'organizer') and request.organizer:
|
||||
domain = get_domain(request.organizer)
|
||||
if hasattr(request, 'event') and request.event:
|
||||
domain = get_event_domain(request.event, fallback=True)
|
||||
else:
|
||||
domain = get_organizer_domain(request.organizer)
|
||||
if domain:
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||
|
||||
@@ -6,7 +6,7 @@ import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
import pretix.base.validators
|
||||
from pretix.base.i18n import language
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.db import migrations, models
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
import pretix.base.models.auth
|
||||
import pretix.base.validators
|
||||
|
||||
20
src/pretix/base/migrations/0147_user_session_token.py
Normal file
20
src/pretix/base/migrations/0147_user_session_token.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 2.2.9 on 2020-03-21 15:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.auth
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0146_giftcardtransaction_text'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='session_token',
|
||||
field=models.CharField(default=pretix.base.models.auth.generate_session_token, max_length=32),
|
||||
),
|
||||
]
|
||||
24
src/pretix/base/migrations/0148_cancellationrequest.py
Normal file
24
src/pretix/base/migrations/0148_cancellationrequest.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.0.4 on 2020-03-25 10:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0147_user_session_token'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CancellationRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('cancellation_fee', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('refund_as_giftcard', models.BooleanField(default=False)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cancellation_requests', to='pretixbase.Order')),
|
||||
],
|
||||
),
|
||||
]
|
||||
46
src/pretix/base/migrations/0149_order_cancellation_date.py
Normal file
46
src/pretix/base/migrations/0149_order_cancellation_date.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.0.4 on 2020-03-25 14:40
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Count, OuterRef, Q, Subquery
|
||||
from django.utils.timezone import now
|
||||
|
||||
|
||||
def fill_cancellation_date(apps, schema_editor):
|
||||
Order = apps.get_model('pretixbase', 'Order')
|
||||
LogEntry = apps.get_model('pretixbase', 'LogEntry')
|
||||
OrderPosition = apps.get_model('pretixbase', 'OrderPosition')
|
||||
|
||||
s = OrderPosition.all.filter(
|
||||
order=OuterRef('pk'),
|
||||
canceled=False,
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
for o in Order.objects.annotate(
|
||||
pcnt=Subquery(s)
|
||||
).filter(
|
||||
Q(pcnt=0) | Q(pcnt__isnull=True) | Q(status="c")
|
||||
).values('id').iterator():
|
||||
le = LogEntry.objects.filter(
|
||||
content_type__model="order",
|
||||
object_id=o['id'],
|
||||
action_type='pretix.event.order.canceled'
|
||||
).order_by('-datetime').only('datetime').first()
|
||||
if le:
|
||||
Order.objects.filter(pk=o['id']).update(
|
||||
cancellation_date=le.datetime,
|
||||
last_modified=now()
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0148_cancellationrequest'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='cancellation_date',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(fill_cancellation_date, migrations.RunPython.noop)
|
||||
]
|
||||
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
74
src/pretix/base/migrations/0150_auto_20200401_1123.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# Generated by Django 3.0.4 on 2020-04-01 11:24
|
||||
|
||||
import django_countries.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0149_order_cancellation_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='city',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='company',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='state',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='street',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='zipcode',
|
||||
field=models.CharField(max_length=30, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='city',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='company',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(max_length=2, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='state',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='street',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='zipcode',
|
||||
field=models.CharField(max_length=30, null=True),
|
||||
),
|
||||
]
|
||||
@@ -12,9 +12,9 @@ from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
from django_scopes import scopes_disabled
|
||||
from u2flib_server.utils import (
|
||||
@@ -54,6 +54,10 @@ def generate_notifications_token():
|
||||
return get_random_string(length=32)
|
||||
|
||||
|
||||
def generate_session_token():
|
||||
return get_random_string(length=32)
|
||||
|
||||
|
||||
class SuperuserPermissionSet:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
@@ -110,6 +114,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
)
|
||||
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
||||
auth_backend = models.CharField(max_length=255, default='native')
|
||||
session_token = models.CharField(max_length=32, default=generate_session_token)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
@@ -382,6 +387,20 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
self._staff_session_cache[session_key] = sess
|
||||
return self._staff_session_cache[session_key]
|
||||
|
||||
def get_session_auth_hash(self):
|
||||
"""
|
||||
Return an HMAC that needs to
|
||||
"""
|
||||
key_salt = "pretix.base.models.User.get_session_auth_hash"
|
||||
payload = self.password
|
||||
payload += self.email
|
||||
payload += self.session_token
|
||||
return salted_hmac(key_salt, payload).hexdigest()
|
||||
|
||||
def update_session_token(self):
|
||||
self.session_token = generate_session_token()
|
||||
self.save(update_fields=['session_token'])
|
||||
|
||||
|
||||
class StaffSession(models.Model):
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
@@ -3,7 +3,7 @@ import string
|
||||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
@@ -15,9 +15,10 @@ from django.db import models
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
@@ -293,7 +294,7 @@ class Event(EventMixin, LoggedModel):
|
||||
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$",
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
|
||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||
),
|
||||
EventSlugBanlistValidator()
|
||||
@@ -370,6 +371,8 @@ class Event(EventMixin, LoggedModel):
|
||||
"""
|
||||
self.settings.invoice_renderer = 'modern1'
|
||||
self.settings.invoice_include_expire_date = True
|
||||
self.settings.ticketoutput_pdf__enabled = True
|
||||
self.settings.ticketoutput_passbook__enabled = True
|
||||
|
||||
@property
|
||||
def social_image(self):
|
||||
@@ -385,7 +388,7 @@ class Event(EventMixin, LoggedModel):
|
||||
if img:
|
||||
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
@@ -416,7 +419,7 @@ class Event(EventMixin, LoggedModel):
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
||||
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
@@ -622,8 +625,10 @@ class Event(EventMixin, LoggedModel):
|
||||
q.dependency_question = question_map[q.dependency_question_id]
|
||||
q.save(update_fields=['dependency_question'])
|
||||
|
||||
checkin_list_map = {}
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
||||
items = list(cl.limit_products.all())
|
||||
checkin_list_map[cl.pk] = cl
|
||||
cl.pk = None
|
||||
cl.event = self
|
||||
cl.save()
|
||||
@@ -678,7 +683,7 @@ class Event(EventMixin, LoggedModel):
|
||||
event_copy_data.send(
|
||||
sender=self, other=other,
|
||||
tax_map=tax_map, category_map=category_map, item_map=item_map, variation_map=variation_map,
|
||||
question_map=question_map
|
||||
question_map=question_map, checkin_list_map=checkin_list_map
|
||||
)
|
||||
|
||||
def get_payment_providers(self, cached=False) -> dict:
|
||||
@@ -1030,9 +1035,13 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
ordering = ("date_from", "name")
|
||||
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
||||
return '{} - {} {}'.format(
|
||||
self.name,
|
||||
self.get_date_range_display(),
|
||||
date_format(self.date_from.astimezone(self.timezone), "TIME_FORMAT") if self.settings.show_times else ""
|
||||
).strip()
|
||||
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
@@ -1066,7 +1075,7 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
||||
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.banlist import banned
|
||||
from pretix.base.models import LoggedModel
|
||||
@@ -83,6 +83,7 @@ class GiftCard(LoggedModel):
|
||||
|
||||
class Meta:
|
||||
unique_together = (('secret', 'issuer'),)
|
||||
ordering = ("issuance",)
|
||||
|
||||
|
||||
class GiftCardTransaction(models.Model):
|
||||
|
||||
@@ -157,9 +157,12 @@ class Invoice(models.Model):
|
||||
state_name = self.invoice_to_state
|
||||
if str(self.invoice_to_country) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.invoice_to_country)][1] == 'long':
|
||||
state_name = pycountry.subdivisions.get(
|
||||
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
|
||||
).name
|
||||
try:
|
||||
state_name = pycountry.subdivisions.get(
|
||||
code='{}-{}'.format(self.invoice_to_country, self.invoice_to_state)
|
||||
).name
|
||||
except:
|
||||
pass
|
||||
|
||||
parts = [
|
||||
self.invoice_to_company,
|
||||
|
||||
@@ -16,7 +16,7 @@ from django.utils import formats
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import is_naive, make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import ScopedManager
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
@@ -455,7 +455,8 @@ class Item(LoggedModel):
|
||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
else:
|
||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
|
||||
t = self.tax_rule.tax(price, base_price_is=base_price_is,
|
||||
currency=currency or self.event.currency)
|
||||
|
||||
if include_bundled:
|
||||
for b in self.bundles.all():
|
||||
@@ -1116,10 +1117,13 @@ class Question(LoggedModel):
|
||||
return None
|
||||
|
||||
if self.type == Question.TYPE_CHOICE:
|
||||
try:
|
||||
return self.options.get(Q(pk=answer) | Q(identifier=answer))
|
||||
except:
|
||||
q = Q(identifier=answer)
|
||||
if isinstance(answer, int) or answer.isdigit():
|
||||
q |= Q(pk=answer)
|
||||
o = self.options.filter(q).first()
|
||||
if not o:
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
return o
|
||||
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
if isinstance(answer, str):
|
||||
l_ = list(self.options.filter(
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.signals import logentry_object_link
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class NotificationSetting(models.Model):
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import string
|
||||
from collections import Counter
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List, Union
|
||||
@@ -25,7 +26,7 @@ from django.utils.encoding import escape_uri_path
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import Country, CountryField
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
@@ -43,6 +44,7 @@ from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import order_gracefully_delete
|
||||
|
||||
from ...helpers.countries import CachedCountries
|
||||
from .base import LockModel, LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
||||
@@ -151,6 +153,9 @@ class Order(LockModel, LoggedModel):
|
||||
datetime = models.DateTimeField(
|
||||
verbose_name=_("Date"), db_index=True
|
||||
)
|
||||
cancellation_date = models.DateTimeField(
|
||||
null=True, blank=True
|
||||
)
|
||||
expires = models.DateTimeField(
|
||||
verbose_name=_("Expiration date")
|
||||
)
|
||||
@@ -469,6 +474,8 @@ class Order(LockModel, LoggedModel):
|
||||
"""
|
||||
from .checkin import Checkin
|
||||
|
||||
if self.cancellation_requests.exists():
|
||||
return False
|
||||
positions = list(
|
||||
self.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||
@@ -694,16 +701,19 @@ class Order(LockModel, LoggedModel):
|
||||
|
||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
||||
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False) -> Union[bool, str]:
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
|
||||
check_voucher_usage=False) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
||||
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
||||
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
||||
}
|
||||
now_dt = now_dt or now()
|
||||
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
|
||||
quota_cache = {}
|
||||
v_budget = {}
|
||||
v_usage = Counter()
|
||||
try:
|
||||
for i, op in enumerate(positions):
|
||||
if op.seat:
|
||||
@@ -722,6 +732,13 @@ class Order(LockModel, LoggedModel):
|
||||
))
|
||||
v_budget[op.voucher] -= disc
|
||||
|
||||
if op.voucher and check_voucher_usage:
|
||||
v_usage[op.voucher.pk] += 1
|
||||
if v_usage[op.voucher.pk] + op.voucher.redeemed > op.voucher.max_usages:
|
||||
raise Quota.QuotaExceededException(error_messages['voucher_usages'].format(
|
||||
voucher=op.voucher.code
|
||||
))
|
||||
|
||||
quotas = list(op.quotas)
|
||||
if len(quotas) == 0:
|
||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||
@@ -1049,6 +1066,13 @@ class AbstractPosition(models.Model):
|
||||
'Seat', null=True, blank=True, on_delete=models.PROTECT
|
||||
)
|
||||
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
|
||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
|
||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
|
||||
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
|
||||
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -2090,7 +2114,8 @@ class InvoiceAddress(models.Model):
|
||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
||||
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
||||
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
|
||||
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
|
||||
countries=CachedCountries)
|
||||
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
|
||||
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
||||
help_text=_('Only for business customers within the EU.'))
|
||||
@@ -2197,6 +2222,13 @@ class CachedCombinedTicket(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class CancellationRequest(models.Model):
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='cancellation_requests')
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
cancellation_fee = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
refund_as_giftcard = models.BooleanField(default=False)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedTicket)
|
||||
def cachedticket_delete(sender, instance, **kwargs):
|
||||
if instance.file:
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.validators import OrganizerSlugBanlistValidator
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, ugettext_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_countries.fields import CountryField
|
||||
from i18nfield.fields import I18nCharField
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db.models import F, OuterRef, Q, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.banlist import banned
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.models import Event, LogEntry
|
||||
from pretix.base.signals import register_notification_types
|
||||
@@ -223,6 +223,12 @@ def register_default_notification_types(sender, **kwargs):
|
||||
_('Order canceled'),
|
||||
_('Order {order.code} has been canceled.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.reactivated',
|
||||
_('Order reactivated'),
|
||||
_('Order {order.code} has been reactivated.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.expired',
|
||||
|
||||
@@ -325,7 +325,7 @@ class InvoiceAddressState(ImportColumn):
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('State')
|
||||
return _('Invoice address') + ': ' + pgettext('address', 'State')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
@@ -398,6 +398,99 @@ class AttendeeEmail(ImportColumn):
|
||||
position.attendee_email = value
|
||||
|
||||
|
||||
class AttendeeCompany(ImportColumn):
|
||||
identifier = 'attendee_company'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('Company')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.company = value or ''
|
||||
|
||||
|
||||
class AttendeeStreet(ImportColumn):
|
||||
identifier = 'attendee_street'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('Address')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.address = value or ''
|
||||
|
||||
|
||||
class AttendeeZip(ImportColumn):
|
||||
identifier = 'attendee_zipcode'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('ZIP code')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.zipcode = value or ''
|
||||
|
||||
|
||||
class AttendeeCity(ImportColumn):
|
||||
identifier = 'attendee_city'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('City')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.city = value or ''
|
||||
|
||||
|
||||
class AttendeeCountry(ImportColumn):
|
||||
identifier = 'attendee_country'
|
||||
default_value = None
|
||||
|
||||
@property
|
||||
def initial(self):
|
||||
return 'static:' + str(guess_country(self.event))
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('Country')
|
||||
|
||||
def static_choices(self):
|
||||
return list(countries)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and not Country(value).numeric:
|
||||
raise ValidationError(_("Please enter a valid country code."))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.country = value or ''
|
||||
|
||||
|
||||
class AttendeeState(ImportColumn):
|
||||
identifier = 'attendee_state'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee address') + ': ' + _('State')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
if previous_values.get('attendee_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(_("States are not supported for this country."))
|
||||
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('attendee_country')]
|
||||
match = [
|
||||
s for s in pycountry.subdivisions.get(country_code=previous_values.get('attendee_country'))
|
||||
if s.type in types and (s.code[3:] == value or s.name == value)
|
||||
]
|
||||
if len(match) == 0:
|
||||
raise ValidationError(_("Please enter a valid state."))
|
||||
return match[0].code[3:]
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.state = value or ''
|
||||
|
||||
|
||||
class Price(ImportColumn):
|
||||
identifier = 'price'
|
||||
verbose_name = gettext_lazy('Price')
|
||||
@@ -596,6 +689,12 @@ def get_all_columns(event):
|
||||
default.append(AttendeeNamePart(event, n, l))
|
||||
default += [
|
||||
AttendeeEmail(event),
|
||||
AttendeeCompany(event),
|
||||
AttendeeStreet(event),
|
||||
AttendeeZip(event),
|
||||
AttendeeCity(event),
|
||||
AttendeeCountry(event),
|
||||
AttendeeState(event),
|
||||
Price(event),
|
||||
Secret(event),
|
||||
Locale(event),
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries import Countries
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
@@ -29,6 +29,7 @@ from pretix.base.models import (
|
||||
OrderRefund, Quota,
|
||||
)
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
@@ -1106,8 +1107,16 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
return
|
||||
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
|
||||
|
||||
remainder = cart['total'] - gc.value
|
||||
if remainder >= Decimal('0.00'):
|
||||
total = sum(p.total for p in cart['positions'])
|
||||
# Recompute fees. Some plugins, e.g. pretix-servicefees, change their fee schedule if a gift card is
|
||||
# applied.
|
||||
fees = get_fees(
|
||||
self.event, request, total, cart['invoice_address'], cs.get('payment'),
|
||||
cart['raw']
|
||||
)
|
||||
total += sum([f.value for f in fees])
|
||||
remainder = total - gc.value
|
||||
if remainder > Decimal('0.00'):
|
||||
del cs['payment']
|
||||
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
|
||||
money_filter(remainder, self.event.currency)
|
||||
@@ -1210,6 +1219,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
)
|
||||
refund.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_code': gc.secret,
|
||||
'transaction_id': trans.pk,
|
||||
}
|
||||
refund.done()
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PyPDF2 import PdfFileReader
|
||||
from pytz import timezone
|
||||
from reportlab.graphics import renderPDF
|
||||
@@ -205,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"editor_sample": _("Sample city"),
|
||||
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
|
||||
}),
|
||||
("attendee_company", {
|
||||
"label": _("Attendee company"),
|
||||
"editor_sample": _("Sample company"),
|
||||
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
|
||||
}),
|
||||
("addons", {
|
||||
"label": _("List of Add-Ons"),
|
||||
"editor_sample": _("Addon 1\nAddon 2"),
|
||||
|
||||
@@ -7,7 +7,7 @@ from dateutil import parser
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
BASE_CHOICES = (
|
||||
@@ -324,7 +324,7 @@ class ModelRelativeDateTimeField(models.CharField):
|
||||
return value.to_string()
|
||||
return value
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return None
|
||||
return RelativeDateWrapper.from_string(value)
|
||||
|
||||
@@ -2,17 +2,15 @@ import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, IntegerField, OuterRef, Subquery, Sum,
|
||||
)
|
||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Event, InvoiceAddress, Order, OrderFee, OrderPosition, SubEvent, User,
|
||||
WaitingListEntry,
|
||||
Event, InvoiceAddress, Order, OrderFee, OrderPosition, OrderRefund,
|
||||
SubEvent, User, WaitingListEntry,
|
||||
)
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException, TolerantDict, mail
|
||||
@@ -85,10 +83,10 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
|
||||
keep_fee_percentage: str, keep_fees: bool,
|
||||
send: bool, send_subject: dict, send_message: dict,
|
||||
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
|
||||
send: bool=False, send_subject: dict=None, send_message: dict=None,
|
||||
send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={},
|
||||
user: int=None):
|
||||
user: int=None, refund_as_giftcard: bool=False):
|
||||
send_subject = LazyI18nString(send_subject)
|
||||
send_message = LazyI18nString(send_message)
|
||||
send_waitinglist_subject = LazyI18nString(send_waitinglist_subject)
|
||||
@@ -151,27 +149,30 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
||||
for o in orders_to_cancel.only('id', 'total'):
|
||||
try:
|
||||
fee = Decimal('0.00')
|
||||
fee_sum = Decimal('0.00')
|
||||
keep_fee_objects = []
|
||||
if keep_fees:
|
||||
fee += o.fees.filter(
|
||||
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
|
||||
OrderFee.FEE_TYPE_CANCELLATION)
|
||||
).aggregate(
|
||||
s=Sum('value')
|
||||
)['s'] or 0
|
||||
for f in o.fees.all():
|
||||
if f.fee_type in keep_fees:
|
||||
fee += f.value
|
||||
keep_fee_objects.append(f)
|
||||
fee_sum += f.value
|
||||
if keep_fee_percentage:
|
||||
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee)
|
||||
fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum)
|
||||
if keep_fee_fixed:
|
||||
fee += Decimal(keep_fee_fixed)
|
||||
fee = round_decimal(min(fee, o.payment_refund_sum), event.currency)
|
||||
|
||||
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee)
|
||||
_cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects)
|
||||
refund_amount = o.payment_refund_sum
|
||||
|
||||
if auto_refund:
|
||||
_try_auto_refund(o.pk)
|
||||
|
||||
if send:
|
||||
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
|
||||
try:
|
||||
if auto_refund:
|
||||
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard)
|
||||
finally:
|
||||
if send:
|
||||
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
|
||||
except LockTimeoutException:
|
||||
logger.exception("Could not cancel order")
|
||||
failed += 1
|
||||
@@ -212,7 +213,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
|
||||
refund_amount = o.payment_refund_sum - o.total
|
||||
|
||||
if auto_refund:
|
||||
_try_auto_refund(o.pk)
|
||||
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
|
||||
|
||||
if send:
|
||||
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.db import DatabaseError, transaction
|
||||
from django.db.models import Count, Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext as _
|
||||
from django.utils.translation import gettext as _, pgettext_lazy
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
@@ -213,7 +213,7 @@ class CartManager:
|
||||
has_variations=Count('variations'),
|
||||
).filter(
|
||||
id__in=[i for i in item_ids if i and i not in self._items_cache]
|
||||
)
|
||||
).order_by()
|
||||
})
|
||||
self._variations_cache.update({
|
||||
v.pk: v
|
||||
@@ -221,7 +221,7 @@ class CartManager:
|
||||
'quotas'
|
||||
).select_related('item', 'item__event').filter(
|
||||
id__in=[i for i in variation_ids if i and i not in self._variations_cache]
|
||||
)
|
||||
).order_by()
|
||||
})
|
||||
|
||||
def _check_max_cart_size(self):
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.db import transaction
|
||||
from django.db.models import Prefetch
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Any, Dict
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import override
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.translation import gettext
|
||||
|
||||
from pretix.base.i18n import LazyLocaleException, language
|
||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||
@@ -26,7 +26,7 @@ def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any])
|
||||
d = ex.render(form_data)
|
||||
if d is None:
|
||||
raise ExportError(
|
||||
ugettext('Your export did not contain any data.')
|
||||
gettext('Your export did not contain any data.')
|
||||
)
|
||||
file.filename, file.type, data = d
|
||||
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
from django.utils.translation import gettext as _, pgettext
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -20,7 +20,7 @@ from django.core.mail import (
|
||||
)
|
||||
from django.core.mail.message import SafeMIMEText
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
from django.utils.translation import gettext as _, pgettext
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -276,20 +276,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
cm = lambda: scopes_disabled() # noqa
|
||||
|
||||
with cm():
|
||||
if invoices:
|
||||
invoices = Invoice.objects.filter(pk__in=invoices)
|
||||
for inv in invoices:
|
||||
if inv.file:
|
||||
try:
|
||||
with language(inv.order.locale):
|
||||
email.attach(
|
||||
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
||||
inv.file.file.read(),
|
||||
'application/pdf'
|
||||
)
|
||||
except:
|
||||
logger.exception('Could not attach invoice to email')
|
||||
pass
|
||||
if event:
|
||||
if order:
|
||||
try:
|
||||
@@ -344,6 +330,21 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
|
||||
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
|
||||
|
||||
if invoices:
|
||||
invoices = Invoice.objects.filter(pk__in=invoices)
|
||||
for inv in invoices:
|
||||
if inv.file:
|
||||
try:
|
||||
with language(inv.order.locale):
|
||||
email.attach(
|
||||
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
||||
inv.file.file.read(),
|
||||
'application/pdf'
|
||||
)
|
||||
except:
|
||||
logger.exception('Could not attach invoice to email')
|
||||
pass
|
||||
|
||||
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
|
||||
|
||||
try:
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.db.transaction import get_connection
|
||||
from django.dispatch import receiver
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import OAuthApplication
|
||||
@@ -94,6 +94,54 @@ def mark_order_paid(*args, **kwargs):
|
||||
raise NotImplementedError("This method is no longer supported since pretix 1.17.")
|
||||
|
||||
|
||||
def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None):
|
||||
"""
|
||||
Reactivates a canceled order. If ``force`` is not set to ``True``, this will fail if there is not
|
||||
enough quota.
|
||||
"""
|
||||
if order.status != Order.STATUS_CANCELED:
|
||||
raise OrderError('The order was not canceled.')
|
||||
|
||||
with order.event.lock() as now_dt:
|
||||
is_available = force or order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True)
|
||||
if is_available is True:
|
||||
if order.payment_refund_sum >= order.total:
|
||||
order.status = Order.STATUS_PAID
|
||||
else:
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.cancellation_date = None
|
||||
order.set_expires(now(),
|
||||
order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
|
||||
with transaction.atomic():
|
||||
order.save(update_fields=['expires', 'status', 'cancellation_date'])
|
||||
order.log_action(
|
||||
'pretix.event.order.reactivated',
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'expires': order.expires,
|
||||
}
|
||||
)
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') + 1))
|
||||
|
||||
for gc in position.issued_gift_cards.all():
|
||||
gc = GiftCard.objects.select_for_update().get(pk=gc.pk)
|
||||
gc.transactions.create(value=position.price, order=order)
|
||||
break
|
||||
else:
|
||||
raise OrderError(is_available)
|
||||
|
||||
order_approved.send(order.event, order=order)
|
||||
if order.status == Order.STATUS_PAID:
|
||||
order_paid.send(order.event, order=order)
|
||||
|
||||
num_invoices = order.invoices.filter(is_cancellation=False).count()
|
||||
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
|
||||
generate_invoice(order)
|
||||
|
||||
|
||||
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
|
||||
"""
|
||||
Extends the deadline of an order. If the order is already expired, the quota will be checked to
|
||||
@@ -117,9 +165,10 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
|
||||
'state_change': was_expired
|
||||
}
|
||||
)
|
||||
|
||||
if was_expired:
|
||||
num_invoices = order.invoices.filter(is_cancellation=False).count()
|
||||
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices:
|
||||
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
|
||||
generate_invoice(order)
|
||||
|
||||
if order.status == Order.STATUS_PENDING:
|
||||
@@ -271,12 +320,13 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
|
||||
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
||||
cancellation_fee=None):
|
||||
cancellation_fee=None, keep_fees=None):
|
||||
"""
|
||||
Mark this order as canceled
|
||||
:param order: The order to change
|
||||
:param user: The user that performed the change
|
||||
"""
|
||||
# If new actions are added to this function, make sure to add the reverse operation to reactivate_order()
|
||||
with transaction.atomic():
|
||||
if isinstance(order, int):
|
||||
order = Order.objects.select_for_update().get(pk=order)
|
||||
@@ -318,31 +368,38 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
|
||||
position.canceled = True
|
||||
position.save(update_fields=['canceled'])
|
||||
new_fee = cancellation_fee
|
||||
for fee in order.fees.all():
|
||||
fee.canceled = True
|
||||
fee.save(update_fields=['canceled'])
|
||||
if keep_fees and fee in keep_fees:
|
||||
new_fee -= fee.value
|
||||
else:
|
||||
fee.canceled = True
|
||||
fee.save(update_fields=['canceled'])
|
||||
|
||||
f = OrderFee(
|
||||
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||
value=cancellation_fee,
|
||||
tax_rule=order.event.settings.tax_rate_default,
|
||||
order=order,
|
||||
)
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
if new_fee:
|
||||
f = OrderFee(
|
||||
fee_type=OrderFee.FEE_TYPE_CANCELLATION,
|
||||
value=new_fee,
|
||||
tax_rule=order.event.settings.tax_rate_default,
|
||||
order=order,
|
||||
)
|
||||
f._calculate_tax()
|
||||
f.save()
|
||||
|
||||
if order.payment_refund_sum < cancellation_fee:
|
||||
raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
|
||||
order.status = Order.STATUS_PAID
|
||||
order.total = f.value
|
||||
order.save(update_fields=['status', 'total'])
|
||||
order.total = cancellation_fee
|
||||
order.cancellation_date = now()
|
||||
order.save(update_fields=['status', 'cancellation_date', 'total'])
|
||||
|
||||
if i:
|
||||
invoices.append(generate_invoice(order))
|
||||
else:
|
||||
with order.event.lock():
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save(update_fields=['status'])
|
||||
order.cancellation_date = now()
|
||||
order.save(update_fields=['status', 'cancellation_date'])
|
||||
|
||||
for position in order.positions.all():
|
||||
if position.voucher:
|
||||
@@ -350,6 +407,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
|
||||
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device,
|
||||
data={'cancellation_fee': cancellation_fee})
|
||||
order.cancellation_requests.all().delete()
|
||||
|
||||
if send_mail:
|
||||
email_template = order.event.settings.mail_text_order_canceled
|
||||
@@ -633,7 +691,7 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
|
||||
total = sum([c.price for c in positions])
|
||||
|
||||
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
|
||||
meta_info=meta_info, positions=positions):
|
||||
meta_info=meta_info, positions=positions, gift_cards=gift_cards):
|
||||
if resp:
|
||||
fees += resp
|
||||
total += sum(f.value for f in fees)
|
||||
@@ -970,7 +1028,6 @@ def send_download_reminders(sender, **kwargs):
|
||||
F('event__date_from')
|
||||
)
|
||||
).filter(
|
||||
status=Order.STATUS_PAID,
|
||||
download_reminder_sent=False,
|
||||
datetime__lte=now() - timedelta(hours=2),
|
||||
first_date__gte=today,
|
||||
@@ -1000,6 +1057,22 @@ def send_download_reminders(sender, **kwargs):
|
||||
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
|
||||
continue
|
||||
|
||||
if not o.ticket_download_available:
|
||||
continue
|
||||
positions = o.positions.select_related('item')
|
||||
|
||||
if o.status != Order.STATUS_PAID:
|
||||
if o.status != Order.STATUS_PENDING or o.require_approval or not \
|
||||
o.event.settings.ticket_download_pending:
|
||||
continue
|
||||
send = False
|
||||
for p in positions:
|
||||
if p.generate_ticket:
|
||||
send = True
|
||||
break
|
||||
if not send:
|
||||
continue
|
||||
|
||||
with language(o.locale):
|
||||
o.download_reminder_sent = True
|
||||
o.save(update_fields=['download_reminder_sent'])
|
||||
@@ -1017,6 +1090,9 @@ def send_download_reminders(sender, **kwargs):
|
||||
|
||||
if event.settings.mail_send_download_reminder_attendee:
|
||||
for p in o.positions.all():
|
||||
if not p.generate_ticket:
|
||||
continue
|
||||
|
||||
if p.subevent_id:
|
||||
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
@@ -1148,6 +1224,27 @@ class OrderChangeManager:
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.SubeventOperation(position, subevent))
|
||||
|
||||
def change_item_and_subevent(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation],
|
||||
subevent: SubEvent):
|
||||
if (not variation and item.has_variations) or (variation and variation.item_id != item.pk):
|
||||
raise OrderError(self.error_messages['product_without_variation'])
|
||||
|
||||
price = get_price(item, variation, voucher=position.voucher, subevent=subevent,
|
||||
invoice_address=self._invoice_address)
|
||||
|
||||
if price is None: # NOQA
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
|
||||
new_quotas = (variation.quotas.filter(subevent=subevent)
|
||||
if variation else item.quotas.filter(subevent=subevent))
|
||||
if not new_quotas:
|
||||
raise OrderError(self.error_messages['quota_missing'])
|
||||
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._quotadiff.subtract(position.quotas)
|
||||
self._operations.append(self.ItemOperation(position, item, variation))
|
||||
self._operations.append(self.SubeventOperation(position, subevent))
|
||||
|
||||
def regenerate_secret(self, position: OrderPosition):
|
||||
self._operations.append(self.RegenerateSecretOperation(position))
|
||||
|
||||
@@ -1816,7 +1913,8 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
|
||||
raise OrderError(str(error_messages['busy']))
|
||||
|
||||
|
||||
def _try_auto_refund(order):
|
||||
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
|
||||
refund_as_giftcard=False):
|
||||
notify_admin = False
|
||||
error = False
|
||||
if isinstance(order, int):
|
||||
@@ -1825,14 +1923,55 @@ def _try_auto_refund(order):
|
||||
if refund_amount <= Decimal('0.00'):
|
||||
return
|
||||
|
||||
proposals = order.propose_auto_refunds(refund_amount)
|
||||
can_auto_refund = sum(proposals.values()) == refund_amount
|
||||
if refund_as_giftcard:
|
||||
proposals = {}
|
||||
can_auto_refund = True
|
||||
can_auto_refund_sum = refund_amount
|
||||
with transaction.atomic():
|
||||
giftcard = order.event.organizer.issued_gift_cards.create(
|
||||
currency=order.event.currency,
|
||||
testmode=order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', data={})
|
||||
r = order.refunds.create(
|
||||
order=order,
|
||||
payment=None,
|
||||
source=source,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
execution_date=now(),
|
||||
amount=can_auto_refund_sum,
|
||||
provider='giftcard',
|
||||
info=json.dumps({
|
||||
'gift_card': giftcard.pk
|
||||
})
|
||||
)
|
||||
try:
|
||||
r.payment_provider.execute_refund(r)
|
||||
except PaymentException as e:
|
||||
with transaction.atomic():
|
||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||
r.save()
|
||||
order.log_action('pretix.event.order.refund.failed', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
'error': str(e)
|
||||
})
|
||||
error = True
|
||||
notify_admin = True
|
||||
else:
|
||||
if r.state != OrderRefund.REFUND_STATE_DONE:
|
||||
notify_admin = True
|
||||
|
||||
else:
|
||||
proposals = order.propose_auto_refunds(refund_amount)
|
||||
can_auto_refund_sum = sum(proposals.values())
|
||||
can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount
|
||||
if can_auto_refund:
|
||||
for p, value in proposals.items():
|
||||
with transaction.atomic():
|
||||
r = order.refunds.create(
|
||||
payment=p,
|
||||
source=OrderRefund.REFUND_SOURCE_BUYER,
|
||||
source=source,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=value,
|
||||
provider=p.provider
|
||||
@@ -1856,10 +1995,24 @@ def _try_auto_refund(order):
|
||||
error = True
|
||||
notify_admin = True
|
||||
else:
|
||||
if r.state != OrderRefund.REFUND_STATE_DONE:
|
||||
if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE):
|
||||
notify_admin = True
|
||||
elif refund_amount != Decimal('0.00'):
|
||||
notify_admin = True
|
||||
|
||||
if refund_amount - can_auto_refund_sum > Decimal('0.00'):
|
||||
if manual_refund:
|
||||
with transaction.atomic():
|
||||
r = order.refunds.create(
|
||||
source=source,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=refund_amount - can_auto_refund_sum,
|
||||
provider='manual'
|
||||
)
|
||||
order.log_action('pretix.event.order.refund.created', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
})
|
||||
else:
|
||||
notify_admin = True
|
||||
|
||||
if notify_admin:
|
||||
order.log_action('pretix.event.order.refund.requested')
|
||||
@@ -1874,13 +2027,13 @@ def _try_auto_refund(order):
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
@scopes_disabled()
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||
device=None, cancellation_fee=None, try_auto_refund=False):
|
||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False):
|
||||
try:
|
||||
try:
|
||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||
cancellation_fee)
|
||||
if try_auto_refund:
|
||||
_try_auto_refund(order)
|
||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard)
|
||||
return ret
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.db.models import Count, Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.i18n import LazyLocaleException
|
||||
from pretix.base.models import CartPosition, Seat
|
||||
@@ -25,7 +25,7 @@ def validate_plan_change(event, subevent, plan):
|
||||
subevent=subevent,
|
||||
).filter(
|
||||
Q(has_v=True) | Q(has_op=True)
|
||||
).values_list('seat_guid', flat=True)
|
||||
).values_list('seat_guid', flat=True).order_by()
|
||||
)
|
||||
new_seats = {
|
||||
ss.guid for ss in plan.iter_all_seats()
|
||||
@@ -40,7 +40,7 @@ def generate_seats(event, subevent, plan, mapping):
|
||||
current_seats = {}
|
||||
for s in event.seats.select_related('product').annotate(
|
||||
has_op=Count('orderposition'), has_v=Count('vouchers')
|
||||
).filter(subevent=subevent):
|
||||
).filter(subevent=subevent).order_by():
|
||||
if s.seat_guid in current_seats:
|
||||
s.delete() # Duplicates should not exist
|
||||
else:
|
||||
|
||||
@@ -8,7 +8,7 @@ from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import CachedFile, Event, cachedfile_name
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.db.models import (
|
||||
Case, Count, DateTimeField, F, Max, OuterRef, Subquery, Sum, Value, When,
|
||||
)
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
|
||||
from pretix.base.models.event import SubEvent
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.i18n import language
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import timedelta
|
||||
import requests
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext_noop
|
||||
from django.utils.translation import gettext_lazy as _, gettext_noop
|
||||
from django_scopes import scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -91,7 +91,7 @@ def send_update_notification_email():
|
||||
gs.settings.update_check_email,
|
||||
_('pretix update available'),
|
||||
LazyI18nString.from_gettext(
|
||||
ugettext_noop(
|
||||
gettext_noop(
|
||||
'Hi!\n\nAn update is available for pretix or for one of the plugins you installed in your '
|
||||
'pretix installation. Please click on the following link for more information:\n\n {url} \n\n'
|
||||
'You can always find information on the latest updates on the pretix.eu blog:\n\n'
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.core.files import File
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db.models import Model
|
||||
from django.utils.translation import (
|
||||
pgettext, pgettext_lazy, ugettext_lazy as _, ugettext_noop,
|
||||
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
|
||||
)
|
||||
from django_countries import countries
|
||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||
@@ -19,6 +19,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.fields import ListMultipleChoiceField
|
||||
from pretix.api.serializers.i18n import I18nField
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.reldate import (
|
||||
@@ -104,6 +105,44 @@ DEFAULTS = {
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
|
||||
)
|
||||
},
|
||||
'attendee_company_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for company per ticket"),
|
||||
)
|
||||
},
|
||||
'attendee_company_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require company per ticket"),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_company_asked'}),
|
||||
)
|
||||
},
|
||||
'attendee_addresses_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for postal addresses per ticket"),
|
||||
)
|
||||
},
|
||||
'attendee_addresses_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require postal addresses per ticket"),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_addresses_asked'}),
|
||||
)
|
||||
},
|
||||
'order_email_asked_twice': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -365,7 +404,7 @@ DEFAULTS = {
|
||||
'type': RelativeDateWrapper,
|
||||
'form_class': RelativeDateField,
|
||||
'serializer_class': SerializerRelativeDateField,
|
||||
'form_kawrgs': dict(
|
||||
'form_kwargs': dict(
|
||||
label=_('Last date of payments'),
|
||||
help_text=_("The last date any payments are accepted. This has precedence over the number of "
|
||||
"days configured above. If you use the event series feature and an order contains tickets for "
|
||||
@@ -639,7 +678,7 @@ DEFAULTS = {
|
||||
'locales': {
|
||||
'default': json.dumps([settings.LANGUAGE_CODE]),
|
||||
'type': list,
|
||||
'serializer_class': serializers.MultipleChoiceField,
|
||||
'serializer_class': ListMultipleChoiceField,
|
||||
'serializer_kwargs': dict(
|
||||
choices=settings.LANGUAGES,
|
||||
required=True,
|
||||
@@ -668,6 +707,17 @@ DEFAULTS = {
|
||||
label=_("Default language"),
|
||||
)
|
||||
},
|
||||
'show_dates_on_frontpage': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_class': forms.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Show event times and dates on the ticket shop"),
|
||||
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
|
||||
"does however not affect the display in other locations."),
|
||||
)
|
||||
},
|
||||
'show_date_to': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
@@ -770,8 +820,8 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_class': forms.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Use feature"),
|
||||
help_text=_("Use pretix to generate tickets for the user to download and print out."),
|
||||
label=_("Allow users to download tickets"),
|
||||
help_text=_("If this is off, nobody can download a ticket."),
|
||||
)
|
||||
},
|
||||
'ticket_download_date': {
|
||||
@@ -792,7 +842,11 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_class': forms.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Offer to download tickets separately for add-on products"),
|
||||
label=_("Generate tickets for add-on products"),
|
||||
help_text=_('By default, tickets are only issued for products selected individually, not for add-on '
|
||||
'products. With this option, a separate ticket is issued for every add-on product as well.'),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
|
||||
'data-checkbox-dependency-visual': 'on'}),
|
||||
)
|
||||
},
|
||||
'ticket_download_nonadm': {
|
||||
@@ -801,7 +855,11 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_class': forms.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Generate tickets for non-admission products"),
|
||||
label=_("Generate tickets for all products"),
|
||||
help_text=_('If turned off, tickets are only issued for products that are marked as an "admission ticket"'
|
||||
'in the product settings. You can also turn off tickt issuing in every product separately.'),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
|
||||
'data-checkbox-dependency-visual': 'on'}),
|
||||
)
|
||||
},
|
||||
'ticket_download_pending': {
|
||||
@@ -810,7 +868,10 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_class': forms.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Offer to download tickets even before an order is paid"),
|
||||
label=_("Generate tickets for pending orders"),
|
||||
help_text=_('If turned off, ticket downloads are only possible after an order has been marked as paid.'),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
|
||||
'data-checkbox-dependency-visual': 'on'}),
|
||||
)
|
||||
},
|
||||
'event_list_availability': {
|
||||
@@ -895,6 +956,66 @@ DEFAULTS = {
|
||||
label=_("Keep a percentual cancellation fee"),
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_adjust_fees': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow customers to voluntarily choose a lower refund"),
|
||||
help_text=_("With this option enabled, your customers can choose to get a smaller refund to support you.")
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_adjust_fees_explanation': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop(
|
||||
'However, if you want us to help keep the lights on here, please consider using the slider below to '
|
||||
'request a smaller refund. Thank you!'
|
||||
)),
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Voluntary lower refund explanation"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown in between the explanation of how the refunds work and the slider "
|
||||
"which your customers can use to choose the amount they would like to receive. You can use it "
|
||||
"e.g. to explain choosing a lower refund will help your organisation.")
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_require_approval': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Customers can only request a cancellation that needs to be approved by the event organizer "
|
||||
"before the order is canceled and a refund is issued."),
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_refund_as_giftcard': {
|
||||
'default': 'off',
|
||||
'type': str,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': dict(
|
||||
choices=[
|
||||
('off', _('All refunds are issued to the original payment method')),
|
||||
('option', _('Customers can choose between a gift card and a refund to their payment method')),
|
||||
('force', _('All refunds are issued as gift cards')),
|
||||
],
|
||||
),
|
||||
'form_class': forms.ChoiceField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Refund method'),
|
||||
choices=[
|
||||
('off', _('All refunds are issued to the original payment method')),
|
||||
('option', _('Customers can choose between a gift card and a refund to their payment method')),
|
||||
('force', _('All refunds are issued as gift cards')),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
# When adding a new ordering, remember to also define it in the event model
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_until': {
|
||||
'default': None,
|
||||
'type': RelativeDateWrapper,
|
||||
@@ -994,7 +1115,7 @@ DEFAULTS = {
|
||||
},
|
||||
'mail_text_resend_link': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
you receive this message because you asked us to send you the link
|
||||
to your order for {event}.
|
||||
@@ -1007,7 +1128,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_resend_all_links': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
somebody requested a list of your orders for {event}.
|
||||
The list is as follows:
|
||||
@@ -1019,7 +1140,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_free_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||
|
||||
you have been registered for {event} successfully.
|
||||
|
||||
@@ -1031,7 +1152,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_free': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
your order for {event} was successful. As you only ordered free products,
|
||||
no payment is required.
|
||||
@@ -1048,7 +1169,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_placed_require_approval': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
we successfully received your order for {event}. Since you ordered
|
||||
a product that requires approval by the event organizer, we ask you to
|
||||
@@ -1062,7 +1183,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_placed': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
we successfully received your order for {event} with a total value
|
||||
of {total_with_currency}. Please complete your payment before {expire_date}.
|
||||
@@ -1081,7 +1202,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_placed_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||
|
||||
a ticket for {event} has been ordered for you.
|
||||
|
||||
@@ -1093,7 +1214,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_changed': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
your order for {event} has been changed.
|
||||
|
||||
@@ -1105,7 +1226,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_paid': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
we successfully received your payment for {event}. Thank you!
|
||||
|
||||
@@ -1123,7 +1244,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_paid_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||
|
||||
a ticket for {event} that has been ordered for you is now paid.
|
||||
|
||||
@@ -1139,7 +1260,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_expire_warning': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
we did not yet receive a full payment for your order for {event}.
|
||||
Please keep in mind that we only guarantee your order if we receive
|
||||
@@ -1153,7 +1274,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_waiting_list': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
you submitted yourself to the waiting list for {event},
|
||||
for the product {product}.
|
||||
@@ -1176,7 +1297,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_canceled': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
your order {code} for {event} has been canceled.
|
||||
|
||||
@@ -1188,7 +1309,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_approved': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
we approved your order for {event} and will be happy to welcome you
|
||||
at our event.
|
||||
@@ -1204,7 +1325,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_denied': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
unfortunately, we denied your order request for {event}.
|
||||
|
||||
@@ -1219,7 +1340,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_order_custom_mail': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
@@ -1237,7 +1358,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_download_reminder_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {attendee_name},
|
||||
|
||||
you are registered for {event}.
|
||||
|
||||
@@ -1249,7 +1370,7 @@ Your {event} team"""))
|
||||
},
|
||||
'mail_text_download_reminder': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
you bought a ticket for {event}.
|
||||
|
||||
@@ -1365,6 +1486,32 @@ Your {event} team"""))
|
||||
widget=I18nTextarea
|
||||
)
|
||||
},
|
||||
'banner_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Banner text (top)"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown above every page of your shop. Please only use this for "
|
||||
"very important messages.")
|
||||
)
|
||||
},
|
||||
'banner_text_bottom': {
|
||||
'default': '',
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Banner text (bottom)"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown below every page of your shop. Please only use this for "
|
||||
"very important messages.")
|
||||
)
|
||||
},
|
||||
'voucher_explanation_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString,
|
||||
@@ -1379,7 +1526,7 @@ Your {event} team"""))
|
||||
)
|
||||
},
|
||||
'checkout_email_helptext': {
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop(
|
||||
'default': LazyI18nString.from_gettext(gettext_noop(
|
||||
'Make sure to enter a valid email address. We will send you an order '
|
||||
'confirmation including a link that you need to access your order later.'
|
||||
)),
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.db.models import Max, Q
|
||||
from django.db.models.functions import Greatest
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerSerializer, InvoiceAddressSerializer,
|
||||
@@ -194,15 +194,23 @@ class WaitingListShredder(BaseDataShredder):
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
|
||||
class AttendeeNameShredder(BaseDataShredder):
|
||||
verbose_name = _('Attendee names')
|
||||
identifier = 'attendee_names'
|
||||
description = _('This will remove all attendee names from order positions, as well as logged changes to them.')
|
||||
class AttendeeInfoShredder(BaseDataShredder):
|
||||
verbose_name = _('Attendee info')
|
||||
identifier = 'attendee_info'
|
||||
description = _('This will remove all attendee names and postal addresses from order positions, as well as logged '
|
||||
'changes to them.')
|
||||
|
||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||
yield 'attendee-names.json', 'application/json', json.dumps({
|
||||
'{}-{}'.format(op.order.code, op.positionid): op.attendee_name
|
||||
for op in OrderPosition.all.filter(
|
||||
yield 'attendee-info.json', 'application/json', json.dumps({
|
||||
'{}-{}'.format(op.order.code, op.positionid): {
|
||||
'name': op.attendee_name,
|
||||
'company': op.company,
|
||||
'street': op.street,
|
||||
'zipcode': op.zipcode,
|
||||
'city': op.city,
|
||||
'country': str(op.country) if op.country else None,
|
||||
'state': op.state
|
||||
} for op in OrderPosition.all.filter(
|
||||
order__event=self.event
|
||||
).filter(
|
||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
||||
@@ -214,8 +222,10 @@ class AttendeeNameShredder(BaseDataShredder):
|
||||
OrderPosition.all.filter(
|
||||
order__event=self.event
|
||||
).filter(
|
||||
Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False))
|
||||
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True})
|
||||
Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) |
|
||||
Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False)
|
||||
).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None,
|
||||
zipcode=None, city=None)
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
@@ -227,6 +237,14 @@ class AttendeeNameShredder(BaseDataShredder):
|
||||
d['data'][i]['attendee_name_parts'] = {
|
||||
'_legacy': '█'
|
||||
}
|
||||
if 'company' in row:
|
||||
d['data'][i]['company'] = '█'
|
||||
if 'street' in row:
|
||||
d['data'][i]['street'] = '█'
|
||||
if 'zipcode' in row:
|
||||
d['data'][i]['zipcode'] = '█'
|
||||
if 'city' in row:
|
||||
d['data'][i]['city'] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
@@ -357,7 +375,7 @@ class PaymentInfoShredder(BaseDataShredder):
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
return [
|
||||
EmailAddressShredder,
|
||||
AttendeeNameShredder,
|
||||
AttendeeInfoShredder,
|
||||
InvoiceAddressShredder,
|
||||
QuestionAnswerShredder,
|
||||
InvoiceShredder,
|
||||
|
||||
@@ -341,6 +341,16 @@ as the first argument.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_reactivated = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out every time a canceled order is reactivated. The order object is given
|
||||
as the first argument.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
order_expired = EventPluginSignal(
|
||||
providing_args=["order"]
|
||||
)
|
||||
@@ -472,7 +482,7 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
|
||||
"""
|
||||
|
||||
event_copy_data = EventPluginSignal(
|
||||
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map"]
|
||||
providing_args=["other", "tax_map", "category_map", "item_map", "question_map", "variation_map", "checkin_list_map"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out when a new event is created as a clone of an existing event, i.e.
|
||||
@@ -484,9 +494,9 @@ but you might need to modify that data.
|
||||
|
||||
The ``sender`` keyword argument will contain the event of the **new** event. The ``other``
|
||||
keyword argument will contain the event to **copy from**. The keyword arguments
|
||||
``tax_map``, ``category_map``, ``item_map``, ``question_map``, and ``variation_map`` contain
|
||||
mappings from object IDs in the original event to objects in the new event of the respective
|
||||
types.
|
||||
``tax_map``, ``category_map``, ``item_map``, ``question_map``, ``variation_map`` and
|
||||
``checkin_list_map`` contain mappings from object IDs in the original event to objects
|
||||
in the new event of the respective types.
|
||||
"""
|
||||
|
||||
item_copy_data = EventPluginSignal(
|
||||
@@ -517,7 +527,7 @@ an OrderedDict of (setting name, form field).
|
||||
"""
|
||||
|
||||
order_fee_calculation = EventPluginSignal(
|
||||
providing_args=['positions', 'invoice_address', 'meta_info', 'total']
|
||||
providing_args=['positions', 'invoice_address', 'meta_info', 'total', 'gift_cards']
|
||||
)
|
||||
"""
|
||||
This signals allows you to add fees to an order while it is being created. You are expected to
|
||||
@@ -528,7 +538,8 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
||||
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
|
||||
tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total``
|
||||
keyword argument will contain the total cart sum without any fees. You should not rely on this
|
||||
``total`` value for fee calculations as other fees might interfere.
|
||||
``total`` value for fee calculations as other fees might interfere. The ``gift_cards`` argument lists
|
||||
the gift cards in use.
|
||||
"""
|
||||
|
||||
order_fee_type_name = EventPluginSignal(
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load rich_text %}
|
||||
{% if widget.wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.wrap_label %} {{ widget.label|rich_text_snippet }}</label>{% endif %}
|
||||
@@ -7,7 +7,7 @@ from django import template
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
register = template.Library()
|
||||
@@ -66,7 +66,7 @@ ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not is_safe_url(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
attrs[None, 'target'] = '_blank'
|
||||
|
||||
@@ -6,7 +6,7 @@ from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, Order, OrderPosition
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
@@ -142,7 +142,7 @@ class BaseTicketOutput:
|
||||
return OrderedDict([
|
||||
('_enabled',
|
||||
forms.BooleanField(
|
||||
label=_('Enable output'),
|
||||
label=_('Enable ticket format'),
|
||||
required=False,
|
||||
)),
|
||||
])
|
||||
|
||||
@@ -133,7 +133,7 @@ def timeline_for_event(event, subevent=None):
|
||||
|
||||
if not event.has_subevents:
|
||||
days = event.settings.get('mail_days_download_reminder', as_type=int)
|
||||
if days is not None:
|
||||
if days is not None and event.settings.ticket_download:
|
||||
reminder_date = (ev.date_from - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class BanlistValidator:
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.middleware.csrf import REASON_NO_CSRF_COOKIE, REASON_NO_REFERER
|
||||
from django.template import TemplateDoesNotExist, loader
|
||||
from django.template.loader import get_template
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.csrf import requires_csrf_token
|
||||
from sentry_sdk import last_event_id
|
||||
|
||||
|
||||
@@ -85,10 +85,20 @@ class BaseQuestionsViewMixin:
|
||||
for k, v in form.cleaned_data.items():
|
||||
if k == 'attendee_name_parts':
|
||||
form.pos.attendee_name_parts = v if v else None
|
||||
form.pos.save()
|
||||
elif k == 'attendee_email':
|
||||
form.pos.attendee_email = v if v != '' else None
|
||||
form.pos.save()
|
||||
elif k == 'company':
|
||||
form.pos.company = v if v != '' else None
|
||||
elif k == 'street':
|
||||
form.pos.street = v if v != '' else None
|
||||
elif k == 'zipcode':
|
||||
form.pos.zipcode = v if v != '' else None
|
||||
elif k == 'city':
|
||||
form.pos.city = v if v != '' else None
|
||||
elif k == 'country':
|
||||
form.pos.country = v if v != '' else None
|
||||
elif k == 'state':
|
||||
form.pos.state = v if v != '' else None
|
||||
elif k.startswith('question_'):
|
||||
field = form.fields[k]
|
||||
if hasattr(field, 'answer'):
|
||||
@@ -119,7 +129,7 @@ class BaseQuestionsViewMixin:
|
||||
meta_info['question_form_data'][k] = v
|
||||
|
||||
form.pos.meta_info = json.dumps(meta_info)
|
||||
form.pos.save(update_fields=['meta_info'])
|
||||
form.pos.save()
|
||||
return not failed
|
||||
|
||||
def _save_to_answer(self, field, answer, value):
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from pretix.celery_app import app
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from pretix.control.navigation import (
|
||||
from ..helpers.i18n import (
|
||||
get_javascript_format, get_javascript_output_format, get_moment_locale,
|
||||
)
|
||||
from ..multidomain.urlreverse import get_event_domain
|
||||
from .signals import html_head, nav_topbar
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
@@ -25,6 +26,12 @@ def contextprocessor(request):
|
||||
"""
|
||||
Adds data to all template contexts
|
||||
"""
|
||||
if not hasattr(request, '_pretix_control_default_context'):
|
||||
request._pretix_control_default_context = _default_context(request)
|
||||
return request._pretix_control_default_context
|
||||
|
||||
|
||||
def _default_context(request):
|
||||
try:
|
||||
url = resolve(request.path_info)
|
||||
except Resolver404:
|
||||
@@ -51,7 +58,7 @@ def contextprocessor(request):
|
||||
if request.event.settings.get('payment_term_weekdays'):
|
||||
_js_payment_weekdays_disabled = '[0,6]'
|
||||
|
||||
ctx['has_domain'] = request.event.organizer.domains.exists()
|
||||
ctx['has_domain'] = get_event_domain(request.event, fallback=True) is not None
|
||||
|
||||
if not request.event.testmode:
|
||||
with scope(organizer=request.organizer):
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.forms.utils import from_current_timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ...base.forms import I18nModelForm
|
||||
# Import for backwards compatibility with okd import paths
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -10,7 +10,7 @@ from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries import Countries
|
||||
from django_countries.fields import LazyTypedChoiceField
|
||||
from i18nfield.forms import (
|
||||
@@ -32,6 +32,7 @@ from pretix.control.forms import (
|
||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.banktransfer.payment import BankTransfer
|
||||
from pretix.presale.style import get_fonts
|
||||
@@ -291,6 +292,15 @@ class EventUpdateForm(I18nModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.change_slug = kwargs.pop('change_slug', False)
|
||||
self.domain = kwargs.pop('domain', False)
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
self.instance = kwargs['instance']
|
||||
if self.domain and self.instance:
|
||||
initial_domain = self.instance.domains.first()
|
||||
if initial_domain:
|
||||
kwargs['initial'].setdefault('domain', initial_domain.domainname)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.change_slug:
|
||||
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
|
||||
@@ -298,6 +308,47 @@ class EventUpdateForm(I18nModelForm):
|
||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||
'Sample Conference Center\nHeidelberg, Germany'
|
||||
)
|
||||
if self.domain:
|
||||
self.fields['domain'] = forms.CharField(
|
||||
max_length=255,
|
||||
label=_('Custom domain'),
|
||||
required=False,
|
||||
help_text=_('You need to configure the custom domain in the webserver beforehand.')
|
||||
)
|
||||
|
||||
def clean_domain(self):
|
||||
d = self.cleaned_data['domain']
|
||||
if d:
|
||||
if d == urlparse(settings.SITE_URL).hostname:
|
||||
raise ValidationError(
|
||||
_('You cannot choose the base domain of this installation.')
|
||||
)
|
||||
if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists():
|
||||
raise ValidationError(
|
||||
_('This domain is already in use for a different event or organizer.')
|
||||
)
|
||||
return d
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit)
|
||||
|
||||
if self.domain:
|
||||
current_domain = instance.domains.first()
|
||||
if self.cleaned_data['domain']:
|
||||
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
|
||||
current_domain.delete()
|
||||
KnownDomain.objects.create(
|
||||
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
|
||||
)
|
||||
elif not current_domain:
|
||||
KnownDomain.objects.create(
|
||||
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
|
||||
)
|
||||
elif current_domain:
|
||||
current_domain.delete()
|
||||
instance.cache.clear()
|
||||
|
||||
return instance
|
||||
|
||||
def clean_slug(self):
|
||||
if self.change_slug:
|
||||
@@ -439,6 +490,7 @@ class EventSettingsForm(SettingsForm):
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'voucher_explanation_text',
|
||||
'show_dates_on_frontpage',
|
||||
'show_date_to',
|
||||
'show_times',
|
||||
'show_items_outside_presale_period',
|
||||
@@ -463,7 +515,13 @@ class EventSettingsForm(SettingsForm):
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
'attendee_emails_required',
|
||||
'attendee_company_asked',
|
||||
'attendee_company_required',
|
||||
'attendee_addresses_asked',
|
||||
'attendee_addresses_required',
|
||||
'confirm_text',
|
||||
'banner_text',
|
||||
'banner_text_bottom',
|
||||
'order_email_asked_twice',
|
||||
'last_order_modification_date',
|
||||
]
|
||||
@@ -513,6 +571,10 @@ class CancelSettingsForm(SettingsForm):
|
||||
'cancel_allow_user_paid_keep',
|
||||
'cancel_allow_user_paid_keep_fees',
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
]
|
||||
|
||||
|
||||
@@ -1188,7 +1250,7 @@ class QuickSetupForm(I18nForm):
|
||||
|
||||
class QuickSetupProductForm(I18nForm):
|
||||
name = I18nFormField(
|
||||
max_length=255,
|
||||
max_length=200, # Max length of Quota.name
|
||||
label=_("Product name"),
|
||||
widget=I18nTextInput
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db.models.functions import Coalesce, ExtractWeekDay
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.forms.widgets import DatePickerWidget
|
||||
from pretix.base.models import (
|
||||
@@ -220,6 +220,7 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
('underpaid', _('Underpaid')),
|
||||
('pendingpaid', _('Pending (but fully paid)')),
|
||||
('testmode', _('Test mode')),
|
||||
('rc', _('Cancellation requested')),
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
@@ -305,6 +306,10 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
|
||||
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
|
||||
)
|
||||
elif fdata.get('status') == 'rc':
|
||||
qs = qs.filter(
|
||||
cancellation_requests__isnull=False
|
||||
)
|
||||
elif fdata.get('status') == 'pendingpaid':
|
||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
||||
qs = qs.filter(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
|
||||
from pretix.base.forms import SettingsForm
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.db.models import Max
|
||||
from django.forms.formsets import DELETION_FIELD_NAME
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import (
|
||||
pgettext_lazy, ugettext as __, ugettext_lazy as _,
|
||||
gettext as __, gettext_lazy as _, pgettext_lazy,
|
||||
)
|
||||
from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
@@ -303,6 +303,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
self.instance.allow_cancel = self.cleaned_data['copy_from'].allow_cancel
|
||||
self.instance.min_per_order = self.cleaned_data['copy_from'].min_per_order
|
||||
self.instance.max_per_order = self.cleaned_data['copy_from'].max_per_order
|
||||
self.instance.generate_tickets = self.cleaned_data['copy_from'].generate_tickets
|
||||
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
|
||||
self.instance.free_price = self.cleaned_data['copy_from'].free_price
|
||||
self.instance.original_price = self.cleaned_data['copy_from'].original_price
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import (
|
||||
gettext_noop, pgettext_lazy, ugettext_lazy as _,
|
||||
gettext_lazy as _, gettext_noop, pgettext_lazy,
|
||||
)
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
@@ -16,7 +16,9 @@ from i18nfield.strings import LazyI18nString
|
||||
from pretix.base.email import get_available_placeholders
|
||||
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, OrderFee, OrderPosition,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -239,6 +241,7 @@ class OrderPositionAddForm(forms.Form):
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.items = kwargs.pop('items')
|
||||
order = kwargs.pop('order')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -248,11 +251,13 @@ class OrderPositionAddForm(forms.Form):
|
||||
ia = None
|
||||
|
||||
choices = []
|
||||
for i in order.event.items.prefetch_related('variations').all():
|
||||
for i in self.items:
|
||||
pname = str(i)
|
||||
if not i.is_available():
|
||||
pname += ' ({})'.format(_('inactive'))
|
||||
variations = list(i.variations.all())
|
||||
if i.tax_rule: # performance optimization
|
||||
i.tax_rule.event = order.event
|
||||
if variations:
|
||||
for v in variations:
|
||||
p = get_price(i, v, invoice_address=ia)
|
||||
@@ -262,7 +267,11 @@ class OrderPositionAddForm(forms.Form):
|
||||
p = get_price(i, invoice_address=ia)
|
||||
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(order.event.currency))))
|
||||
self.fields['itemvar'].choices = choices
|
||||
if ItemAddOn.objects.filter(base_item__event=order.event).exists():
|
||||
if order.event.cache.get_or_set(
|
||||
'has_addon_products',
|
||||
default=lambda: ItemAddOn.objects.filter(base_item__event=order.event).exists(),
|
||||
timeout=300
|
||||
):
|
||||
self.fields['addon_to'].queryset = order.positions.filter(addon_to__isnull=True).select_related(
|
||||
'item', 'variation'
|
||||
)
|
||||
@@ -291,10 +300,12 @@ class OrderPositionAddForm(forms.Form):
|
||||
class OrderPositionAddFormset(forms.BaseFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.order = kwargs.pop('order', None)
|
||||
self.items = kwargs.pop('items')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['order'] = self.order
|
||||
kwargs['items'] = self.items
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
@@ -305,6 +316,7 @@ class OrderPositionAddFormset(forms.BaseFormSet):
|
||||
empty_permitted=True,
|
||||
use_required_attribute=False,
|
||||
order=self.order,
|
||||
items=self.items,
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
@@ -344,6 +356,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.pop('instance')
|
||||
items = kwargs.pop('items')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
initial['price'] = instance.price
|
||||
@@ -372,7 +385,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
choices = [
|
||||
('', _('(Unchanged)'))
|
||||
]
|
||||
for i in instance.order.event.items.prefetch_related('variations').all():
|
||||
for i in items:
|
||||
pname = str(i)
|
||||
if not i.is_available():
|
||||
pname += ' ({})'.format(_('inactive'))
|
||||
@@ -524,14 +537,29 @@ class EventCancelForm(forms.Form):
|
||||
subevent = forms.ModelChoiceField(
|
||||
SubEvent.objects.none(),
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
required=True,
|
||||
empty_label=None
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
auto_refund = forms.BooleanField(
|
||||
label=_('Automatically refund money if possible'),
|
||||
initial=True,
|
||||
required=False
|
||||
)
|
||||
manual_refund = forms.BooleanField(
|
||||
label=_('Create manual refund if the payment method odes not support automatic refunds'),
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
|
||||
initial=True,
|
||||
required=False,
|
||||
help_text=_('If checked, all payments with a payment method not supporting automatic refunds will be on your '
|
||||
'manual refund to-do list. Do not check if you want to refund some of the orders by offsetting '
|
||||
'with different orders or issuing gift cards.')
|
||||
)
|
||||
refund_as_giftcard = forms.BooleanField(
|
||||
label=_('Refund order value to a gift card instead instead of the original payment method'),
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
|
||||
initial=False,
|
||||
required=False,
|
||||
)
|
||||
keep_fee_fixed = forms.DecimalField(
|
||||
label=_("Keep a fixed cancellation fee"),
|
||||
max_digits=10, decimal_places=2,
|
||||
@@ -542,8 +570,13 @@ class EventCancelForm(forms.Form):
|
||||
max_digits=10, decimal_places=2,
|
||||
required=False
|
||||
)
|
||||
keep_fees = forms.BooleanField(
|
||||
label=_("Keep payment, shipping and service fees"),
|
||||
keep_fees = forms.MultipleChoiceField(
|
||||
label=_("Keep fees"),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
choices=[(k, v) for k, v in OrderFee.FEE_TYPES if k != OrderFee.FEE_TYPE_GIFTCARD],
|
||||
help_text=_('The selected types of fees will not be refunded but instead added to the cancellation fee. Fees '
|
||||
'are never refunded in when an order in an event series is only partially canceled since it '
|
||||
'consists of tickets for multiple dates.'),
|
||||
required=False,
|
||||
)
|
||||
send = forms.BooleanField(
|
||||
@@ -581,6 +614,7 @@ class EventCancelForm(forms.Form):
|
||||
self.fields['send_subject'] = I18nFormField(
|
||||
label=_("Subject"),
|
||||
required=True,
|
||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
|
||||
initial=_('Canceled: {event}'),
|
||||
widget=I18nTextInput,
|
||||
locales=self.event.settings.get('locales'),
|
||||
@@ -589,6 +623,7 @@ class EventCancelForm(forms.Form):
|
||||
label=_('Message'),
|
||||
widget=I18nTextarea,
|
||||
required=True,
|
||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_send'}},
|
||||
locales=self.event.settings.get('locales'),
|
||||
initial=LazyI18nString.from_gettext(gettext_noop(
|
||||
'Hello,\n\n'
|
||||
@@ -598,6 +633,7 @@ class EventCancelForm(forms.Form):
|
||||
'Your {event} team'
|
||||
))
|
||||
)
|
||||
|
||||
self._set_field_placeholders('send_subject', ['event_or_subevent', 'refund_amount', 'position_or_address',
|
||||
'order', 'event'])
|
||||
self._set_field_placeholders('send_message', ['event_or_subevent', 'refund_amount', 'position_or_address',
|
||||
@@ -607,6 +643,7 @@ class EventCancelForm(forms.Form):
|
||||
required=True,
|
||||
initial=_('Canceled: {event}'),
|
||||
widget=I18nTextInput,
|
||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},
|
||||
locales=self.event.settings.get('locales'),
|
||||
)
|
||||
self.fields['send_waitinglist_message'] = I18nFormField(
|
||||
@@ -614,6 +651,7 @@ class EventCancelForm(forms.Form):
|
||||
widget=I18nTextarea,
|
||||
required=True,
|
||||
locales=self.event.settings.get('locales'),
|
||||
widget_kwargs={'attrs': {'data-display-dependency': '#id_send_waitinglist'}},
|
||||
initial=LazyI18nString.from_gettext(gettext_noop(
|
||||
'Hello,\n\n'
|
||||
'with this email, we regret to inform you that {event} has been canceled.\n\n'
|
||||
@@ -634,11 +672,10 @@ class EventCancelForm(forms.Form):
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': pgettext_lazy('subevent', 'Date')
|
||||
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||
}
|
||||
)
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
self.fields['subevent'].required = True
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
change_decimal_field(self.fields['keep_fee_fixed'], self.event.currency)
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
@@ -95,9 +95,10 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
raise ValidationError(
|
||||
_('You cannot choose the base domain of this installation.')
|
||||
)
|
||||
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk).exists():
|
||||
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk,
|
||||
event__isnull=True).exists():
|
||||
raise ValidationError(
|
||||
_('This domain is already in use for a different organizer.')
|
||||
_('This domain is already in use for a different event or organizer.')
|
||||
)
|
||||
return d
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.urls import reverse
|
||||
from django.utils.dates import MONTHS, WEEKDAYS
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from i18nfield.forms import I18nInlineFormSet
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user