Compare commits

..

3 Commits

Author SHA1 Message Date
Raphael Michel
c1416fbdb8 Bump version to 3.0.1 2019-08-16 12:28:06 +02:00
Raphael Michel
1693affba4 Widget builder: explicit encoding for file reading 2019-08-16 12:27:13 +02:00
Raphael Michel
43d3eb3fe2 Order creation API: Do not consume expired carts 2019-08-16 12:26:40 +02:00
412 changed files with 64458 additions and 130953 deletions

View File

@@ -14,18 +14,18 @@ if [ "$PRETIX_CONFIG_FILE" == "tests/travis_postgres.cfg" ]; then
fi
if [ "$1" == "style" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements.txt -r src/requirements/dev.txt
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt
cd src
flake8 .
isort -c -rc -df .
fi
if [ "$1" == "doctests" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
cd doc
make doctest
fi
if [ "$1" == "doc-spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur doc/requirements.txt
XDG_CACHE_HOME=/cache pip3 install -Ur doc/requirements.txt
cd doc
make spelling
if [ -s _build/spelling/output.txt ]; then
@@ -33,26 +33,26 @@ if [ "$1" == "doc-spelling" ]; then
fi
fi
if [ "$1" == "translation-spelling" ]; then
XDG_CACHE_HOME=/cache pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements/dev.txt
cd src
potypo
fi
if [ "$1" == "tests" ]; then
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
cd src
python manage.py check
make all compress
py.test --reruns 5 -n 3 tests
fi
if [ "$1" == "tests-cov" ]; then
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
cd src
python manage.py check
make all compress
coverage run -m py.test --reruns 5 tests && codecov
fi
if [ "$1" == "plugins" ]; then
pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
cd src
python setup.py develop
make all compress

View File

@@ -3,7 +3,7 @@ Contributing to pretix
Hey there and welcome to pretix!
We've got a contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
We've got an contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/)
together with notes on the [development setup](https://docs.pretix.eu/en/latest/development/setup.html).
Please note that we have a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html)

View File

@@ -57,8 +57,6 @@ COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
COPY src /pretix/src
RUN cd /pretix/src && pip3 install .
RUN chmod +x /usr/local/bin/pretix && \
rm /etc/nginx/sites-enabled/default && \
cd /pretix/src && \

View File

@@ -57,9 +57,6 @@ Example::
A comma-separated list of plugins that are not available even though they are installed.
Defaults to an empty string.
``auth_backends``
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
``cookie_domain``
The cookie domain to be set. Defaults to ``None``.

View File

@@ -276,7 +276,7 @@ choice)::
Then, go to that directory and build the image::
$ docker build . -t mypretix
$ docker build -t mypretix
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update.

View File

@@ -68,7 +68,7 @@ generated key and installs the plugin from the URL we told you::
mkdir -p /etc/ssh && \
ssh-keyscan -t rsa -p 10022 code.rami.io >> /root/.ssh/known_hosts && \
echo StrictHostKeyChecking=no >> /root/.ssh/config && \
DJANGO_SETTINGS_MODULE=pretix.settings pip3 install -U "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
pip3 install -Ue "git+ssh://git@code.rami.io:10022/pretix/pretix-slack.git@stable#egg=pretix-slack" && \
cd /pretix/src && \
sudo -u pretixuser make production
USER pretixuser

View File

@@ -1,132 +0,0 @@
Creating an external checkout process
=====================================
Occasionally, we get asked whether it is possible to just use pretix' powerful backend as a ticketing engine but use
a fully-customized checkout process that only communicates via the API. This is possible, but with a few limitations.
If you go down this route, you will miss out on many of pretix features and safeguards, as well as the added flexibility
by most of pretix' plugins. We strongly recommend to talk this through with us before you decide this is the way to go.
However, this is really useful if you need to tightly integrate pretix into existing web applications that e.g. control
the pricing of your products in a way that cannot be mapped to pretix' product structures.
Creating orders
---------------
After letting your user select the products to buy in your application, you should create a new order object inside
pretix. Below, you can see an example of such an order, but most fields are optional and there are some more features
supported. Read :ref:`rest-orders-create` to learn more about this endpoint.
Please note that this endpoint assumes trustworthy input for the most part. By default, the endpoint checks that
you do not exceed any quotas, do not sell any seats twice, or do not use any redeemed vouchers. However, it will not
complain about violation of any other availability constraints, such as violation of time frames or minimum/maximum
amounts of either your product or event. Bundled products will not be added in automatically and fees will not be
calculated automatically.
.. sourcecode:: http
POST /api/v1/organizers/democon/events/3vjrh/orders/ HTTP/1.1
Host: test.pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Authorization: …
{
"email": "dummy@example.org",
"locale": "en",
"sales_channel": "web",
"payment_provider": "banktransfer",
"invoice_address": {
"is_business": false,
"company": "Sample company",
"name_parts": {"full_name": "John Doe"},
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
"country": "US",
"state": "NY",
"internal_reference": "",
"vat_id": ""
},
"positions": [
{
"item": 21,
"variation": null,
"attendee_name_parts": {
"full_name": "Peter"
},
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": null
}
],
"fees": []
}
You will be returned a full order object that you can inspect, store, or use to build emails or confirmation pages for
the user. If you don't want to do that yourself, it will also contain the URL to our confirmation page in the ``url``
attribute. If you pass the ``"send_mail": true`` option, pretix will also send order confirmations for you.
Handling payments yourself
--------------------------
If you want to handle payments in your application, you can either just create the orders with status "paid" or you can
create them in "pending" state (the default) and later confirm the payment. We strongly advise to use the payment
provider ``"manual"`` in this case to avoid interference with payment code with pretix.
However, it is often unfeasible to implement the payment process yourself, and it also requires you to give up a
lot of pretix functionality, such as automatic refunds. Therefore, it is also possible to utilize pretix' native
payment process even in this case:
Using pretix payment providers
------------------------------
If you passed a ``payment_provider`` during order creation above, pretix will have created a payment object with state
``created`` that you can see in the returned order object. This payment object will have an attribute ``payment_url``
that you can use to let the user pay. For example, you could link or redirect to this page.
If you want the user to return to your application after the payment is complete, you can pass a query parameter
``return_url``. To prepare your event for this, open your event in the pretix backend and go to "Settings", then
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
either enter ``https://example.org`` or ``https://example.org/order/``.
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
**regardless of whether it was successful or not**. Make sure you use our API to check if the payment actually
worked! Your final URL could look like this::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234
You can also embed this page in an ``<iframe>`` instead. Note, however, that this causes problems with some payment
methods such as PayPal which do not allow being opened in an iframe. pretix can partly work around these issues by
opening a new window, but will only to so if you also append an ``iframe=1`` parameter to the URL::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/123/?return_url=https%3A%2F%2Fexample.org%2Forder%2Freturn%3Ftx_id%3D1234&iframe=1
If you did **not** pass a payment method since you want us to ask the user which payment method they want to use, you
need to construct the URL from the ``url`` attribute of the order and the sub-path ``pay/change```. For example, you
would end up with the following URL::
https://test.pretix.eu/democon/3vjrh/order/NSLEZ/ujbrnsjzbq4dzhck/pay/change
Of course, you can also use the ``iframe`` and ``return_url`` parameters here.
Optional: Cart reservations
---------------------------
Creating orders is an atomic operation: The order is either created as a whole or not at all. However, pretix'
built-in checkout automatically reserves tickets in a user's cart for a configurable amount of time to ensure users
will actually get their tickets once they started entering all their details. If you want a similar behavior in your
application, you need to create :ref:`rest-carts` through the API.
When creating your order, you can pass a ``consume_carts`` parameter with the cart ID(s) of your user. This way, the
quota reserved by the cart will be credited towards the order and the carts will be destroyed if (and only if) the
order creation succeeds.
Cart creation is currently even more limited than the order creation endpoints, as cart creation currently does not
support vouchers or automatic price calculation. If you require these features, please get in touch with us.

View File

@@ -1,11 +0,0 @@
.. _`rest-api-guides`:
API Usage Guides
================
This part of the documentation contains how-to guides on some special use cases of our API.
.. toctree::
:maxdepth: 2
custom_checkout

View File

@@ -18,4 +18,3 @@ in functionality over time.
resources/index
ratelimit
webhooks
guides/index

View File

@@ -1,5 +1,3 @@
.. spelling:: checkin
Check-in lists
==============
@@ -29,7 +27,6 @@ subevent integer ID of the date
position_count integer Number of tickets that match this list (read-only).
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -44,10 +41,6 @@ auto_checkin_sales_channels list of strings All items on th
The ``include_pending`` field has been added.
.. versionchanged:: 3.2
The ``auto_checkin_sales_channels`` field has been added.
Endpoints
---------
@@ -88,10 +81,7 @@ Endpoints
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
"subevent": null
}
]
}
@@ -132,10 +122,7 @@ Endpoints
"all_products": true,
"limit_products": [],
"include_pending": false,
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -228,10 +215,7 @@ Endpoints
"name": "VIP entry",
"all_products": false,
"limit_products": [1, 2],
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
"subevent": null
}
**Example response**:
@@ -250,10 +234,7 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
"subevent": null
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
@@ -302,10 +283,7 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"auto_checkin_sales_channels": [
"pretixpos"
]
"subevent": null
}
:param organizer: The ``slug`` field of the organizer to modify
@@ -364,11 +342,6 @@ Order position endpoints
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
returns ``400`` instead of ``404`` on tickets which are known but not paid.
.. versionchanged:: 3.2
The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed
automatically by the system.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as
@@ -427,8 +400,7 @@ Order position endpoints
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
"datetime": "2017-12-25T12:45:23Z"
}
],
"answers": [
@@ -538,8 +510,7 @@ Order position endpoints
"checkins": [
{
"list": 1,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": true
"datetime": "2017-12-25T12:45:23Z"
}
],
"answers": [

View File

@@ -1,9 +1,3 @@
.. spelling::
geo
lat
lon
Events
======
@@ -31,8 +25,6 @@ is_public boolean If ``true``, th
presale_start datetime The date at which the ticket shop opens (or ``null``)
presale_end datetime The date at which the ticket shop closes (or ``null``)
location multi-lingual string The event location (or ``null``)
geo_lat float Latitude of the location (or ``null``)
geo_lon float Longitude of the location (or ``null``)
has_subevents boolean ``true`` if the event series feature is active for this
event. Cannot change after event is created.
meta_data object Values set for organizer-specific meta data parameters.
@@ -70,17 +62,9 @@ seat_category_mapping object An object mappi
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.
Endpoints
---------
.. versionchanged:: 3.3
The events resource can now be filtered by meta data attributes.
.. http:get:: /api/v1/organizers/(organizer)/events/
Returns a list of all events within a given organizer the authenticated user/token has access to.
@@ -121,8 +105,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"meta_data": {},
"seating_plan": null,
@@ -147,10 +129,6 @@ Endpoints
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
Default: ``slug``.
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of events will
only contain the events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return only those
events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that have no value
set. Please note that this filter will respect default values set on organizer level.
:param organizer: The ``slug`` field of a valid organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
@@ -191,8 +169,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},
@@ -244,8 +220,6 @@ Endpoints
"seating_plan": null,
"seat_category_mapping": {},
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"meta_data": {},
"plugins": [
@@ -275,8 +249,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"has_subevents": false,
@@ -296,11 +268,11 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
Creates a new event with properties as set in the request body. The properties that are copied are: ``is_public``,
``testmode``, ``has_subevents``, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their
value. Otherwise their value will be copied from the existing event.
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
their value will be copied from the existing event.
Please note that you can only copy from events under the same organizer.
@@ -328,8 +300,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"has_subevents": false,
@@ -361,8 +331,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},
@@ -426,8 +394,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"has_subevents": false,
"seating_plan": null,
"seat_category_mapping": {},

View File

@@ -1,229 +0,0 @@
.. _`rest-giftcards`:
Gift cards
==========
Resource description
--------------------
The gift card resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the gift card
secret string Gift card code (can not be modified later)
value money (string) Current gift card value
currency string Currency of the value (can not be modified later)
testmode boolean Whether this is a test gift card
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/giftcards/
Returns a list of all gift cards issued by a given organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/giftcards/ 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,
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"value": "13.37"
}
]
}
:query integer page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/giftcards/(id)/
Returns information on one gift card, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/giftcards/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,
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param id: The ``id`` field of the gift card to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/giftcards/
Creates a new gift card
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/giftcards/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"value": "13.37"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"testmode": false,
"currency": "EUR",
"value": "13.37"
}
:param organizer: The ``slug`` field of the organizer to create a gift card for
:statuscode 201: no error
:statuscode 400: The gift card could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/giftcards/(id)/
Update a gift card. 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.
You can change all fields of the resource except the ``id``, ``secret``, ``testmode``, and ``currency`` fields. Be
careful when modifying the ``value`` field to avoid race conditions. We recommend to use the ``transact`` method
described below.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/giftcards/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"value": "14.00"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"testmode": false,
"currency": "EUR",
"value": "14.00"
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the gift card to modify
:statuscode 200: no error
:statuscode 400: The gift card could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:post:: /api/v1/organizers/(organizer)/giftcards/(id)/transact/
Atomically change the value of a gift card. A positive amount will increase the value of the gift card,
a negative amount will decrease it.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"value": "2.00"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"secret": "HLBYVELFRC77NCQY",
"currency": "EUR",
"testmode": false,
"value": "15.37"
}
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the gift card to modify
:statuscode 200: no error
:statuscode 400: The gift card could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.

View File

@@ -21,7 +21,6 @@ Resources and endpoints
vouchers
checkinlists
waitinglist
giftcards
carts
webhooks
seatingplans

View File

@@ -77,7 +77,6 @@ generate_tickets boolean If ``false``, t
rules apply.
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
product when it is sold out.
issue_giftcard boolean If ``true``, buying this product will yield a gift card.
show_quota_left boolean Publicly show how many tickets are still available.
If this is ``null``, the event default is used.
has_variations boolean Shows whether or not this item has variations.
@@ -207,7 +206,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -302,7 +300,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -378,7 +375,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -441,7 +437,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,
@@ -536,7 +531,6 @@ Endpoints
"tax_rate": "0.00",
"tax_rule": 1,
"admission": false,
"issue_giftcard": false,
"position": 0,
"picture": null,
"available_from": null,

View File

@@ -53,9 +53,7 @@ invoice_address object Invoice address
├ street string Customer street
├ zipcode string Customer ZIP code
├ city string Customer city
├ country string Customer country code
├ state string Customer state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US.
├ country string Customer country
├ internal_reference string Customer's internal reference to be printed on the invoice
├ vat_id string Customer VAT ID
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
@@ -84,7 +82,6 @@ require_approval boolean If ``true`` and
needs approval by an organizer before it can
continue. If ``true`` and the order is canceled,
this order has been denied by the event organizer.
url string The full URL to the order confirmation page
payments list of objects List of payment processes (see below)
refunds list of objects List of refund processes (see below)
last_modified datetime Last modification of this object
@@ -131,21 +128,15 @@ last_modified datetime Last modificati
The ``sales_channel`` attribute has been added.
.. versionchanged:: 2.4
.. versionchanged:: 2.4:
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
``…/mark_refunded/`` has been deprecated.
.. versionchanged:: 2.5
.. versionchanged:: 2.5:
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. versionchanged:: 3.1
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
vouchers are now supported and many fields are now optional.
.. _order-position-resource:
Order position resource
@@ -175,8 +166,7 @@ subevent integer ID of the date
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
checkins list of objects List of check-ins with this ticket
├ list integer Internal ID of the check-in list
datetime datetime Time of check-in
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
datetime datetime Time of check-in
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL
@@ -215,15 +205,6 @@ pdf_data object Data object req
The attribute ``seat`` has been added.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. versionchanged:: 3.3
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
:ref:`order-position-ticket-download` for details.
.. _order-payment-resource:
Order payment resource
@@ -240,27 +221,13 @@ amount money (string) Payment amount
created datetime Date and time of creation of this payment
payment_date datetime Date and time of completion of this payment (or ``null``)
provider string Identification string of the payment provider
payment_url string The URL where an user can continue with the payment (or ``null``)
details object Payment-specific information. This is a dictionary
with various fields that can be different between
payment providers, versions, payment states, etc. If
you read this field, you always need to be able to
deal with situations where values that you expect are
missing. Mostly, the field contains various IDs that
can be used for matching with other systems. If a
payment provider does not implement this feature,
the object is empty.
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
.. versionchanged:: 3.1
The attributes ``payment_url`` and ``details`` have been added.
.. _order-refund-resource:
.. _order-payment-resource:
Order refund resource
---------------------
@@ -321,7 +288,6 @@ List of all orders
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
@@ -344,8 +310,7 @@ List of all orders
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": "",
"country": "Testikistan",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false
@@ -375,8 +340,7 @@ List of all orders
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
"datetime": "2017-12-25T12:45:23Z"
}
],
"answers": [
@@ -409,8 +373,6 @@ List of all orders
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
],
@@ -469,7 +431,6 @@ Fetching individual orders
"status": "p",
"testmode": false,
"secret": "k24fiuwvu8kxz3y1",
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
"email": "tester@example.org",
"locale": "en",
"sales_channel": "web",
@@ -492,8 +453,7 @@ Fetching individual orders
"street": "Test street 12",
"zipcode": "12345",
"city": "Testington",
"country": "DE",
"state": "",
"country": "Testikistan",
"internal_reference": "",
"vat_id": "EU123456789",
"vat_id_validated": false
@@ -523,8 +483,7 @@ Fetching individual orders
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
"datetime": "2017-12-25T12:45:23Z"
}
],
"answers": [
@@ -557,8 +516,6 @@ Fetching individual orders
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
],
@@ -734,8 +691,6 @@ Deleting orders
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource **or** the order may not be deleted.
:statuscode 404: The requested order does not exist.
.. _rest-orders-create:
Creating orders
---------------
@@ -761,20 +716,24 @@ Creating orders
* does not validate the number of items per order or the number of times an item can be included in an order
* does not validate any requirements related to add-on products and does not add bundled products automatically
* does not validate any requirements related to add-on products
* does not check prices but believes any prices you send
* does not check or calculate prices but believes any prices you send
* does not support the redemption of vouchers
* does not prevent you from buying items that can only be bought with a voucher
* does not calculate fees automatically
* does not calculate fees
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
module
* does not support file upload questions
* does not send order confirmations via email
* does not support redeeming gift cards
* does not support reverse charge taxation
* does not support file upload questions
You can supply the following fields of the resource:
@@ -791,10 +750,9 @@ Creating orders
* ``email``
* ``locale``
* ``sales_channel``
* ``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
zero, otherwise it is required.
* ``payment_provider`` 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.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``info``
value of the payment object that will be created. How this value is handled is up to the payment provider and you
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
@@ -812,22 +770,17 @@ Creating orders
* ``zipcode``
* ``city``
* ``country``
* ``state``
* ``internal_reference``
* ``vat_id``
* ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``positions``
* ``positionid`` (optional, see below)
* ``item``
* ``variation``
* ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product)
* ``price``
* ``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``
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
@@ -847,8 +800,6 @@ Creating orders
* ``tax_rule``
* ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
``false``.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
to incrementing integers starting with ``1``. Then, you can reference one of these
@@ -886,7 +837,6 @@ Creating orders
"zipcode": "12345",
"city": "Sample City",
"country": "UK",
"state": "",
"internal_reference": "",
"vat_id": ""
},
@@ -910,7 +860,7 @@ Creating orders
],
"subevent": null
}
]
],
}
**Example response**:
@@ -1301,11 +1251,6 @@ List of all order positions
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
``pseudonymization_id``.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. note:: Individually canceled order positions are currently not visible via the API at all.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
@@ -1357,8 +1302,7 @@ List of all order positions
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
"datetime": "2017-12-25T12:45:23Z"
}
],
"answers": [
@@ -1459,8 +1403,7 @@ Fetching individual positions
"checkins": [
{
"list": 44,
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
"datetime": "2017-12-25T12:45:23Z"
}
],
"answers": [
@@ -1488,8 +1431,6 @@ Fetching individual positions
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested order position does not exist.
.. _`order-position-ticket-download`:
Order position ticket download
------------------------------
@@ -1499,11 +1440,6 @@ Order position ticket download
Depending on the chosen output, the response might be a ZIP file, PDF file or something else. The order details
response contains a list of output options for this particular order position.
Be aware that the output does not have to be a file, but can also be a regular HTTP response with a ``Content-Type``
set to ``text/uri-list``. In this case, the user is expected to navigate to that URL in order to access their ticket.
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
in a webbrowser and leave it up to the user to handle the result.
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
configuration downloads might be only unavailable for add-on products or non-admission products.
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status
@@ -1610,8 +1546,6 @@ Order payment endpoints
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}
]
@@ -1652,8 +1586,6 @@ Order payment endpoints
"amount": "23.00",
"created": "2017-12-01T10:00:00Z",
"payment_date": "2017-12-04T12:13:12Z",
"payment_url": null,
"details": {},
"provider": "banktransfer"
}

View File

@@ -41,8 +41,6 @@ ask_during_checkin boolean If ``true``, th
the ticket instead.
hidden boolean If ``true``, the question will only be shown in the
backend.
print_on_invoice boolean If ``true``, the question will only be shown on
invoices.
options list of objects In case of question type ``C`` or ``M``, this lists the
available objects. Only writable during creation,
use separate endpoint to modify this later.
@@ -82,10 +80,6 @@ dependency_value string An old version
The attribute ``dependency_values`` has been added.
.. versionchanged:: 3.1
The attribute ``print_on_invoice`` has been added.
Endpoints
---------
@@ -129,7 +123,6 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -199,7 +192,6 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -253,7 +245,6 @@ Endpoints
"position": 1,
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_values": [],
"options": [
@@ -288,7 +279,6 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],
@@ -362,7 +352,6 @@ Endpoints
"identifier": "WY3TP9SL",
"ask_during_checkin": false,
"hidden": false,
"print_on_invoice": false,
"dependency_question": null,
"dependency_value": null,
"dependency_values": [],

View File

@@ -1,9 +1,3 @@
.. spelling::
geo
lat
lon
.. _rest-subevents:
Event series dates / Sub-events
@@ -34,8 +28,6 @@ date_admission datetime The sub-event's
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
location multi-lingual string The sub-event location (or ``null``)
geo_lat float Latitude of the location (or ``null``)
geo_lon float Longitude of the location (or ``null``)
item_price_overrides list of objects List of items for which this sub-event overrides the
default price
├ item integer The internal item ID
@@ -70,17 +62,9 @@ seat_category_mapping object An object mappi
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.
Endpoints
---------
.. versionchanged:: 3.3
The sub-events resource can now be filtered by meta data attributes.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
Returns a list of all sub-events of an event.
@@ -120,8 +104,6 @@ Endpoints
"seating_plan": null,
"seat_category_mapping": {},
"location": null,
"geo_lat": null,
"geo_lon": null,
"item_price_overrides": [
{
"item": 2,
@@ -141,11 +123,6 @@ Endpoints
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the main event
:query array attr[meta_data_key]: By providing the key and value of a meta data attribute, the list of sub-events
will only contain the sub-events matching the set criteria. Providing ``?attr[Format]=Seminar`` would return
only those sub-events having set their ``Format`` meta data to ``Seminar``, ``?attr[Format]=`` only those, that
have no value set. Please note that this filter will respect default values set on
organizer or event level.
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
@@ -175,8 +152,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -209,8 +184,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -264,8 +237,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -332,8 +303,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [
@@ -420,8 +389,6 @@ Endpoints
"presale_start": null,
"presale_end": null,
"location": null,
"geo_lat": null,
"geo_lon": null,
"seating_plan": null,
"seat_category_mapping": {},
"item_price_overrides": [

View File

@@ -1,70 +0,0 @@
.. highlight:: python
:linenothreshold: 5
Pluggable authentication backends
=================================
Plugins can supply additional authentication backends. This is mainly useful in self-hosted installations
and allows you to use company-wide login mechanisms such as LDAP or OAuth for accessing pretix' backend.
Every authentication backend contains an implementation of the interface defined in ``pretix.base.auth.BaseAuthBackend``
(see below). Note that pretix authentication backends work differently than plain Django authentication backends.
Basically, three pre-defined flows are supported:
* Authentication mechanisms that rely on a **set of input parameters**, e.g. a username and a password. These can be
implemented by supplying the ``login_form_fields`` property and a ``form_authenticate`` method.
* Authentication mechanisms that rely on **external sessions**, e.g. a cookie or a proxy HTTP header. These can be
implemented by supplying a ``request_authenticate`` method.
* Authentication mechanisms that rely on **redirection**, e.g. to an OAuth provider. These can be implemented by
supplying a ``authentication_url`` method and implementing a custom return view.
Authentication backends are *not* collected through a signal. Instead, they must explicitly be set through the
``auth_backends`` directive in the ``pretix.cfg`` :ref:`configuration file <config>`.
In each of these methods (``form_authenticate``, ``request_authenticate`` or your custom view) you are supposed to
either get an existing :py:class:`pretix.base.models.User` object from the database or create a new one. There are a
few rules you need to follow:
* You **MUST** only return users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
* You **MUST** create new users with the ``auth_backend`` attribute set to the ``identifier`` value of your backend.
* Every user object **MUST** have an email address. Email addresses are globally unique. If the email address is
already registered to a user who signs in through a different backend, you **SHOULD** refuse the login.
The backend interface
---------------------
.. class:: pretix.base.auth.BaseAuthBackend
The central object of each backend is the subclass of ``BaseAuthBackend``.
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: verbose_name
This is an abstract attribute, you **must** override this!
.. autoattribute:: login_form_fields
.. autoattribute:: visible
.. automethod:: form_authenticate
.. automethod:: request_authenticate
.. automethod:: authentication_url
Logging users in
----------------
If you return a user from ``form_authenticate`` or ``request_authenticate``, the system will handle everything else
for you correctly. However, if you use a redirection method and build a custom view to verify the login, we strongly
recommend that you use the following utility method to correctly set session values and enforce two-factor
authentication (if activated):
.. autofunction:: pretix.control.views.auth.process_login

View File

@@ -12,7 +12,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter
item_copy_data, register_sales_channels, register_global_settings, quota_availability
Order events
""""""""""""
@@ -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
: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
Frontend
--------

View File

@@ -12,10 +12,8 @@ Contents:
payment
payment_2.0
email
placeholder
invoice
shredder
customview
auth
general
quality

View File

@@ -108,8 +108,6 @@ The provider class
.. automethod:: execute_refund
.. automethod:: api_payment_details
.. automethod:: shred_payment_info
.. autoattribute:: is_implicit

View File

@@ -1,79 +0,0 @@
.. highlight:: python
:linenothreshold: 5
Writing an e-mail placeholder plugin
====================================
An email placeholder is a dynamic value that pretix users can use in their email templates.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
Placeholder registration
------------------------
The placeholder API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available email placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``::
from django.dispatch import receiver
from pretix.base.signals import register_mail_placeholders
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
def register_mail_renderers(sender, **kwargs):
from .email import MyPlaceholderClass
return MyPlaceholder()
Context mechanism
-----------------
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
the context of an order, but some are not, such as the notification of a waiting list voucher.
Not all placeholders make sense in every email, and placeholders usually depend some parameters
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
what values they depend on and they will only be available in an email if all those dependencies are
met. Currently, placeholders can depend on the following context parameters:
* ``event``
* ``order``
* ``position``
* ``waiting_list_entry``
* ``invoice_address``
* ``payment``
There are a few more that are only to be used internally but not by plugins.
The placeholder class
---------------------
.. class:: pretix.base.email.BaseMailTextPlaceholder
.. autoattribute:: identifier
This is an abstract attribute, you **must** override this!
.. autoattribute:: required_context
This is an abstract attribute, you **must** override this!
.. automethod:: render
This is an abstract method, you **must** implement this!
.. automethod:: render_sample
This is an abstract method, you **must** implement this!
Helper class for simple placeholders
------------------------------------
pretix ships with a helper class that makes it easy to provide placeholders based on simple
functions::
placeholder = SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL'
)

View File

@@ -35,9 +35,9 @@ The shredder class
.. class:: pretix.base.shredder.BaseDataShredder
The central object of each data shredder is the subclass of ``BaseDataShredder``.
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
.. py:attribute:: BaseDataShredder.event
.. py:attribute:: BaseInvoiceRenderer.event
The default constructor sets this property to the event we are currently
working for.

View File

@@ -69,9 +69,3 @@ The output class
.. automethod:: generate_order
.. autoattribute:: download_button_text
.. autoattribute:: download_button_icon
.. autoattribute:: preview_allowed
.. autoattribute:: javascript_required

View File

@@ -65,7 +65,7 @@ Then, create the local database::
python manage.py migrate
A first user with username ``admin@localhost`` and password ``admin`` will be automatically
created.
created.
If you want to see pretix in a different language than English, you have to compile our language
files::
@@ -81,7 +81,8 @@ To run the local development webserver, execute::
and head to http://localhost:8000/
As we did not implement an overall front page yet, you need to go directly to
http://localhost:8000/control/ for the admin view.
http://localhost:8000/control/ for the admin view or, if you imported the test
data as suggested above, to the event page at http://localhost:8000/bigevents/2019/
.. note:: If you want the development server to listen on a different interface or
port (for example because you develop on `pretixdroid`_), you can check

View File

@@ -31,7 +31,6 @@ deprovision
discoverable
django
dockerfile
downloadable
durations
eu
filename
@@ -42,13 +41,11 @@ formsets
frontend
frontpage
gettext
giftcard
gunicorn
guid
hardcoded
hostname
idempotency
iframe
incrementing
inofficial
invalidations
@@ -57,7 +54,6 @@ Jimdo
libpretixprint
libsass
linters
login
memcached
metadata
middleware
@@ -79,7 +75,6 @@ overpayment
param
passphrase
percental
pluggable
positionid
pre
prepend
@@ -109,7 +104,6 @@ screenshot
scss
searchable
selectable
serializable
serializers
serializers
sexualized
@@ -141,7 +135,6 @@ username
url
versa
versioning
viewable
viewset
viewsets
waitinglist

View File

@@ -1,71 +0,0 @@
.. spelling::
Warengutschein
Wertgutschein
Gift cards
==========
Gift cards, also known as "gift coupons" or "gift certificates" are a mechanism that allows you to sell tokens that
can later be used to pay for tickets.
Gift cards are very different feature than **vouchers**. The difference is:
* Vouchers can be used to give a discount. When a voucher is used, the price of a ticket is reduced by the configured
discount and sold at a lower price. They therefore reduce both revenue as well as taxes. Vouchers (in pretix) are
always specific to a certain product in an order. Vouchers are usually not sold but given out as part of a
marketing campaign or to specific groups of people. Vouchers in pretix are bound to a specific event.
* Gift cards are not a discount, but rather a means of payment. If you buy a €20 ticket with a €10 gift card, it is
still a €20 ticket and will still count towards your revenue with €20. Gift cards are usually bought for the money
that they are worth. Gift cards in pretix can be used across events (and even organizers).
Selling gift cards
------------------
Selling gift cards works like selling every other type of product in pretix: Create a new product, then head to
"Additional settings" and select the option "This product is a gift card". Whenever someone buys this product and
pays for it, a new gift card will be created.
In this case, the gift card code corresponds to the "ticket secret" in the PDF ticket. Therefore, if selling gift cards,
you can use ticket downloads just as with normal tickets and use our ticket editor to create beautiful gift certificates
people can give to their loved ones.
Of course, you can use pretix' flexible options to modify your product. For example, you can configure that the customer
can freely choose the price of the gift card.
.. note::
pretix currently does not support charging sales tax or VAT when selling gift cards, but instead charges VAT on
the full price when the gift card is redeemed. This is the correct behavior in Germany and some other countries for
gift cards which are not bound to a very specific service ("Warengutschein"), but instead to a monetary amount
("Wertgutschein").
.. note::
The ticket PDF will not contain the correct gift card code before the order has been paid, so we recommend not
selling gift cards in events where tickets are issued before payments arrive.
Accepting gift cards
--------------------
All your events have have the payment provider "Gift card" enabled by default, but it will only show up in the ticket
shop once the very first gift card has been issued on your organizer account. Of course, you can turn off gift card
payments if you do not want them for a specific event.
If gift card payments are enabled, buyers will be able to select "Gift card" as a payment method during checkout. If
a gift card with a value less than the order total is used, the buyer will be asked to select a second payment method
for the remaining payment. If a gift card with a value greater than the order total is used, the surplus amount
remains on the gift card and can be used in a different purchase.
If it possible to accept gift cards across organizer accounts. To do so, you need to have access to both organizer
accounts. Then, you will see a configuration section at the bottom of the "Gift cards" page of your organizer settings
where you can specify which gift cards should be accepted.
Manually issuing or using gift cards
------------------------------------
Of course, you can also issue or redeem gift cards manually through our backend using the "Gift cards" menu item in your
organizer profile or using our API. These gift cards will be tracked by pretix, but do not correspond to any purchase
within pretix. You will therefore need to account for them in your books separately.

View File

@@ -206,21 +206,6 @@ If you want, you can suppress us loading the widget and/or modify the user data
If you then later want to trigger loading the widgets, just call ``window.PretixWidget.buildWidgets()``.
Waiting for the widget to load
------------------------------
If you want to run custom JavaScript once the widget is fully loaded, you can register a callback function. Note that
this function might be run multiple times, for example if you have multiple widgets on a page or if the user switches
e.g. from an event list to an event detail view::
<script type="text/javascript">
window.pretixWidgetCallback = function () {
window.PretixWidget.addLoadListener(function () {
console.log("Widget has loaded!");
});
}
</script>
Passing user data to the widget
-------------------------------
@@ -289,9 +274,6 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
};
</script>
In some combinations with Google Tag Manager, the widget does not load this way. In this case, try replacing
``tracker.get('clientId')`` with ``ga.getAll()[0].get('clientId')``.
.. versionchanged:: 2.3

View File

@@ -12,6 +12,5 @@ wanting to use pretix to sell tickets.
events/settings
events/structureguide
events/widget
events/giftcards
faq
markdown
markdown

View File

@@ -22,5 +22,3 @@ recursive-include pretix/plugins/ticketoutputpdf/templates *
recursive-include pretix/plugins/ticketoutputpdf/static *
recursive-include pretix/plugins/badges/templates *
recursive-include pretix/plugins/badges/static *
recursive-include pretix/plugins/returnurl/templates *
recursive-include pretix/plugins/returnurl/static *

View File

@@ -1 +1 @@
__version__ = "3.3.0"
__version__ = "3.0.1"

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.1 on 2019-10-28 15:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0004_auto_20190405_1048'),
]
operations = [
migrations.AlterField(
model_name='oauthgrant',
name='redirect_uri',
field=models.CharField(max_length=2500),
),
]

View File

@@ -43,7 +43,6 @@ class OAuthGrant(AbstractGrant):
OAuthApplication, on_delete=models.CASCADE
)
organizers = models.ManyToManyField('pretixbase.Organizer')
redirect_uri = models.CharField(max_length=2500) # Only 255 in AbstractGrant, which caused problems
class OAuthAccessToken(AbstractAccessToken):

View File

@@ -82,8 +82,6 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
seat = self.context['event'].seats.get(seat_guid=validated_data['seat'], subevent=validated_data.get('subevent'))
except Seat.DoesNotExist:
raise ValidationError('The specified seat does not exist.')
except Seat.MultipleObjectsReturned:
raise ValidationError('The specified seat ID is not unique.')
else:
validated_data['seat'] = seat
if not seat.is_available():

View File

@@ -3,7 +3,6 @@ from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import CheckinList
@@ -14,7 +13,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending', 'auto_checkin_sales_channels')
'include_pending')
def validate(self, data):
data = super().validate(data)
@@ -36,8 +35,4 @@ class CheckinListSerializer(I18nAwareModelSerializer):
if full_data.get('subevent'):
raise ValidationError(_('The subevent does not belong to this event.'))
for channel in full_data.get('auto_checkin_sales_channels') or []:
if channel not in get_all_sales_channels():
raise ValidationError(_('Unknown sales channel.'))
return data

View File

@@ -70,7 +70,7 @@ class EventSerializer(I18nAwareModelSerializer):
model = Event
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'presale_end', 'location', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping')
def validate(self, data):
@@ -239,7 +239,6 @@ class CloneEventSerializer(EventSerializer):
plugins = validated_data.pop('plugins', None)
is_public = validated_data.pop('is_public', None)
testmode = validated_data.pop('testmode', None)
has_subevents = validated_data.pop('has_subevents', None)
new_event = super().create(validated_data)
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
@@ -251,8 +250,6 @@ class CloneEventSerializer(EventSerializer):
new_event.is_public = is_public
if testmode is not None:
new_event.testmode = testmode
if has_subevents is not None:
new_event.has_subevents = has_subevents
new_event.save()
return new_event
@@ -280,9 +277,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
'seat_category_mapping')
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
def validate(self, data):
data = super().validate(data)

View File

@@ -119,7 +119,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard')
'show_quota_left', 'hidden_if_available', 'allow_waitinglist')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):
@@ -134,17 +134,6 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
if data.get('issue_giftcard'):
if data.get('tax_rule') and data.get('tax_rule').rate > 0:
raise ValidationError(
_("Gift card products should not be associated with non-zero tax rates since sales tax will be "
"applied when the gift card is redeemed.")
)
if data.get('admission'):
raise ValidationError(_(
"Gift card products should not be admission products at the same time."
))
return data
def validate_category(self, value):
@@ -230,7 +219,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
model = Question
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
'hidden', 'dependency_value', 'print_on_invoice')
'hidden', 'dependency_value')
def validate_identifier(self, value):
Question._clean_identifier(self.context['event'], value, self.instance)
@@ -260,9 +249,6 @@ class QuestionSerializer(I18nAwareModelSerializer):
dep = full_data.get('dependency_question')
if dep:
if dep.ask_during_checkin:
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
seen_ids = {self.instance.pk} if self.instance else set()
while dep:
if dep.pk in seen_ids:

View File

@@ -1,9 +1,6 @@
import json
from collections import Counter
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_countries.fields import Country
@@ -17,17 +14,13 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, Voucher,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
)
from pretix.base.pdf import get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.pricing import get_price
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.base.signals import register_ticket_outputs
from pretix.multidomain.urlreverse import build_absolute_uri
class CompatibleCountryField(serializers.Field):
@@ -48,8 +41,8 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceAddress
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified',)
'vat_id', 'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified', 'vat_id_validated')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -64,24 +57,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
)
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['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
@@ -114,7 +89,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime', 'list', 'auto_checked_in')
fields = ('datetime', 'list')
class OrderDownloadsField(serializers.Field):
@@ -286,33 +261,10 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
class PaymentURLField(serializers.URLField):
def to_representation(self, instance: OrderPayment):
if instance.state != OrderPayment.PAYMENT_STATE_CREATED:
return None
return build_absolute_uri(self.context['event'], 'presale:event.order.pay', kwargs={
'order': instance.order.code,
'secret': instance.order.secret,
'payment': instance.pk,
})
class PaymentDetailsField(serializers.Field):
def to_representation(self, value: OrderPayment):
pp = value.payment_provider
if not pp:
return {}
return pp.api_payment_details(value)
class OrderPaymentSerializer(I18nAwareModelSerializer):
payment_url = PaymentURLField(source='*', allow_null=True, read_only=True)
details = PaymentDetailsField(source='*', allow_null=True, read_only=True)
class Meta:
model = OrderPayment
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url',
'details')
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
class OrderRefundSerializer(I18nAwareModelSerializer):
@@ -323,14 +275,6 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
class OrderURLField(serializers.URLField):
def to_representation(self, instance: Order):
return build_absolute_uri(self.context['event'], 'presale:event.order', kwargs={
'order': instance.code,
'secret': instance.secret,
})
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(allow_null=True)
positions = OrderPositionSerializer(many=True, read_only=True)
@@ -340,15 +284,13 @@ class OrderSerializer(I18nAwareModelSerializer):
refunds = OrderRefundSerializer(many=True, read_only=True)
payment_date = OrderPaymentDateField(source='*', read_only=True)
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
url = OrderURLField(source='*', read_only=True)
class Meta:
model = Order
fields = (
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url'
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
@@ -387,7 +329,7 @@ class OrderSerializer(I18nAwareModelSerializer):
}
try:
ia = instance.invoice_address
if iadata.get('vat_id') != ia.vat_id and 'vat_id_validated' not in iadata:
if iadata.get('vat_id') != ia.vat_id:
ia.vat_id_validated = False
self.fields['invoice_address'].update(ia, iadata)
except InvoiceAddress.DoesNotExist:
@@ -495,15 +437,11 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
secret = serializers.CharField(required=False)
attendee_name = serializers.CharField(required=False, allow_null=True)
seat = serializers.CharField(required=False, allow_null=True)
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
max_digits=10)
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
'secret', 'addon_to', 'subevent', 'answers', 'seat')
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -577,7 +515,7 @@ class CompatibleJSONField(serializers.JSONField):
class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=True)
positions = OrderPositionCreateSerializer(many=True, required=False)
fees = OrderFeeCreateSerializer(many=True, required=False)
status = serializers.ChoiceField(choices=(
('n', Order.STATUS_PENDING),
@@ -589,26 +527,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
min_length=5
)
comment = serializers.CharField(required=False, allow_blank=True)
payment_provider = serializers.CharField(required=False, allow_null=True)
payment_provider = serializers.CharField(required=True)
payment_info = CompatibleJSONField(required=False)
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
force = serializers.BooleanField(default=False, required=False)
payment_date = serializers.DateTimeField(required=False, allow_null=True)
send_mail = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
class Meta:
model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_mail')
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force')
def validate_payment_provider(self, pp):
if pp is None:
return None
if pp not in self.context['event'].get_payment_providers():
raise ValidationError('The given payment provider is not known.')
return pp
@@ -675,28 +605,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(errs)
return data
def validate_testmode(self, testmode):
if 'sales_channel' in self.initial_data:
try:
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
if testmode and not sales_channel.testmode_supported:
raise ValidationError('This sales channel does not provide support for testmode.')
except KeyError:
# We do not need to raise a ValidationError here, since there is another check to validate the
# sales_channel
pass
return testmode
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
payment_provider = validated_data.pop('payment_provider', None)
payment_provider = validated_data.pop('payment_provider')
payment_info = validated_data.pop('payment_info', '{}')
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
self._send_mail = validated_data.pop('send_mail', False)
if 'invoice_address' in validated_data:
iadata = validated_data.pop('invoice_address')
@@ -715,7 +630,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
consume_carts = validated_data.pop('consume_carts', [])
delete_cps = []
quota_avail_cache = {}
voucher_usage = Counter()
if consume_carts:
for cp in CartPosition.objects.filter(
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
@@ -727,8 +641,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
quota_avail_cache[quota] = list(quota.availability())
if quota_avail_cache[quota][1] is not None:
quota_avail_cache[quota][1] += 1
if cp.voucher:
voucher_usage[cp.voucher] -= 1
if cp.expires > now_dt:
if cp.seat:
free_seats.add(cp.seat)
@@ -736,55 +648,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
v = pos_data['voucher']
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
continue
if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id:
errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']]
continue
if v.valid_until is not None and v.valid_until < now_dt:
errs[i]['voucher'] = [error_messages['voucher_expired']]
continue
voucher_usage[v] += 1
if voucher_usage[v] > 0:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
try:
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
except Seat.DoesNotExist:
errs[i]['seat'] = ['The specified seat does not exist.']
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
errs[i]['seat'] = [ugettext_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.']
if not force:
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
continue
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
@@ -806,6 +671,23 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
)
]
for i, pos_data in enumerate(positions_data):
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
if pos_data.get('seat'):
if not seated:
errs[i]['seat'] = ['The specified product does not allow to choose a seat.']
try:
seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent'))
except Seat.DoesNotExist:
errs[i]['seat'] = ['The specified seat does not exist.']
else:
pos_data['seat'] = seat
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
errs[i]['seat'] = [ugettext_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.']
if any(errs):
raise ValidationError({'positions': errs})
@@ -813,14 +695,38 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
validated_data['locale'] = self.context['event'].settings.locale
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
order.meta_info = "{}"
order.total = Decimal('0.00')
order.save()
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
payment_date=now()
)
elif payment_provider == "free" and order.total != Decimal('0.00'):
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
elif validated_data.get('status') == Order.STATUS_PAID:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
payment_date=payment_date,
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
state=OrderPayment.PAYMENT_STATE_CREATED
)
if ia:
ia.order = order
ia.save()
pos_map = {}
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
@@ -832,27 +738,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
}
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()
if addon_to:
pos.addon_to = pos_map[addon_to]
if pos.price is None:
price = get_price(
item=pos.item,
variation=pos.variation,
voucher=pos.voucher,
custom_price=None,
subevent=pos.subevent,
addon_to=pos.addon_to,
invoice_address=ia,
)
pos.price = price.gross
pos.tax_rate = price.rate
pos.tax_value = price.tax
pos.tax_rule = pos.item.tax_rule
else:
pos._calculate_tax()
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
pos_map[pos.positionid] = pos
for answ_data in answers_data:
@@ -862,46 +750,12 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for cp in delete_cps:
cp.delete()
for fee_data in fees_data:
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
order.total = sum([p.price for p in order.positions.all()]) + sum([f.value for f in order.fees.all()])
order.save(update_fields=['total'])
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED,
payment_date=now()
)
elif payment_provider == "free" and order.total != Decimal('0.00'):
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
elif validated_data.get('status') == Order.STATUS_PAID:
if not payment_provider:
raise ValidationError('You cannot create a paid order without a payment provider.')
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
payment_date=payment_date,
state=OrderPayment.PAYMENT_STATE_CONFIRMED
)
elif payment_provider:
order.payments.create(
amount=order.total,
provider=payment_provider,
info=payment_info,
state=OrderPayment.PAYMENT_STATE_CREATED
)
return order

View File

@@ -1,11 +1,6 @@
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from pretix.base.models import GiftCard, Organizer, SeatingPlan
from pretix.base.models import Organizer, SeatingPlan
from pretix.base.models.seating import SeatingPlanLayoutValidator
@@ -23,27 +18,3 @@ class SeatingPlanSerializer(I18nAwareModelSerializer):
class Meta:
model = SeatingPlan
fields = ('id', 'name', 'layout')
class GiftCardSerializer(I18nAwareModelSerializer):
value = serializers.DecimalField(max_digits=10, decimal_places=2)
def validate(self, data):
data = super().validate(data)
s = data['secret']
qs = GiftCard.objects.filter(
secret=s
).filter(
Q(issuer=self.context["organizer"]) | Q(issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(
{'secret': _('A gift card with the same secret already exists in your or an affiliated organizer account.')}
)
return data
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode')

View File

@@ -19,7 +19,6 @@ orga_router.register(r'events', event.EventViewSet)
orga_router.register(r'subevents', event.SubEventViewSet)
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
event_router = routers.DefaultRouter()
event_router.register(r'subevents', event.SubEventViewSet)

View File

@@ -44,6 +44,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
qs = self.request.event.checkin_lists.prefetch_related(
'limit_products',
)
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
return qs
def perform_create(self, serializer):

View File

@@ -18,7 +18,6 @@ from pretix.base.models import (
)
from pretix.base.models.event import SubEvent
from pretix.helpers.dicts import merge_dicts
from pretix.presale.views.organizer import filter_qs_by_attr
with scopes_disabled():
class EventFilter(FilterSet):
@@ -86,8 +85,6 @@ class EventViewSet(viewsets.ModelViewSet):
organizer=self.request.organizer
)
qs = filter_qs_by_attr(qs, self.request)
return qs.prefetch_related(
'meta_values', 'meta_values__property', 'seat_category_mappings'
)
@@ -244,9 +241,6 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
event__organizer=self.request.organizer,
event__in=self.request.user.get_events_with_any_permission()
)
qs = filter_qs_by_attr(qs, self.request)
return qs.prefetch_related(
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
)

View File

@@ -6,7 +6,7 @@ import pytz
from django.db import transaction
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse, HttpResponse
from django.http import FileResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext as _
@@ -41,8 +41,7 @@ from pretix.base.services.invoices import (
)
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,
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
)
from pretix.base.services.pricing import get_price
@@ -148,16 +147,12 @@ class OrderViewSet(viewsets.ModelViewSet):
generate.apply_async(args=('order', order.pk, provider.identifier))
raise RetryException()
else:
if ct.type == 'text/uri-list':
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
)
return resp
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
)
return resp
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
@@ -436,7 +431,6 @@ class OrderViewSet(viewsets.ModelViewSet):
serializer.is_valid(raise_exception=True)
with transaction.atomic():
self.perform_create(serializer)
send_mail = serializer._send_mail
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
@@ -445,50 +439,14 @@ class OrderViewSet(viewsets.ModelViewSet):
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
)
order_placed.send(self.request.event, order=order)
with language(order.locale):
order_placed.send(self.request.event, order=order)
gen_invoice = invoice_qualified(order) and (
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
invoice = None
if gen_invoice:
invoice = generate_invoice(order, trigger_pdf=True)
if send_mail:
payment = order.payments.last()
free_flow = (
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
not order.require_approval and payment.provider == "free"
)
if free_flow:
email_template = request.event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = request.event.settings.mail_send_order_free_attendee
email_attendees_template = request.event.settings.mail_text_order_free_attendee
else:
email_template = request.event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
email_attendees = request.event.settings.mail_send_order_placed_attendee
email_attendees_template = request.event.settings.mail_text_order_placed_attendee
_order_placed_email(
request.event, order, payment.payment_provider if payment else None, email_template,
log_entry, invoice, payment
)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(request.event, order, p, email_attendees_template, log_entry)
if not free_flow and order.status == Order.STATUS_PAID and payment:
payment._send_paid_mail(invoice, None, '')
if self.request.event.settings.mail_send_order_paid_attendee:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
payment._send_paid_mail_attendee(p, None)
gen_invoice = invoice_qualified(order) and (
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
if gen_invoice:
generate_invoice(order, trigger_pdf=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
@@ -765,16 +723,12 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
generate.apply_async(args=('orderposition', pos.pk, provider.identifier))
raise RetryException()
else:
if ct.type == 'text/uri-list':
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
)
return resp
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
)
return resp
def perform_destroy(self, instance):
try:

View File

@@ -1,14 +1,11 @@
from django.db import transaction
from rest_framework import filters, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
from rest_framework.response import Response
from rest_framework import filters, viewsets
from rest_framework.exceptions import PermissionDenied
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import (
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
OrganizerSerializer, SeatingPlanSerializer,
)
from pretix.base.models import GiftCard, Organizer, SeatingPlan
from pretix.base.models import Organizer, SeatingPlan
from pretix.helpers.dicts import merge_dicts
@@ -84,66 +81,3 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
data={'id': instance.pk}
)
instance.delete()
class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer
queryset = GiftCard.objects.none()
permission = 'can_manage_gift_cards'
write_permission = 'can_manage_gift_cards'
def get_queryset(self):
return self.request.organizer.issued_gift_cards.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_create(self, serializer):
value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer)
inst.transactions.create(value=value)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
def perform_update(self, serializer):
GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
old_value = serializer.instance.value
value = serializer.validated_data.pop('value')
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
diff = value - old_value
inst.transactions.create(value=diff)
inst.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': diff}
)
return inst
@action(detail=True, methods=["POST"])
@transaction.atomic()
def transact(self, request, **kwargs):
gc = GiftCard.objects.select_for_update().get(pk=self.get_object().pk)
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
request.data.get('value')
)
gc.transactions.create(value=value)
gc.log_action(
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={'value': value}
)
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
def perform_destroy(self, instance):
raise MethodNotAllowed("Gift cards cannot be deleted.")

View File

@@ -13,7 +13,7 @@ class PretixBaseConfig(AppConfig):
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
try:
from .celery_app import app as celery_app # NOQA

View File

@@ -1,109 +0,0 @@
from collections import OrderedDict
from importlib import import_module
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.utils.translation import gettext_lazy as _
def get_auth_backends():
backends = {}
for b in settings.PRETIX_AUTH_BACKENDS:
mod, name = b.rsplit('.', 1)
b = getattr(import_module(mod), name)()
backends[b.identifier] = b
return backends
class BaseAuthBackend:
"""
This base class defines the interface that needs to be implemented by every class that supplies
an authentication method to pretix. Please note that pretix authentication backends are different
from plain Django authentication backends! Be sure to read the documentation chapter on authentication
backends before you implement one.
"""
@property
def identifier(self):
"""
A short and unique identifier for this authentication backend.
This should only contain lowercase letters and in most cases will
be the same as your package name.
"""
raise NotImplementedError()
@property
def verbose_name(self):
"""
A human-readable name of this authentication backend.
"""
raise NotImplementedError()
@property
def visible(self):
"""
Whether or not this backend can be selected by users actively. Set this to ``False``
if you only implement ``request_authenticate``.
"""
return True
@property
def login_form_fields(self) -> dict:
"""
This property may return form fields that the user needs to fill in to log in.
"""
return {}
def form_authenticate(self, request, form_data):
"""
This method will be called after the user filled in the login form. ``request`` will contain
the current request and ``form_data`` the input for the form fields defined in ``login_form_fields``.
You are expected to either return a ``User`` object (if login was successful) or ``None``.
"""
return
def request_authenticate(self, request):
"""
This method will be called when the user opens the login form. If the user already has a valid session
according to your login mechanism, for example a cookie set by a different system or HTTP header set by a
reverse proxy, you can directly return a ``User`` object that will be logged in.
``request`` will contain the current request.
You are expected to either return a ``User`` object (if login was successful) or ``None``.
"""
return
def authentication_url(self, request):
"""
This method will be called to populate the URL for your authentication method's tab on the login page.
For example, if your method works through OAuth, you could return the URL of the OAuth authorization URL the
user needs to visit.
If you return ``None`` (the default), the link will point to a page that shows the form defined by
``login_form_fields``.
"""
return
class NativeAuthBackend(BaseAuthBackend):
identifier = 'native'
verbose_name = _('pretix User')
@property
def login_form_fields(self) -> dict:
"""
This property may return form fields that the user needs to fill in
to log in.
"""
d = OrderedDict([
('email', forms.EmailField(label=_("E-mail"), max_length=254,
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput)),
])
return d
def form_authenticate(self, request, form_data):
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
if u and u.auth_backend == self.identifier:
return u

View File

@@ -1,80 +0,0 @@
import re
# banlist based on http://www.bannedwordlist.com/lists/swearWords.txt
banlist = [
"anal",
"anus",
"arse",
"ass",
"balls",
"bastard",
"bitch",
"biatch",
"bloody",
"blowjob",
"bollock",
"bollok",
"boner",
"boob",
"bugger",
"bum",
"butt",
"clitoris",
"cock",
"coon",
"crap",
"cunt",
"damn",
"dick",
"dildo",
"dyke",
"fag",
"feck",
"fellate",
"fellatio",
"felching",
"fuck",
"fudgepacker",
"flange",
"goddamn",
"hell",
"homo",
"jerk",
"jizz",
"knobend",
"labia",
"lmao",
"lmfao",
"muff",
"nigger",
"nigga",
"omg",
"penis",
"piss",
"poop",
"prick",
"pube",
"pussy",
"queer",
"scrotum",
"sex",
"shit",
"sh1t",
"slut",
"smegma",
"spunk",
"tit",
"tosser",
"turd",
"twat",
"vagina",
"wank",
"whore",
"wtf"
]
blacklist_regex = re.compile('(' + '|'.join(banlist) + ')')
def banned(string):
return bool(blacklist_regex.search(string.lower()))

View File

@@ -35,13 +35,6 @@ class SalesChannel:
"""
return "circle"
@property
def testmode_supported(self) -> bool:
"""
Indication, if a saleschannels supports test mode orders
"""
return True
def get_all_sales_channels():
global _ALL_CHANNELS

View File

@@ -1,15 +0,0 @@
import sys
from django.conf import settings
def contextprocessor(request):
ctx = {
'rtl': getattr(request, 'LANGUAGE_CODE', 'en') in settings.LANGUAGES_RTL,
}
if settings.DEBUG and 'runserver' not in sys.argv:
ctx['debug_warning'] = True
elif 'runserver' in sys.argv:
ctx['development_warning'] = True
return ctx

View File

@@ -1,23 +1,15 @@
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from smtplib import SMTPResponseException
from django.conf import settings
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 ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
from pretix.base.models import Event
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
logger = logging.getLogger('pretix.base.email')
@@ -52,8 +44,8 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
position=None) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
position: OrderPosition=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -105,7 +97,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
body_md = markdown_compile_email(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
@@ -144,305 +136,3 @@ class ClassicMailRenderer(TemplateBasedMailRenderer):
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
def base_renderers(sender, **kwargs):
return [ClassicMailRenderer]
class BaseMailTextPlaceholder:
"""
This is the base class for for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
params = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_email_context(**kwargs):
from pretix.base.models import InvoiceAddress
event = kwargs['event']
if 'order' in kwargs:
try:
kwargs['invoice_address'] = kwargs['order'].invoice_address
except InvoiceAddress.DoesNotExist:
kwargs['invoice_address'] = InvoiceAddress()
ctx = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
ctx[v.identifier] = v.render(kwargs)
return ctx
def _placeholder_payment(order, payment):
if not payment:
return None
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
return str(payment.payment_provider.order_pending_mail_render(order, payment))
else:
return str(payment.payment_provider.order_pending_mail_render(order))
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.base.models import InvoiceAddress
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalMailTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalMailTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
# TODO: This used to be "date" in some placeholders, add a migration!
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_entry.voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalMailTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret']
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
]
),
),
SimpleFunctionalMailTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalMailTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
),
SimpleFunctionalMailTextPlaceholder(
'name', ['name'], lambda name: name,
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['order', 'payment'], _placeholder_payment,
_('The amount has been charged to your card.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalMailTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
lambda position_or_address: (
position_or_address.name
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name
),
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: position.attendee_name_parts.get(f, ''),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: (
position_or_address.name_parts.get(f, '')
if isinstance(position_or_address, InvoiceAddress)
else position_or_address.attendee_name_parts.get(f, '')
),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
return ph

View File

@@ -111,7 +111,7 @@ class ListExporter(BaseExporter):
raise NotImplementedError() # noqa
def get_filename(self):
return 'export'
return 'export.csv'
def _render_csv(self, form_data, output_file=None, **kwargs):
if output_file:

View File

@@ -129,11 +129,8 @@ class DekodiNREIExporter(BaseExporter):
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
'DT': '30' if invoice.is_cancellation else '10',
'EM': invoice.order.email,
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1] if invoice.invoice_to_name else '',
'FN': (
invoice.invoice_to_name.rsplit(' ', 1)[0]
if invoice.invoice_to_name and ' ' in invoice.invoice_to_name else ''
),
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1],
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
'INo': invoice.full_invoice_no,
'IsNet': invoice.reverse_charge,

View File

@@ -9,7 +9,7 @@ from django.utils.formats import date_format, localize
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
from pretix.base.models import (
InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
InvoiceAddress, InvoiceLine, Order, OrderPosition,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -96,7 +96,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Date of last payment'), _('Fees'), _('Order locale')
]
@@ -109,8 +109,6 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Invoice numbers'))
headers.append(_('Sales channel'))
headers.append(_('Requires special attention'))
headers.append(_('Comment'))
yield headers
@@ -155,11 +153,10 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
@@ -181,8 +178,6 @@ class OrderListExporter(MultiSheetListExporter):
row.append(', '.join([i.number for i in order.invoices.all()]))
row.append(order.sales_channel)
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.comment or "")
yield row
def iterate_fees(self, form_data: dict):
@@ -213,7 +208,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
]
yield headers
@@ -248,11 +243,10 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
yield row
def iterate_positions(self, form_data: dict):
@@ -307,7 +301,7 @@ class OrderListExporter(MultiSheetListExporter):
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
]
headers.append(_('Sales channel'))
@@ -345,12 +339,7 @@ class OrderListExporter(MultiSheetListExporter):
]
acache = {}
for a in op.answers.all():
# We do not want to localize Date, Time and Datetime question answers, as those can lead
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
if a.question.type in Question.UNLOCALIZED_TYPES:
acache[a.question_id] = a.answer
else:
acache[a.question_id] = str(a)
acache[a.question_id] = str(a)
for q in questions:
row.append(acache.get(q.pk, ''))
try:
@@ -369,11 +358,10 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row.append(order.sales_channel)
yield row
@@ -515,7 +503,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -565,7 +552,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
@@ -605,7 +591,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('ZIP code'),
_('Invoice recipient:') + ' ' + _('City'),
_('Invoice recipient:') + ' ' + _('Country'),
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
@@ -645,7 +630,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_zipcode,
i.invoice_to_city,
i.invoice_to_country,
i.invoice_to_state,
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,

View File

@@ -1,5 +1,6 @@
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
@@ -13,33 +14,32 @@ class LoginForm(forms.Form):
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""
email = forms.EmailField(label=_("E-mail"), max_length=254, widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
error_messages = {
'invalid_login': _("This combination of credentials is not known to our system."),
'invalid_login': _("Please enter a correct email address and password."),
'inactive': _("This account is inactive.")
}
def __init__(self, backend, request=None, *args, **kwargs):
def __init__(self, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user_cache = None
self.backend = backend
super().__init__(*args, **kwargs)
for k, f in backend.login_form_fields.items():
self.fields[k] = f
if not settings.PRETIX_LONG_SESSIONS:
del self.fields['keep_logged_in']
else:
self.fields.move_to_end('keep_logged_in')
def clean(self):
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
if email and password:
self.user_cache = authenticate(request=self.request, email=email.lower(), password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
@@ -181,44 +181,3 @@ class PasswordForgotForm(forms.Form):
def clean_email(self):
return self.cleaned_data['email']
class ReauthForm(forms.Form):
error_messages = {
'invalid_login': _("This combination of credentials is not known to our system."),
'inactive': _("This account is inactive.")
}
def __init__(self, backend, user, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user = user
self.backend = backend
super().__init__(*args, **kwargs)
for k, f in backend.login_form_fields.items():
self.fields[k] = f
if 'email' in self.fields:
self.fields['email'].disabled = True
def clean(self):
self.cleaned_data['email'] = self.user.email
user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
if user_cache != self.user:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login'
)
else:
self.confirm_login_allowed(user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user: User):
if not user.is_active:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',
)

View File

@@ -5,7 +5,6 @@ from decimal import Decimal
from urllib.error import HTTPError
import dateutil.parser
import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
@@ -16,9 +15,7 @@ from django.db.models import QuerySet
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 _,
)
from django.utils.translation import get_language, ugettext_lazy as _
from django_countries import countries
from django_countries.fields import Country, CountryField
@@ -27,11 +24,8 @@ from pretix.base.forms.widgets import (
TimePickerWidget, UploadedFileWidget,
)
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.escapejson import escapejson_attr
@@ -43,14 +37,6 @@ logger = logging.getLogger(__name__)
class NamePartsWidget(forms.MultiWidget):
widget = forms.TextInput
autofill_map = {
'given_name': 'given-name',
'family_name': 'family-name',
'middle_name': 'additional-name',
'title': 'honorific-prefix',
'full_name': 'name',
'calling_name': 'nickname',
}
def __init__(self, scheme: dict, field: forms.Field, attrs=None, titles: list=None):
widgets = []
@@ -97,7 +83,6 @@ class NamePartsWidget(forms.MultiWidget):
title=self.scheme['fields'][i][1],
placeholder=self.scheme['fields'][i][1],
)
final_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
final_attrs['data-size'] = self.scheme['fields'][i][2]
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
return mark_safe(self.format_output(output))
@@ -203,12 +188,7 @@ class BaseQuestionsForm(forms.Form):
self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
widget=forms.EmailInput(
attrs={
'autocomplete': 'email'
}
)
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
)
for q in questions:
@@ -338,18 +318,12 @@ class BaseQuestionsForm(forms.Form):
self.fields[key] = value
value.initial = data.get('question_form_data', {}).get(key)
for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
def clean(self):
d = super().clean()
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
def question_is_visible(parentid, qvals):
if parentid not in question_cache:
return False
parentq = question_cache[parentid]
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
return False
@@ -382,29 +356,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class Meta:
model = InvoiceAddress
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
'vat_id', 'internal_reference', 'beneficiary')
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
'internal_reference', 'beneficiary')
widgets = {
'is_business': BusinessBooleanRadio,
'street': forms.Textarea(attrs={
'rows': 2,
'placeholder': _('Street and Number'),
'autocomplete': 'street-address'
}),
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
'beneficiary': forms.Textarea(attrs={'rows': 3}),
'country': forms.Select(attrs={
'autocomplete': 'country',
}),
'zipcode': forms.TextInput(attrs={
'autocomplete': 'postal-code',
}),
'city': forms.TextInput(attrs={
'autocomplete': 'address-level2',
}),
'company': forms.TextInput(attrs={
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
@@ -442,33 +400,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
cc = None
if fprefix + 'country' in self.data:
cc = str(self.data[fprefix + 'country'])
elif 'country' in self.initial:
cc = str(self.initial['country'])
elif self.instance and self.instance.country:
cc = str(self.instance.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
if not event.settings.invoice_address_required or self.all_optional:
for k, f in self.fields.items():
f.required = False
@@ -495,18 +426,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
)
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
if not event.settings.invoice_name_required:
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary']
for k, v in self.fields.items():
if v.widget.attrs.get('autocomplete') or k == 'name_parts':
v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '')
def clean(self):
data = self.cleaned_data
if not data.get('is_business'):
@@ -520,10 +446,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if 'vat_id' in self.changed_data or not data.get('vat_id'):
self.instance.vat_id_validated = False
if data.get('city') and data.get('country') and str(data['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
if not data.get('state'):
self.add_error('state', _('This field is required.'))
self.instance.name_parts = data.get('name_parts')
if all(
@@ -535,7 +457,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
if data.get('vat_id')[:2] != str(data.get('country')):
raise ValidationError(_('Your VAT ID does not match the selected country.'))
try:
result = vat_moss.id.validate(data.get('vat_id'))
@@ -566,8 +488,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for f in list(self.fields.keys()):
if f != 'name_parts':
if f != 'name':
del self.fields[f]

View File

@@ -56,11 +56,6 @@ class UserSettingsForm(forms.ModelForm):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['email'].required = True
if self.user.auth_backend != 'native':
del self.fields['old_pw']
del self.fields['new_pw']
del self.fields['new_pw_repeat']
self.fields['email'].disabled = True
def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw')
@@ -120,5 +115,5 @@ class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
('u2f', _('U2F-compatible hardware token (e.g. Yubikey)')),
))

View File

@@ -26,7 +26,7 @@ class PlaceholderValidator(BaseValidator):
if value.count('{') != value.count('}'):
raise ValidationError(
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
code='invalid_placeholder_syntax',
code='invalid',
)
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
@@ -37,7 +37,7 @@ class PlaceholderValidator(BaseValidator):
if invalid_placeholders:
raise ValidationError(
_('Invalid placeholder(s): %(value)s'),
code='invalid_placeholders',
code='invalid',
params={'value': ", ".join(invalid_placeholders,)})
def clean(self, x):

View File

@@ -46,14 +46,6 @@ class TimePickerWidget(forms.TimeInput):
class UploadedFileWidget(forms.ClearableFileInput):
def __init__(self, *args, **kwargs):
# Browsers can't recognize that the server already has a file uploaded
# Don't mark this input as being required if we already have an answer
# (this needs to be done via the attrs, otherwise we wouldn't get the "required" star on the field label)
attrs = kwargs.get('attrs', {})
if kwargs.get('required') and kwargs.get('initial'):
attrs.update({'required': None})
kwargs.update({'attrs': attrs})
self.position = kwargs.pop('position')
self.event = kwargs.pop('event')
self.answer = kwargs.pop('answer')

View File

@@ -23,7 +23,7 @@ modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
IGNORED_ATTRS = [
# (field type, attribute name, banlist of field sub-types)
# (field type, attribute name, blacklist of field sub-types)
(models.Field, 'verbose_name', []),
(models.Field, 'help_text', []),
(models.Field, 'validators', []),
@@ -38,8 +38,8 @@ original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
name, path, args, kwargs = original_deconstruct(self)
for ftype, attr, banlist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in banlist):
for ftype, attr, blacklist in IGNORED_ATTRS:
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in blacklist):
kwargs.pop(attr, None)
return name, path, args, kwargs

View File

@@ -11,13 +11,13 @@ from django.core.management.commands.migrate import Command as Parent
class OutputFilter(OutputWrapper):
banlist = (
blacklist = (
"Your models have changes that are not yet reflected",
"Run 'manage.py makemigrations' to make new "
)
def write(self, msg, style_func=None, ending=None):
if any(b in msg for b in self.banlist):
if any(b in msg for b in self.blacklist):
return
super().write(msg, style_func, ending)

View File

@@ -14,7 +14,6 @@ from django.utils.translation.trans_real import (
parse_accept_lang_header,
)
from pretix.base.settings import GlobalSettingsObject
from pretix.multidomain.urlreverse import get_domain
_supported = None
@@ -188,11 +187,6 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
img_src = []
gs = GlobalSettingsObject()
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
h = {
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
@@ -202,7 +196,7 @@ class SecurityMiddleware(MiddlewareMixin):
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs

View File

@@ -182,12 +182,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -23,12 +23,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -124,7 +124,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',
@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.RunPython(
code=merge_names,

View File

@@ -346,7 +346,7 @@ class Migration(migrations.Migration):
help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',

View File

@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',

View File

@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.RunPython(merge_names, migrations.RunPython.noop)
]

View File

@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',

View File

@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',

View File

@@ -105,7 +105,7 @@ class Migration(migrations.Migration):
'references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
@@ -125,7 +125,7 @@ class Migration(migrations.Migration):
' events.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',

View File

@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -1,27 +0,0 @@
# Generated by Django 2.2.1 on 2019-08-08 12:53
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0131_auto_20190729_1422'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='invoice_to_state',
field=models.CharField(max_length=190, null=True),
),
migrations.AddField(
model_name='invoiceaddress',
name='state',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 2.2.4 on 2019-08-30 15:13
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0132_auto_20190808_1253'),
]
operations = [
migrations.AddField(
model_name='question',
name='print_on_invoice',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,35 +0,0 @@
# Generated by Django 2.2.4 on 2019-09-09 10:42
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0133_auto_20190830_1513'),
]
operations = [
migrations.CreateModel(
name='WebAuthnDevice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('confirmed', models.BooleanField(default=True)),
('credential_id', models.CharField(max_length=255, null=True)),
('rp_id', models.CharField(max_length=255, null=True)),
('icon_url', models.CharField(max_length=255, null=True)),
('ukey', models.TextField(null=True)),
('pub_key', models.TextField(null=True)),
('sign_count', models.IntegerField(default=0)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 2.2.4 on 2019-10-07 08:03
from django.core.cache import cache
from django.db import migrations
def mail_migrator(app, schema_editor):
Event_SettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
for ss in Event_SettingsStore.objects.filter(
key__in=['mail_text_order_approved', 'mail_text_order_placed', 'mail_text_order_placed_require_approval']
):
chgd = ss.value.replace("{date}", "{expire_date}")
if chgd != ss.value:
ss.value = chgd
ss.save()
cache.delete('hierarkey_{}_{}'.format('event', ss.object_id))
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0134_auto_20190909_1042'),
]
operations = [
migrations.RunPython(mail_migrator, migrations.RunPython.noop)
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 2.2 on 2019-09-18 17:42
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0135_auto_20191007_0803'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='auto_checked_in',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='checkinlist',
name='auto_checkin_sales_channels',
field=pretix.base.models.fields.MultiStringField(default=[]),
)
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 2.2.4 on 2019-10-15 11:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0136_auto_20190918_1742'),
]
operations = [
migrations.AddField(
model_name='user',
name='auth_backend',
field=models.CharField(default='native', max_length=255),
),
]

View File

@@ -1,107 +0,0 @@
# Generated by Django 2.2.4 on 2019-10-17 11:51
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
import pretix.base.models.fields
import pretix.base.models.giftcards
def fwd(app, schema_editor):
Team = app.get_model('pretixbase', 'Team')
Team.objects.filter(can_change_organizer_settings=True).update(can_manage_gift_cards=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0137_auto_20191015_1141'),
]
operations = [
migrations.CreateModel(
name='GiftCard',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('issuance', models.DateTimeField(auto_now_add=True)),
('secret', models.CharField(db_index=True, default=pretix.base.models.giftcards.gen_giftcard_secret,
max_length=190)),
('currency', models.CharField(max_length=10)),
('issued_in', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='issued_gift_cards', to='pretixbase.OrderPosition')),
('issuer',
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='issued_gift_cards',
to='pretixbase.Organizer')),
('testmode', django.db.models.BooleanField(default=False)),
],
options={
'unique_together': {('secret', 'issuer')},
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.AddField(
model_name='item',
name='issue_giftcard',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='team',
name='can_manage_gift_cards',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='question',
name='dependency_values',
field=pretix.base.models.fields.MultiStringField(default=[]),
),
migrations.AlterField(
model_name='voucher',
name='item',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
to='pretixbase.Item'),
),
migrations.AlterField(
model_name='voucher',
name='quota',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
to='pretixbase.Quota'),
),
migrations.AlterField(
model_name='voucher',
name='variation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vouchers',
to='pretixbase.ItemVariation'),
),
migrations.CreateModel(
name='GiftCardTransaction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('datetime', models.DateTimeField(auto_now_add=True)),
('value', models.DecimalField(decimal_places=2, max_digits=10)),
('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions',
to='pretixbase.GiftCard')),
('order', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='gift_card_transactions', to='pretixbase.Order')),
('payment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='gift_card_transactions', to='pretixbase.OrderPayment')),
('refund', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT,
related_name='gift_card_transactions', to='pretixbase.OrderRefund')),
],
options={
'ordering': ('datetime',),
},
),
migrations.CreateModel(
name='GiftCardAcceptance',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('collector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='gift_card_issuer_acceptance', to='pretixbase.Organizer')),
('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='gift_card_collector_acceptance', to='pretixbase.Organizer')),
],
),
migrations.RunPython(
fwd, migrations.RunPython.noop
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 2.2.1 on 2019-10-19 13:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0138_auto_20191017_1151'),
]
operations = [
migrations.AddField(
model_name='event',
name='geo_lat',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='event',
name='geo_lon',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='subevent',
name='geo_lat',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='subevent',
name='geo_lon',
field=models.FloatField(null=True),
),
]

View File

@@ -1,5 +1,5 @@
from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User, WebAuthnDevice
from .auth import U2FDevice, User
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin, CheckinList
from .devices import Device
@@ -7,7 +7,6 @@ from .event import (
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
)
from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,

View File

@@ -1,9 +1,5 @@
import binascii
import json
from datetime import timedelta
from urllib.parse import urlparse
import webauthn
from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin,
@@ -17,9 +13,6 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from u2flib_server.utils import (
pub_key_from_der, websafe_decode, websafe_encode,
)
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
@@ -109,7 +102,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
help_text=_('If turned off, you will not get any notifications.')
)
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
auth_backend = models.CharField(max_length=255, default='native')
objects = UserManager()
@@ -184,7 +176,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'url': build_absolute_uri('control:user.settings')
},
event=None,
user=self,
locale=self.locale
)
except SendMailException:
@@ -200,13 +191,9 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
},
None, locale=self.locale, user=self
None, locale=self.locale
)
@property
def top_logentries(self):
return self.all_logentries
@property
def all_logentries(self):
from pretix.base.models import LogEntry
@@ -335,25 +322,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
)
@scopes_disabled()
def get_organizers_with_permission(self, permission, request=None):
"""
Returns a queryset of organizers the user has a specific permissions to.
:param request: The current request (optional). Required to detect staff sessions properly.
:return: Iterable of Organizers
"""
from .event import Organizer
if request and self.has_active_staff_session(request.session.session_key):
return Organizer.objects.all()
kwargs = {permission: True}
return Organizer.objects.filter(
id__in=self.teams.filter(**kwargs).values_list('organizer', flat=True)
)
def has_active_staff_session(self, session_key=None):
"""
Returns whether or not a user has an active staff session (formerly known as superuser session)
@@ -407,49 +375,3 @@ class StaffSessionAuditLog(models.Model):
class U2FDevice(Device):
json_data = models.TextField()
@property
def webauthnuser(self):
d = json.loads(self.json_data)
# We manually need to convert the pubkey from DER format (used in our
# former U2F implementation) to the format required by webauthn. This
# is based on the following example:
# https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples
pub_key = pub_key_from_der(websafe_decode(d['publicKey'].replace('+', '-').replace('/', '_')))
pub_key = binascii.unhexlify(
'A5010203262001215820{:064x}225820{:064x}'.format(
pub_key.public_numbers().x, pub_key.public_numbers().y
)
)
return webauthn.WebAuthnUser(
d['keyHandle'],
self.user.email,
str(self.user),
settings.SITE_URL,
d['keyHandle'],
websafe_encode(pub_key),
1,
urlparse(settings.SITE_URL).netloc
)
class WebAuthnDevice(Device):
credential_id = models.CharField(max_length=255, null=True, blank=True)
rp_id = models.CharField(max_length=255, null=True, blank=True)
icon_url = models.CharField(max_length=255, null=True, blank=True)
ukey = models.TextField(null=True)
pub_key = models.TextField(null=True)
sign_count = models.IntegerField(default=0)
@property
def webauthnuser(self):
return webauthn.WebAuthnUser(
self.ukey,
self.user.email,
str(self.user),
settings.SITE_URL,
self.credential_id,
self.pub_key,
self.sign_count,
urlparse(settings.SITE_URL).netloc
)

View File

@@ -1,11 +1,11 @@
from django.db import models
from django.db.models import Exists, OuterRef
from django.db.models import Case, Count, F, OuterRef, Q, Subquery, When
from django.db.models.functions import Coalesce
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
class CheckinList(LoggedModel):
@@ -18,69 +18,142 @@ class CheckinList(LoggedModel):
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
'order have not been paid.'))
auto_checkin_sales_channels = MultiStringField(
default=[],
blank=True,
verbose_name=_('Sales channels to automatically check in'),
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
'are not checked again before entry and should be considered validated directly upon purchase.')
)
'order have not been paid. This only works with pretixdesk '
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
objects = ScopedManager(organizer='event__organizer')
class Meta:
ordering = ('subevent__date_from', 'name')
@property
def positions(self):
from . import OrderPosition, Order
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
subevent=self.subevent
)
if not self.all_products:
qs = qs.filter(item__in=self.limit_products.values_list('id', flat=True))
return qs
@property
def checkin_count(self):
return self.event.cache.get_or_set(
'checkin_list_{}_checkin_count'.format(self.pk),
lambda: self.positions.annotate(
checkedin=Exists(Checkin.objects.filter(list_id=self.pk, position=OuterRef('pk')))
).filter(
checkedin=True
).count(),
60
)
@property
def percent(self):
pc = self.position_count
return round(self.checkin_count * 100 / pc) if pc else 0
@property
def position_count(self):
return self.event.cache.get_or_set(
'checkin_list_{}_position_count'.format(self.pk),
lambda: self.positions.count(),
60
)
def touch(self):
self.event.cache.delete('checkin_list_{}_position_count'.format(self.pk))
self.event.cache.delete('checkin_list_{}_checkin_count'.format(self.pk))
@staticmethod
def annotate_with_numbers(qs, event):
# This is only kept for backwards-compatibility reasons. This method used to precompute .position_count
# and .checkin_count through a huge subquery chain, but was dropped for performance reasons.
return qs
"""
Modifies a queryset of checkin lists by annotating it with the number of order positions and
checkins associated with it.
"""
# Import here to prevent circular import
from . import Order, OrderPosition, Item
# This is the mother of all subqueries. Sorry. I try to explain it, at least?
# First, we prepare a subquery that for every check-in that belongs to a paid-order
# position and to the list in question. Then, we check that it also belongs to the
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
# since we filtered by lists).
cqs_paid = Checkin.objects.filter(
position__order__event=event,
position__order__status=Order.STATUS_PAID,
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
cqs_paid_and_pending = Checkin.objects.filter(
position__order__event=event,
position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
list=OuterRef('pk')
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(position__subevent=OuterRef('subevent'))
| (Q(position__subevent__isnull=True))
).order_by().values('list').annotate(
c=Count('*')
).values('c')
# Now for the hard part: getting all order positions that contribute to this list. This
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
# lists that contain all the products of the event. This is the simpler one, it basically
# looks like the check-in counter above.
pqs_all_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_all_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Now we need a subquery for the case of checkin lists that are limited to certain
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
# with the products table and we'd get duplicate rows in the output with different annotations
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
# to retrieve all of those items and then check if the item_id is IN this subquery result.
pqs_limited_paid = OrderPosition.objects.filter(
order__event=event,
order__status=Order.STATUS_PAID,
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
pqs_limited_paid_and_pending = OrderPosition.objects.filter(
order__event=event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
).filter(
# This assumes that in an event with subevents, *all* positions have subevents
# and *all* checkin lists have a subevent assigned
Q(subevent=OuterRef('subevent'))
| (Q(subevent__isnull=True))
).order_by().values('order__event').annotate(
c=Count('*')
).values('c')
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
# we want to display a progress bar.
return qs.annotate(
checkin_count=Coalesce(
Case(
When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())),
default=Subquery(cqs_paid, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
),
position_count=Coalesce(
Case(
When(all_products=True, include_pending=False,
then=Subquery(pqs_all_paid, output_field=models.IntegerField())),
When(all_products=True, include_pending=True,
then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())),
When(all_products=False, include_pending=False,
then=Subquery(pqs_limited_paid, output_field=models.IntegerField())),
default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()),
output_field=models.IntegerField()
),
0
)
).annotate(
percent=Case(
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
default=0,
output_field=models.IntegerField()
)
)
def __str__(self):
return self.name
@@ -96,7 +169,6 @@ class Checkin(models.Model):
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
auto_checked_in = models.BooleanField(default=False)
objects = ScopedManager(organizer='position__order__event__organizer')
@@ -110,11 +182,8 @@ class Checkin(models.Model):
def save(self, **kwargs):
self.position.order.touch()
self.list.event.cache.delete('checkin_count')
self.list.touch()
super().save(**kwargs)
def delete(self, **kwargs):
self.position.order.touch()
super().delete(**kwargs)
self.list.touch()

View File

@@ -22,7 +22,7 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBanlistValidator
from pretix.base.validators import EventSlugBlacklistValidator
from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange
from pretix.helpers.json import safe_string
@@ -65,7 +65,7 @@ class EventMixin:
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_from_display(self, tz=None, show_times=True, short=False) -> str:
def get_date_from_display(self, tz=None, show_times=True) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
@@ -73,7 +73,7 @@ class EventMixin:
tz = tz or self.timezone
return _date(
self.date_from.astimezone(tz),
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
)
def get_time_from_display(self, tz=None) -> str:
@@ -86,7 +86,7 @@ class EventMixin:
self.date_from.astimezone(tz), "TIME_FORMAT"
)
def get_date_to_display(self, tz=None, short=False) -> str:
def get_date_to_display(self, tz=None) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
@@ -97,7 +97,7 @@ class EventMixin:
return ""
return _date(
self.date_to.astimezone(tz),
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")
"DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
)
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
@@ -291,7 +291,7 @@ class Event(EventMixin, LoggedModel):
regex="^[a-zA-Z0-9.-]+$",
message=_("The slug may only contain letters, numbers, dots and dashes."),
),
EventSlugBanlistValidator()
EventSlugBlacklistValidator()
],
verbose_name=_("Short form"),
)
@@ -324,14 +324,6 @@ class Event(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Location"),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True,
)
plugins = models.TextField(
null=False, blank=True,
verbose_name=_("Plugins"),
@@ -616,24 +608,22 @@ class Event(EventMixin, LoggedModel):
question_map=question_map
)
def get_payment_providers(self, cached=False) -> dict:
def get_payment_providers(self) -> dict:
"""
Returns a dictionary of initialized payment providers mapped by their identifiers.
"""
from ..signals import register_payment_providers
if not cached or not hasattr(self, '_cached_payment_providers'):
responses = register_payment_providers.send(self)
providers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
providers[pp.identifier] = pp
responses = register_payment_providers.send(self)
providers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
providers[pp.identifier] = pp
self._cached_payment_providers = OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
return self._cached_payment_providers
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
def get_html_mail_renderer(self):
"""
@@ -738,7 +728,7 @@ class Event(EventMixin, LoggedModel):
def has_payment_provider(self):
result = False
for provider in self.get_payment_providers().values():
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting', 'giftcard'):
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice', 'offsetting'):
result = True
break
return result
@@ -834,24 +824,18 @@ class Event(EventMixin, LoggedModel):
def enable_plugin(self, module, allow_restricted=False):
plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module not in plugins_active:
plugins_active.append(module)
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
regenerate_css.apply_async(args=(self.pk,))
def disable_plugin(self, module):
plugins_active = self.get_plugins()
from pretix.presale.style import regenerate_css
if module in plugins_active:
plugins_active.remove(module)
self.set_active_plugins(plugins_active)
regenerate_css.apply_async(args=(self.pk,))
@staticmethod
def clean_has_subevents(event, has_subevents):
if event is not None and event.has_subevents is not None:
@@ -937,14 +921,6 @@ class SubEvent(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Location"),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text")

View File

@@ -1,112 +0,0 @@
from decimal import Decimal
from django.conf import settings
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 pretix.base.banlist import banned
from pretix.base.models import LoggedModel
def gen_giftcard_secret():
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
while True:
code = get_random_string(length=settings.ENTROPY['giftcard_secret'], allowed_chars=charset)
if not banned(code) and not GiftCard.objects.filter(secret=code).exists():
return code
class GiftCardAcceptance(models.Model):
issuer = models.ForeignKey(
'Organizer',
related_name='gift_card_collector_acceptance',
on_delete=models.CASCADE
)
collector = models.ForeignKey(
'Organizer',
related_name='gift_card_issuer_acceptance',
on_delete=models.CASCADE
)
class GiftCard(LoggedModel):
issuer = models.ForeignKey(
'Organizer',
related_name='issued_gift_cards',
on_delete=models.PROTECT,
)
issued_in = models.ForeignKey(
'OrderPosition',
related_name='issued_gift_cards',
on_delete=models.PROTECT,
null=True, blank=True
)
issuance = models.DateTimeField(
auto_now_add=True,
)
secret = models.CharField(
max_length=190,
default=gen_giftcard_secret,
db_index=True,
verbose_name=_('Gift card code'),
)
testmode = models.BooleanField(
verbose_name=_('Test mode card'),
default=False
)
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
currency = models.CharField(max_length=10, choices=CURRENCY_CHOICES)
def __str__(self):
return self.secret
@property
def value(self):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
def accepted_by(self, organizer):
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
class Meta:
unique_together = (('secret', 'issuer'),)
class GiftCardTransaction(models.Model):
card = models.ForeignKey(
'GiftCard',
related_name='transactions',
on_delete=models.PROTECT
)
datetime = models.DateTimeField(
auto_now_add=True
)
value = models.DecimalField(
decimal_places=2,
max_digits=10
)
order = models.ForeignKey(
'Order',
related_name='gift_card_transactions',
null=True,
blank=True,
on_delete=models.PROTECT
)
payment = models.ForeignKey(
'OrderPayment',
related_name='gift_card_transactions',
null=True,
blank=True,
on_delete=models.PROTECT
)
refund = models.ForeignKey(
'OrderRefund',
related_name='gift_card_transactions',
null=True,
blank=True,
on_delete=models.PROTECT
)
class Meta:
ordering = ("datetime",)

View File

@@ -1,7 +1,6 @@
import string
from decimal import Decimal
import pycountry
from django.db import DatabaseError, models, transaction
from django.db.models import Max
from django.db.models.functions import Cast
@@ -12,8 +11,6 @@ from django.utils.translation import pgettext
from django_countries.fields import CountryField
from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def invoice_filename(instance, filename: str) -> str:
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
@@ -93,7 +90,6 @@ class Invoice(models.Model):
invoice_to_street = models.TextField(null=True)
invoice_to_zipcode = models.CharField(max_length=190, null=True)
invoice_to_city = models.TextField(null=True)
invoice_to_state = models.CharField(max_length=190, null=True)
invoice_to_country = CountryField(null=True)
invoice_to_vat_id = models.TextField(null=True)
invoice_to_beneficiary = models.TextField(null=True)
@@ -144,21 +140,11 @@ class Invoice(models.Model):
def address_invoice_to(self):
if self.invoice_to and not self.invoice_to_company and not self.invoice_to_name:
return self.invoice_to
state_name = ""
if self.invoice_to_state:
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
parts = [
self.invoice_to_company,
self.invoice_to_name,
self.invoice_to_street,
((self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or "") + " " + (state_name or "")).strip(),
(self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or ""),
self.invoice_to_country.name if self.invoice_to_country else "",
]
return '\n'.join([p.strip() for p in parts if p and p.strip()])

View File

@@ -242,8 +242,6 @@ class Item(LoggedModel):
:type require_approval: bool
:param sales_channels: Sales channels this item is available on.
:type sales_channels: bool
:param issue_giftcard: If ``True``, buying this product will give you a gift card with the value of the product's price
:type issue_giftcard: bool
"""
objects = ItemQuerySetManager()
@@ -415,12 +413,6 @@ class Item(LoggedModel):
verbose_name=_('Sales channels'),
default=['web']
)
issue_giftcard = models.BooleanField(
verbose_name=_('This product is a gift card'),
help_text=_('When a customer buys this product, they will get a gift card with a value corresponding to the '
'product price.'),
default=False
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/forms/item.py if applicable.
@@ -990,7 +982,6 @@ class Question(LoggedModel):
(TYPE_DATETIME, _("Date and time")),
(TYPE_COUNTRYCODE, _("Country code (ISO 3166-1 alpha-2)")),
)
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
event = models.ForeignKey(
Event,
@@ -1033,6 +1024,8 @@ class Question(LoggedModel):
)
ask_during_checkin = models.BooleanField(
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
help_text=_('This will only work if you handle your check-in with pretixdroid 1.8 or newer or '
'pretixdesk 0.2 or newer.'),
default=False
)
hidden = models.BooleanField(
@@ -1040,10 +1033,6 @@ class Question(LoggedModel):
help_text=_('This question will only show up in the backend.'),
default=False
)
print_on_invoice = models.BooleanField(
verbose_name=_('Print answer on invoices'),
default=False
)
dependency_question = models.ForeignKey(
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
)

View File

@@ -9,7 +9,6 @@ from decimal import Decimal
from typing import Any, Dict, List, Union
import dateutil
import pycountry
import pytz
from django.conf import settings
from django.db import models, transaction
@@ -31,15 +30,12 @@ from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
from pretix.base.banlist import banned
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 User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
@@ -202,7 +198,7 @@ class Order(LockModel, LoggedModel):
return self.full_code
def gracefully_delete(self, user=None, auth=None):
from . import Voucher, GiftCard, GiftCardTransaction
from . import Voucher
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
@@ -213,17 +209,11 @@ class Order(LockModel, LoggedModel):
}
)
order_gracefully_delete.send(self.event, order=self)
if self.status != Order.STATUS_CANCELED:
for position in self.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
GiftCardTransaction.objects.filter(payment__in=self.payments.all()).update(payment=None)
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
GiftCardTransaction.objects.filter(order=self).update(order=None)
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
@@ -465,15 +455,11 @@ class Order(LockModel, LoggedModel):
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
).select_related('item').prefetch_related('issued_gift_cards')
).select_related('item')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
if not cancelable or not positions:
return False
for op in positions:
for gc in op.issued_gift_cards.all():
if gc.value != op.price:
return False
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
if self.status == Order.STATUS_PENDING:
@@ -548,30 +534,16 @@ class Order(LockModel, LoggedModel):
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
# might include OCR'd handwritten text
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
iteration = 0
length = settings.ENTROPY['order_code']
while True:
code = get_random_string(length=length, allowed_chars=charset)
iteration += 1
if banned(code):
continue
code = get_random_string(length=settings.ENTROPY['order_code'], allowed_chars=charset)
if self.testmode:
# Subtle way to recognize test orders while debugging: They all contain a 0 at the second place,
# even though zeros are not used outside test mode.
code = code[0] + "0" + code[2:]
if not Order.objects.filter(event__organizer=self.event.organizer, code=code).exists():
self.code = code
return
if iteration > 20:
# Safeguard: If we don't find an unused and non-blacklisted code within 20 iterations, we increase
# the length.
length += 1
iteration = 0
@property
def can_modify_answers(self) -> bool:
"""
@@ -724,8 +696,7 @@ class Order(LockModel, LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True,
attach_ical=False):
auth=None, attach_tickets=False, position: 'OrderPosition'=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -742,7 +713,6 @@ class Order(LockModel, LoggedModel):
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files
:param position: An order position this refers to. If given, no invoices will be attached, the tickets will
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
@@ -766,7 +736,7 @@ class Order(LockModel, LoggedModel):
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email, attach_ical=attach_ical
position=position
)
except SendMailException:
raise
@@ -782,19 +752,35 @@ class Order(LockModel, LoggedModel):
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
}
)
def resend_link(self, user=None, auth=None):
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.locale):
try:
invoice_name = self.invoice_address.name
invoice_company = self.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.event, order=self)
email_context = {
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.order.open', kwargs={
'order': self.code,
'secret': self.secret,
'hash': self.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': self.code}
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
attach_tickets=True,
attach_tickets=True
)
@property
@@ -1057,8 +1043,6 @@ class AbstractPosition(models.Model):
}
def question_is_visible(parentid, qvals):
if parentid not in question_cache:
return False
parentq = question_cache[parentid]
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
return False
@@ -1211,7 +1195,7 @@ class OrderPayment(models.Model):
"""
Cached access to an instance of the payment provider in use.
"""
return self.order.event.get_payment_providers(cached=True).get(self.provider)
return self.order.event.get_payment_providers().get(self.provider)
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
from pretix.base.signals import order_paid
@@ -1328,36 +1312,66 @@ class OrderPayment(models.Model):
def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
name_scheme = PERSON_NAME_SCHEMES[self.order.event.settings.name_scheme]
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_context = get_email_context(event=self.order.event, order=self.order, position=position)
email_context = {
'event': self.order.event.name,
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'url': build_absolute_uri(self.order.event, 'presale:event.order.position', kwargs={
'order': self.order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[], position=position,
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': self.order.code,
'secret': self.order.secret,
'hash': self.order.email_confirm_hash()
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@@ -1899,26 +1913,25 @@ class OrderPosition(AbstractPosition):
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.attendee_email:
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.order.locale):
recipient = self.attendee_email
with language(self.locale):
recipient = self.email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
self.event, self.locale, self, headers, sender,
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
else:
self.order.log_action(
self.log_action(
log_entry_type,
user=user,
auth=auth,
@@ -1931,18 +1944,6 @@ class OrderPosition(AbstractPosition):
}
)
def resend_link(self, user=None, auth=None):
with language(self.order.locale):
email_template = self.event.settings.mail_text_resend_link
email_context = get_email_context(event=self.order.event, order=self.order, position=self)
email_subject = _('Your event registration: %(code)s') % {'code': self.order.code}
self.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.resend', user=user, auth=auth,
attach_tickets=True
)
class CartPosition(AbstractPosition):
"""
@@ -2018,7 +2019,6 @@ class InvoiceAddress(models.Model):
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'))
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.'))
vat_id_validated = models.BooleanField(default=False)
@@ -2045,22 +2045,6 @@ class InvoiceAddress(models.Model):
self.name_parts = {}
super().save(**kwargs)
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
@property
def name(self):
if not self.name_parts:

View File

@@ -2,13 +2,12 @@ import string
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from pretix.base.models.base import LoggedModel
from pretix.base.validators import OrganizerSlugBanlistValidator
from pretix.base.validators import OrganizerSlugBlacklistValidator
from ..settings import settings_hierarkey
from .auth import User
@@ -40,7 +39,7 @@ class Organizer(LoggedModel):
regex="^[a-zA-Z0-9.-]+$",
message=_("The slug may only contain letters, numbers, dots and dashes.")
),
OrganizerSlugBanlistValidator()
OrganizerSlugBlacklistValidator()
],
verbose_name=_("Short form"),
unique=True
@@ -83,24 +82,6 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self)
@property
def has_gift_cards(self):
return self.cache.get_or_set(
key='has_gift_cards',
timeout=15,
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.exists()
)
@property
def accepted_gift_cards(self):
from .giftcards import GiftCard, GiftCardAcceptance
return GiftCard.objects.annotate(
accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self))
).filter(
Q(issuer=self) | Q(accepted=True)
)
def allow_delete(self):
from . import Order, Invoice
return (
@@ -175,10 +156,6 @@ class Team(LoggedModel):
help_text=_('Someone with this setting can get access to most data of all of your events, i.e. via privacy '
'reports, so be careful who you add to this team!')
)
can_manage_gift_cards = models.BooleanField(
default=False,
verbose_name=_("Can manage gift cards")
)
can_change_event_settings = models.BooleanField(
default=False,

View File

@@ -85,12 +85,6 @@ EU_CURRENCIES = {
}
def cc_to_vat_prefix(country_code):
if country_code == 'GR':
return 'EL'
return country_code
class TaxRule(LoggedModel):
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
name = I18nCharField(

View File

@@ -10,7 +10,6 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.banlist import banned
from pretix.base.models import SeatCategoryMapping
from ..decimal import round_decimal
@@ -22,12 +21,9 @@ from .orders import Order
def _generate_random_code(prefix=None):
charset = list('ABCDEFGHKLMNPQRSTUVWXYZ23456789')
rnd = None
while not rnd or banned(rnd):
rnd = get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
if prefix:
return prefix + rnd
return rnd
return prefix + get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
return get_random_string(length=settings.ENTROPY['voucher_code'], allowed_chars=charset)
@scopes_disabled()

View File

@@ -6,10 +6,10 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_scopes import ScopedManager
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Voucher
from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import build_absolute_uri
from .base import LoggedModel
from .event import Event, SubEvent
@@ -130,7 +130,13 @@ class WaitingListEntry(LoggedModel):
self.email,
_('You have been selected from the waitinglist for {event}').format(event=str(self.event)),
self.event.settings.mail_text_waiting_list,
get_email_context(event=self.event, waiting_list_entry=self),
{
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.redeem') + '?voucher=' + self.voucher.code,
'code': self.voucher.code,
'product': str(self.item) + (' - ' + str(self.variation) if self.variation else ''),
'hours': self.event.settings.waiting_list_hours,
},
self.event,
locale=self.locale
)

View File

@@ -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 ugettext_lazy as _
from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types
@@ -175,23 +175,12 @@ class ParametrizedOrderNotificationType(NotificationType):
url=order_url
)
n.add_attribute(_('Event'), order.event.name)
if order.event.has_subevents:
ses = []
for se in self.event.subevents.filter(id__in=order.positions.values_list('subevent', flat=True)):
ses.append('{} ({})'.format(se.name, se.get_date_range_display()))
n.add_attribute(pgettext_lazy('subevent', 'Dates'), '\n'.join(ses))
else:
n.add_attribute(_('Event date'), order.event.get_date_range_display())
n.add_attribute(_('Order code'), order.code)
n.add_attribute(_('Order total'), money_filter(order.total, logentry.event.currency))
n.add_attribute(_('Pending amount'), money_filter(order.pending_sum, logentry.event.currency))
n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT'))
n.add_attribute(_('Order status'), order.get_status_display())
n.add_attribute(_('Order positions'), str(order.positions.count()))
items = []
for it in self.event.items.filter(id__in=order.positions.values_list('item', flat=True)):
items.append(str(it.name))
n.add_attribute(_('Purchased products'), '\n'.join(items))
n.add_action(_('View order details'), order_url)
return n

View File

@@ -7,9 +7,7 @@ from typing import Any, Dict, Union
import pytz
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured
from django.db import transaction
from django.dispatch import receiver
from django.forms import Form
from django.http import HttpRequest
@@ -22,8 +20,8 @@ from i18nfield.strings import LazyI18nString
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota,
CartPosition, Event, InvoiceAddress, Order, OrderPayment, OrderRefund,
Quota,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -31,8 +29,7 @@ from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.views import get_cart, get_cart_total
from pretix.presale.views import get_cart_total
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
logger = logging.getLogger(__name__)
@@ -534,7 +531,7 @@ class BasePaymentProvider:
containing the URL the user will be redirected to. If you are done with your process
you should return the user to the order's detail page.
If the payment is completed, you should call ``payment.confirm()``. Please note that this might
If the payment is completed, you should call ``payment.confirm()``. Please note that ``this`` might
raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this order is over and
some of the items are sold out. You should use the exception message to display a meaningful error
to the user.
@@ -660,15 +657,6 @@ class BasePaymentProvider:
obj.info = '{}'
obj.save(update_fields=['info'])
def api_payment_details(self, payment: OrderPayment):
"""
Will be called to populate the ``details`` parameter of the payment in the REST API.
:param payment: The payment in question.
:return: A serializable dictionary
"""
return {}
class PaymentException(Exception):
pass
@@ -702,9 +690,8 @@ class FreeOrderProvider(BasePaymentProvider):
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
from .services.cart import get_fees
cart = get_cart(request)
total = get_cart_total(request)
total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)])
total += sum([f.value for f in get_fees(self.event, request, total, None, None)])
return total == 0
def order_change_allowed(self, order: Order) -> bool:
@@ -733,12 +720,6 @@ class BoxOfficeProvider(BasePaymentProvider):
def order_change_allowed(self, order: Order) -> bool:
return False
def api_payment_details(self, payment: OrderPayment):
return {
"pos_id": payment.info_data.get('pos_id', None),
"receipt_id": payment.info_data.get('receipt_id', None),
}
def payment_control_render(self, request, payment) -> str:
if not payment.info:
return
@@ -871,7 +852,7 @@ class OffsettingProvider(BasePaymentProvider):
provider='offsetting',
info=json.dumps({'orders': [refund.order.code]})
)
p.confirm(ignore_date=True)
p.confirm()
@property
def settings_form_fields(self) -> dict:
@@ -883,216 +864,10 @@ class OffsettingProvider(BasePaymentProvider):
def order_change_allowed(self, order: Order) -> bool:
return False
def api_payment_details(self, payment: OrderPayment):
return {
"orders": payment.info_data.get('orders', []),
}
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders']))
class GiftCardPayment(BasePaymentProvider):
identifier = "giftcard"
verbose_name = _("Gift card")
@property
def settings_form_fields(self):
f = super().settings_form_fields
del f['_fee_abs']
del f['_fee_percent']
del f['_fee_reverse_calc']
del f['_total_min']
del f['_total_max']
del f['_invoice_text']
return f
@property
def test_mode_message(self) -> str:
return _("In test mode, only test cards will work.")
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return super().is_allowed(request, total) and self.event.organizer.has_gift_cards
def order_change_allowed(self, order: Order) -> bool:
return super().order_change_allowed(order) and self.event.organizer.has_gift_cards
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
return get_template('pretixcontrol/giftcards/checkout.html').render({})
def checkout_confirm_render(self, request) -> str:
return get_template('pretixcontrol/giftcards/checkout_confirm.html').render({})
def payment_control_render(self, request, payment) -> str:
from .models import GiftCard
if 'gift_card' in payment.info_data:
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
template = get_template('pretixcontrol/giftcards/payment.html')
ctx = {
'request': request,
'event': self.event,
'gc': gc,
}
return template.render(ctx)
def api_payment_details(self, payment: OrderPayment):
from .models import GiftCard
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
return {
'gift_card': {
'id': gc.pk,
'secret': gc.secret,
'organizer': gc.issuer.slug
}
}
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
return True
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return True
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
for p in get_cart(request):
if p.item.issue_giftcard:
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
return
cs = cart_session(request)
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard")
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
return
if gc.testmode and not self.event.testmode:
messages.error(request, _("This gift card can only be used in test mode."))
return
if not gc.testmode and self.event.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
if 'gift_cards' not in cs:
cs['gift_cards'] = []
elif gc.pk in cs['gift_cards']:
messages.error(request, _("This gift card is already used for your payment."))
return
cs['gift_cards'] = cs['gift_cards'] + [gc.pk]
remainder = cart['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)
))
else:
messages.success(request, _("Your gift card has been applied."))
kwargs = {'step': 'payment'}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
return eventreverse(self.event, 'presale:event.checkout', kwargs=kwargs)
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection."))
else:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str, None]:
for p in payment.order.positions.all():
if p.item.issue_giftcard:
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
return
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard")
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
return
if gc.testmode and not payment.order.testmode:
messages.error(request, _("This gift card can only be used in test mode."))
return
if not gc.testmode and payment.order.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
payment.info_data = {
'gift_card': gc.pk,
'retry': True
}
payment.amount = min(payment.amount, gc.value)
payment.save()
return True
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection."))
else:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
# This method will only be called when retrying payments, e.g. after a payment_prepare call. It is not called
# during the order creation phase because this payment provider is a special case.
for p in payment.order.positions.all(): # noqa - just a safeguard
if p.item.issue_giftcard:
raise PaymentException(_("You cannot pay with gift cards when buying a gift card."))
gcpk = payment.info_data.get('gift_card')
if not gcpk or not payment.info_data.get('retry'):
raise PaymentException("Invalid state, should never occur.")
with transaction.atomic():
gc = GiftCard.objects.select_for_update().get(pk=gcpk)
if gc.currency != self.event.currency: # noqa - just a safeguard
raise PaymentException(_("This gift card does not support this currency."))
if not gc.accepted_by(self.event.organizer): # noqa - just a safeguard
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if payment.amount > gc.value: # noqa - just a safeguard
raise PaymentException(_("This gift card was used in the meantime. Please try again"))
trans = gc.transactions.create(
value=-1 * payment.amount,
order=payment.order,
payment=payment
)
payment.info_data = {
'gift_card': gc.pk,
'transaction_id': trans.pk,
}
payment.confirm()
def payment_is_valid_session(self, request: HttpRequest) -> bool:
return True
@transaction.atomic()
def execute_refund(self, refund: OrderRefund):
from .models import GiftCard
gc = GiftCard.objects.get(pk=refund.payment.info_data.get('gift_card'))
trans = gc.transactions.create(
value=refund.amount,
order=refund.order,
refund=refund
)
refund.info_data = {
'gift_card': gc.pk,
'transaction_id': trans.pk,
}
refund.done()
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment]

View File

@@ -14,7 +14,6 @@ from django.conf import settings
from django.contrib.staticfiles import finders
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from PyPDF2 import PdfFileReader
@@ -33,7 +32,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition
from pretix.base.models import Order, OrderPosition, QuestionAnswer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -53,40 +52,35 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": "A1B2C",
"evaluate": lambda orderposition, order, event: orderposition.order.code
}),
("positionid", {
"label": _("Order position number"),
"editor_sample": "1",
"evaluate": lambda orderposition, order, event: str(orderposition.positionid)
}),
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: escape(str(orderposition.item.name))
"evaluate": lambda orderposition, order, event: str(orderposition.item.name)
}),
("variation", {
"label": _("Variation name"),
"editor_sample": _("Sample variation"),
"evaluate": lambda op, order, event: escape(str(op.variation) if op.variation else '')
"evaluate": lambda op, order, event: str(op.variation) if op.variation else ''
}),
("item_description", {
"label": _("Product description"),
"editor_sample": _("Sample product description"),
"evaluate": lambda orderposition, order, event: escape(str(orderposition.item.description))
"evaluate": lambda orderposition, order, event: str(orderposition.item.description)
}),
("itemvar", {
"label": _("Product name and variation"),
"editor_sample": _("Sample product sample variation"),
"evaluate": lambda orderposition, order, event: escape((
"evaluate": lambda orderposition, order, event: (
'{} - {}'.format(orderposition.item.name, orderposition.variation)
if orderposition.variation else str(orderposition.item.name)
))
)
}),
("item_category", {
"label": _("Product category"),
"editor_sample": _("Ticket category"),
"evaluate": lambda orderposition, order, event: escape((
"evaluate": lambda orderposition, order, event: (
str(orderposition.item.category.name) if orderposition.item.category else ""
))
)
}),
("price", {
"label": _("Price"),
@@ -105,12 +99,12 @@ DEFAULT_VARIABLES = OrderedDict((
("attendee_name", {
"label": _("Attendee name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: escape(op.attendee_name or (op.addon_to.attendee_name if op.addon_to else ''))
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
}),
("event_name", {
"label": _("Event name"),
"editor_sample": _("Sample event name"),
"evaluate": lambda op, order, ev: escape(str(ev.name))
"evaluate": lambda op, order, ev: str(ev.name)
}),
("event_date", {
"label": _("Event date"),
@@ -191,12 +185,12 @@ DEFAULT_VARIABLES = OrderedDict((
("invoice_name", {
"label": _("Invoice address name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: escape(order.invoice_address.name if getattr(order, 'invoice_address', None) else '')
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address', None) else ''
}),
("invoice_company", {
"label": _("Invoice address company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: escape(order.invoice_address.company if getattr(order, 'invoice_address', None) else '')
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address', None) else ''
}),
("addons", {
"label": _("List of Add-Ons"),
@@ -213,7 +207,7 @@ DEFAULT_VARIABLES = OrderedDict((
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
"evaluate": lambda op, order, ev: escape(str(order.event.organizer.name))
"evaluate": lambda op, order, ev: str(order.event.organizer.name)
}),
("organizer_info_text", {
"label": _("Organizer info text"),
@@ -247,14 +241,12 @@ DEFAULT_VARIABLES = OrderedDict((
("seat", {
"label": _("Seat: Full name"),
"editor_sample": _("Ground floor, Row 3, Seat 4"),
"evaluate": lambda op, order, ev: str(op.seat if op.seat else
_('General admission') if ev.seating_plan_id is not None else "")
"evaluate": lambda op, order, ev: str(op.seat if op.seat else _('General admission'))
}),
("seat_zone", {
"label": _("Seat: zone"),
"editor_sample": _("Ground floor"),
"evaluate": lambda op, order, ev: str(op.seat.zone_name if op.seat else
_('General admission') if ev.seating_plan_id is not None else "")
"evaluate": lambda op, order, ev: str(op.seat.zone_name if op.seat else _('General admission'))
}),
("seat_row", {
"label": _("Seat: row"),
@@ -272,28 +264,16 @@ DEFAULT_VARIABLES = OrderedDict((
@receiver(layout_text_variables, dispatch_uid="pretix_base_layout_text_variables_questions")
def variables_from_questions(sender, *args, **kwargs):
def get_answer(op, order, event, question_id):
a = None
if op.addon_to:
if 'answers' in getattr(op.addon_to, '_prefetched_objects_cache', {}):
try:
a = [a for a in op.addon_to.answers.all() if a.question_id == question_id][0]
except IndexError:
pass
else:
a = op.addon_to.answers.filter(question_id=question_id).first()
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
try:
try:
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
a = [a for a in op.answers.all() if a.question_id == question_id][0]
except IndexError:
pass
else:
a = op.answers.filter(question_id=question_id).first()
if not a:
else:
a = op.answers.get(question_id=question_id)
return str(a).replace("\n", "<br/>\n")
except QuestionAnswer.DoesNotExist:
return ""
except IndexError:
return ""
else:
return escape(str(a)).replace("\n", "<br/>\n")
d = {}
for q in sender.questions.all():
@@ -306,11 +286,11 @@ def variables_from_questions(sender, *args, **kwargs):
def _get_attendee_name_part(key, op, order, ev):
return escape(op.attendee_name_parts.get(key, ''))
return op.attendee_name_parts.get(key, '')
def _get_ia_name_part(key, op, order, ev):
return escape(order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else '')
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
def get_variables(event):

View File

@@ -97,7 +97,6 @@ error_messages = {
'seat_forbidden': _('You can not select a seat for this position.'),
'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': _('You can not select the same seat multiple times.'),
'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
}
@@ -227,15 +226,11 @@ class CartManager:
def _check_item_constraints(self, op):
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
if not (
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
):
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if op.item.require_voucher and op.voucher is None:
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
raise CartError(error_messages['voucher_required'])
if op.item.hide_without_voucher and (op.voucher is None or not op.voucher.show_hidden_items):
raise CartError(error_messages['voucher_required'])
if not op.item.is_available() or (op.variation and not op.variation.active):
raise CartError(error_messages['unavailable'])
@@ -437,8 +432,6 @@ class CartManager:
seat = (subevent or self.event).seats.get(seat_guid=i.get('seat'))
except Seat.DoesNotExist:
raise CartError(error_messages['seat_invalid'])
except Seat.MultipleObjectsReturned:
raise CartError(error_messages['seat_invalid'])
i['item'] = seat.product_id
if i['item'] not in self._items_cache:
self._update_items_cache([i['item']], [i['variation']])
@@ -619,7 +612,7 @@ class CartManager:
op = self.AddOperation(
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=cp.seat
)
self._check_item_constraints(op)
operations.append(op)
@@ -798,9 +791,8 @@ class CartManager:
for b in op.bundled:
b_quotas = list(b.quotas)
if not b_quotas:
if not op.voucher or not op.voucher.allow_ignore_quota:
err = err or error_messages['unavailable']
available_count = 0
err = err or error_messages['unavailable']
available_count = 0
continue
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b_quotas))
if b_quota_available_count < b.count:
@@ -958,42 +950,15 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
return totaldiff
def get_fees(event, request, total, invoice_address, provider, positions):
from pretix.presale.views.cart import cart_session
def get_fees(event, request, total, invoice_address, provider):
fees = []
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address,
total=total, positions=positions):
total=total):
if resp:
fees += resp
total = total + sum(f.value for f in fees)
cs = cart_session(request)
if cs.get('gift_cards'):
gcs = cs['gift_cards']
gc_qs = event.organizer.accepted_gift_cards.filter(pk__in=cs.get('gift_cards'), currency=event.currency)
summed = 0
for gc in gc_qs:
if gc.testmode != event.testmode:
gcs.remove(gc.pk)
continue
fval = Decimal(gc.value) # TODO: don't require an extra query
fval = min(fval, total - summed)
if fval > 0:
total -= fval
summed += fval
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_GIFTCARD,
internal_type='giftcard',
description=gc.secret,
value=-1 * fval,
tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'),
tax_rule=TaxRule.zero()
))
cs['gift_cards'] = gcs
if provider and total != 0:
provider = event.get_payment_providers().get(provider)
if provider:

View File

@@ -1,13 +1,11 @@
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 pretix.base.models import (
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
)
from pretix.base.signals import order_placed
class CheckInError(Exception):
@@ -157,18 +155,3 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'datetime': dt,
'list': clist.pk
}, user=user, auth=auth)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return
for op in order.positions.all():
for cl in cls:
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
Checkin.objects.create(position=op, list=cl, auto_checked_in=True)

View File

@@ -20,7 +20,9 @@ from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
from pretix.base.models import (
Invoice, InvoiceAddress, InvoiceLine, Order, OrderPayment,
)
from pretix.base.models.tax import EU_CURRENCIES
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.settings import GlobalSettingsObject
@@ -35,6 +37,9 @@ logger = logging.getLogger(__name__)
@transaction.atomic
def build_invoice(invoice: Invoice) -> Invoice:
lp = invoice.order.payments.last()
open_payment = None
if lp and lp.state not in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED):
open_payment = lp
with language(invoice.locale):
invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
@@ -48,11 +53,13 @@ def build_invoice(invoice: Invoice) -> Invoice:
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
if lp and lp.payment_provider:
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
payment = lp.payment_provider.render_invoice_text(invoice.order, lp)
if open_payment and open_payment.payment_provider:
if 'payment' in inspect.signature(open_payment.payment_provider.render_invoice_text).parameters:
payment = open_payment.payment_provider.render_invoice_text(invoice.order, open_payment)
else:
payment = lp.payment_provider.render_invoice_text(invoice.order)
payment = open_payment.payment_provider.render_invoice_text(invoice.order)
elif invoice.order.status == Order.STATUS_PAID:
payment = pgettext('invoice', 'The payment for this invoice has already been received.')
else:
payment = ""
@@ -66,15 +73,12 @@ def build_invoice(invoice: Invoice) -> Invoice:
addr_template = pgettext("invoice", """{i.company}
{i.name}
{i.street}
{i.zipcode} {i.city} {state}
{i.zipcode} {i.city}
{country}""")
invoice.invoice_to = "\n".join(
a.strip() for a in addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old,
state=ia.state_for_address
).split("\n") if a.strip()
)
invoice.invoice_to = addr_template.format(
i=ia,
country=ia.country.name if ia.country else ia.country_old
).strip()
invoice.internal_reference = ia.internal_reference
invoice.invoice_to_company = ia.company
invoice.invoice_to_name = ia.name
@@ -82,7 +86,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.invoice_to_zipcode = ia.zipcode
invoice.invoice_to_city = ia.city
invoice.invoice_to_country = ia.country
invoice.invoice_to_state = ia.state
invoice.invoice_to_beneficiary = ia.beneficiary
if ia.vat_id:
@@ -122,7 +125,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
positions = list(
invoice.order.positions.select_related('addon_to', 'item', 'tax_rule', 'subevent', 'variation').annotate(
addon_c=Count('addons')
).prefetch_related('answers', 'answers__question').order_by('positionid', 'id')
).order_by('positionid', 'id')
)
reverse_charge = False
@@ -139,16 +142,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
desc = " + " + desc
if invoice.event.settings.invoice_attendee_name and p.attendee_name:
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(name=p.attendee_name)
for answ in p.answers.all():
if not answ.question.print_on_invoice:
continue
desc += "<br />{}{} {}".format(
answ.question.question,
"" if str(answ.question.question).endswith("?") else ":",
str(answ)
)
if invoice.event.has_subevents:
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
InvoiceLine.objects.create(
@@ -203,8 +196,6 @@ def build_cancellation(invoice: Invoice):
def generate_cancellation(invoice: Invoice, trigger_pdf=True):
if invoice.refered.exists():
raise ValueError("Invoice should not be canceled twice.")
cancellation = modelcopy(invoice)
cancellation.pk = None
cancellation.invoice_no = None

View File

@@ -4,6 +4,7 @@ import os
import re
import smtplib
import warnings
from email.encoders import encode_noop
from email.mime.image import MIMEImage
from email.utils import formataddr
from typing import Any, Dict, List, Union
@@ -23,15 +24,14 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import (
Event, Invoice, InvoiceAddress, Order, OrderPosition, User,
Event, Invoice, InvoiceAddress, Order, OrderPosition,
)
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter, global_email_filter
from pretix.base.signals import email_filter
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_ical
logger = logging.getLogger('pretix.base.mail')
INVALID_ADDRESS = 'invalid-pretix-mail-address'
@@ -51,7 +51,7 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
invoices: list=None, attach_tickets=False, auto_email=True, user=None, attach_ical=False):
invoices: list=None, attach_tickets=False):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -86,12 +86,6 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param attach_tickets: Whether to attach tickets to this email, if they are available to download.
:param attach_ical: Whether to attach relevant ``.ics`` files to this email
:param auto_email: Whether this email is auto-generated
:param user: The user this email is sent to
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
that the email has been sent, just that it has been queued by the email backend.
"""
@@ -99,9 +93,6 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
return
headers = headers or {}
if auto_email:
headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN'
headers['Auto-Submitted'] = 'auto-generated'
with language(locale):
if isinstance(context, dict) and event:
@@ -121,7 +112,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
})
renderer = ClassicMailRenderer(None)
content_plain = body_plain = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = str(subject).format_map(context)
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
if event:
sender_name = event.settings.mail_from_name or str(event.name)
@@ -218,9 +209,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
invoices=[i.pk for i in invoices] if invoices and not position else [],
order=order.pk if order else None,
position=position.pk if position else None,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
user=user.pk if user else None
attach_tickets=attach_tickets
)
if invoices:
@@ -235,17 +224,13 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
invoices: List[int]=None, order: int=None, attach_tickets=False, user=None,
attach_ical=False) -> bool:
invoices: List[int]=None, order: int=None, attach_tickets=False) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
html_with_cid, cid_images = replace_images_with_cid_paths(html)
email = attach_cid_images(email, cid_images, verify_ssl=True)
email.attach_alternative(html_with_cid, "text/html")
if user:
user = User.objects.get(pk=user)
if event:
with scopes_disabled():
event = Event.objects.get(id=event)
@@ -306,24 +291,8 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
'invoices': [],
}
)
if attach_ical:
ical_events = set()
if event.has_subevents:
if position:
ical_events.add(position.subevent)
else:
for p in order.positions.all():
ical_events.add(p.subevent)
else:
ical_events.add(order.event)
for i, e in enumerate(ical_events):
cal = get_ical([e])
email.attach('event-{}.ics'.format(i), cal.serialize(), 'text/calendar')
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
email = email_filter.send_chained(event, 'message', message=email, order=order)
try:
backend.send_messages([email])
@@ -397,7 +366,7 @@ def replace_images_with_cid_paths(body_html):
def attach_cid_images(msg, cid_images, verify_ssl=True):
if cid_images and len(cid_images) > 0:
msg.mixed_subtype = 'mixed'
msg.mixed_subtype = 'related'
for key, image in enumerate(cid_images):
cid = 'image_%s' % key
try:
@@ -411,27 +380,12 @@ def attach_cid_images(msg, cid_images, verify_ssl=True):
return msg
def encoder_linelength(msg):
"""
RFC1341 mandates that base64 encoded data may not be longer than 76 characters per line
https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html section 5.2
"""
orig = msg.get_payload(decode=True).replace(b"\n", b"").replace(b"\r", b"")
max_length = 76
pieces = []
for i in range(0, len(orig), max_length):
chunk = orig[i:i + max_length]
pieces.append(chunk)
msg.set_payload(b"\r\n".join(pieces))
def convert_image_to_cid(image_src, cid_id, verify_ssl=True):
try:
if image_src.startswith('data:image/'):
image_type, image_content = image_src.split(',', 1)
image_type = re.findall(r'data:image/(\w+);base64', image_type)[0]
mime_image = MIMEImage(image_content, _subtype=image_type, _encoder=encoder_linelength)
mime_image = MIMEImage(image_content, _subtype=image_type, _encoder=encode_noop)
mime_image.add_header('Content-Transfer-Encoding', 'base64')
elif image_src.startswith('data:'):
logger.exception("ERROR creating MIME element %s[%s]" % (cid_id, image_src))

View File

@@ -120,5 +120,4 @@ def send_notification_mail(notification: Notification, user: User):
'html': body_html,
'sender': settings.MAIL_FROM,
'headers': {},
'user': user.pk
})

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