mirror of
https://github.com/pretix/pretix.git
synced 2025-12-20 16:32:26 +00:00
Compare commits
2 Commits
stripe-con
...
loadtest2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e769ba11e | ||
|
|
32e66aeb55 |
@@ -25,6 +25,8 @@ matrix:
|
|||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
- python: 3.5
|
- python: 3.5
|
||||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||||
|
- python: 3.7
|
||||||
|
env: JOB=plugins
|
||||||
- python: 3.7
|
- python: 3.7
|
||||||
env: JOB=doc-spelling
|
env: JOB=doc-spelling
|
||||||
- python: 3.7
|
- python: 3.7
|
||||||
|
|||||||
@@ -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 deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||||
COPY src /pretix/src
|
COPY src /pretix/src
|
||||||
|
|
||||||
RUN cd /pretix/src && pip3 install .
|
|
||||||
|
|
||||||
RUN chmod +x /usr/local/bin/pretix && \
|
RUN chmod +x /usr/local/bin/pretix && \
|
||||||
rm /etc/nginx/sites-enabled/default && \
|
rm /etc/nginx/sites-enabled/default && \
|
||||||
cd /pretix/src && \
|
cd /pretix/src && \
|
||||||
|
|||||||
@@ -78,15 +78,6 @@ Example::
|
|||||||
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
|
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
|
||||||
Defaults to ``off``.
|
Defaults to ``off``.
|
||||||
|
|
||||||
``obligatory_2fa``
|
|
||||||
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
|
|
||||||
Defaults to ``False``
|
|
||||||
|
|
||||||
``trust_x_forwarded_for``
|
|
||||||
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
|
|
||||||
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
|
|
||||||
Defaults to ``off``.
|
|
||||||
|
|
||||||
|
|
||||||
Locale settings
|
Locale settings
|
||||||
---------------
|
---------------
|
||||||
@@ -282,24 +273,6 @@ to speed up various operations::
|
|||||||
If redis is not configured, pretix will store sessions and locks in the database. If memcached
|
If redis is not configured, pretix will store sessions and locks in the database. If memcached
|
||||||
is configured, memcached will be used for caching instead of redis.
|
is configured, memcached will be used for caching instead of redis.
|
||||||
|
|
||||||
Translations
|
|
||||||
------------
|
|
||||||
|
|
||||||
pretix comes with a number of translations. Some of them are marked as "incubating", which means
|
|
||||||
they can usually only be selected in development mode. If you want to use them nevertheless, you
|
|
||||||
can activate them like this::
|
|
||||||
|
|
||||||
[languages]
|
|
||||||
allow_incubating=pt-br,da
|
|
||||||
|
|
||||||
You can also tell pretix about additional paths where it will search for translations::
|
|
||||||
|
|
||||||
[languages]
|
|
||||||
path=/path/to/my/translations
|
|
||||||
|
|
||||||
For a given language (e.g. ``pt-br``), pretix will then look in the
|
|
||||||
specific sub-folder, e.g. ``/path/to/my/translations/pt_BR/LC_MESSAGES/django.po``.
|
|
||||||
|
|
||||||
Celery task queue
|
Celery task queue
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|||||||
@@ -192,8 +192,7 @@ that you have a deep understanding of the semantics of your replication mechanis
|
|||||||
|
|
||||||
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
|
If you do have a replica, you *can* tell pretix about it :ref:`in your configuration <config-replica>`.
|
||||||
This way, pretix can offload complex read-only queries to the replica when it is safe to do so.
|
This way, pretix can offload complex read-only queries to the replica when it is safe to do so.
|
||||||
As of pretix 2.7, this is mainly used for search queries in the backend and for rendering the
|
As of pretix 2.6, this is mainly used for search queries in the backend and therefore does not really accelerate sales throughput, but we plan on expanding this in the future.
|
||||||
product list and event lists in the frontend, but we plan on expanding this in the future.
|
|
||||||
|
|
||||||
Therefore, for now our clear recommendation is: Try to scale your database vertically and put
|
Therefore, for now our clear recommendation is: Try to scale your database vertically and put
|
||||||
it on the most powerful machine you have available.
|
it on the most powerful machine you have available.
|
||||||
@@ -208,29 +207,30 @@ Having some memory available is good in case of e.g. lots of tasks queuing up du
|
|||||||
|
|
||||||
Feel free to set up a redis cluster for availability – but you won't need it for performance in a long time.
|
Feel free to set up a redis cluster for availability – but you won't need it for performance in a long time.
|
||||||
|
|
||||||
The limitations
|
The locking issue
|
||||||
---------------
|
-----------------
|
||||||
|
|
||||||
Up to a certain point, pretix scales really well. However, there are a few things that we consider
|
Currently, there is one big issue with scaling pretix. We take the reliability and correctness
|
||||||
even more important than scalability, and those are correctness and reliability. We want you to be
|
of pretix' core features very seriously and one part of this is that we want to make absolutely
|
||||||
able to trust that pretix will not sell more tickets than you intended or run into similar error
|
sure that pretix never sells the wrong number of tickets or tickets it otherwise shouldn't sell.
|
||||||
cases.
|
|
||||||
|
|
||||||
Combined with pretix' flexibility and complexity, especially around vouchers and quotas, this creates
|
However, due to pretix flexibility and complexity, ensuring this in a high-concurrency environment is really hard.
|
||||||
some hard issues. In many cases, we need to fall back to event-global locking for some actions which
|
We're not just counting down integers, since quota availability in pretix depends on a multitude of factors and requires a handful of complex database queries to calculate.
|
||||||
are likely to run with high concurrency and cause harm.
|
We can't rely on database-level locking alone to get this right.
|
||||||
|
|
||||||
For every event, only one of these locking actions can be run at the same time. Examples for this are
|
Therefore, we currently use a very drastic option:
|
||||||
adding products limited by a quota to a cart, adding items to a cart using a voucher or placing an order
|
During relevant actions that typically occur with high concurrency, such as creating carts and completing orders, we create a system-wide lock for the event and all other such
|
||||||
consisting of cart positions that don't have a valid reservation for much longer. In these cases, it is
|
actions need to wait. In essence, this means **for a given event we're only ever selling
|
||||||
currently not realistically possible to exceed selling **approx. 500 orders per minute per event**, even
|
one ticket at a time**.
|
||||||
if you add more hardware.
|
|
||||||
If you have an unlimited number of tickets, we can apply fewer locking and we've reached **approx.
|
|
||||||
1500 orders per minute per event** in benchmarks, although even more should be possible.
|
|
||||||
|
|
||||||
We're working to reduce the number of cases in which this is relevant and thereby improve the possible
|
Therefore, it is currently very unlikely that you will be able to exceed **300-400 tickets per minute per event**.
|
||||||
throughput. If you want to use pretix for an event with 10,000+ tickets that are likely to be sold out
|
|
||||||
within minutes, please get in touch to discuss possible solutions. We'll work something out for you!
|
For events up to a few thousand tickets, this isn't a problem and it's not even desirable to be able to sell much faster: If all tickets are reserved in the first minute, the shop shows a big "sold out" and everyone else goes away. Fifteen to thirty minutes later, depending on your settings, the first shopping carts will expire because people didn't actually go through with the purchase and tickets will become available again. This leads to a much more frustrating shopping experience for those trying to get a ticket than if tickets are sold at a slower pace and the first reservations expire before the last reservations are made. Not selling tickets through quickly (e.g. through a queue system) can do a lot to smooth out this process.
|
||||||
|
|
||||||
|
That said, we still want to fix this of course and make it possible to achieve much higher
|
||||||
|
throughput rates. We have some plans on how to soften this limitation, but they require
|
||||||
|
lots of time and effort to be realized. If you want to use pretix for an event with
|
||||||
|
10,000+ tickets that will be sold out within minutes, get in touch, we will make it work ;)
|
||||||
|
|
||||||
|
|
||||||
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/
|
.. _object storage cluster: https://behind.pretix.eu/2018/03/20/high-available-cdn/
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -18,4 +18,3 @@ in functionality over time.
|
|||||||
resources/index
|
resources/index
|
||||||
ratelimit
|
ratelimit
|
||||||
webhooks
|
webhooks
|
||||||
guides/index
|
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
pretix Hosted billing invoices
|
|
||||||
==============================
|
|
||||||
|
|
||||||
This endpoint allows you to access invoices you received for pretix Hosted. It only contains invoices created starting
|
|
||||||
November 2017.
|
|
||||||
|
|
||||||
.. note:: Only available on pretix Hosted, not on self-hosted pretix instances.
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
The resource contains the following public fields:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
invoice_number string Invoice number
|
|
||||||
date_issued date Invoice date
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/
|
|
||||||
|
|
||||||
Returns a list of all invoices to a given organizer.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/billing_invoices/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"invoice_number": "R2019002",
|
|
||||||
"date_issued": "2019-06-03"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_issued`` and
|
|
||||||
its reverse, ``-date_issued``. Default: ``date_issued``.
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/(invoice_number)/
|
|
||||||
|
|
||||||
Returns information on one invoice, identified by its invoice number.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/billing_invoices/R2019002/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"invoice_number": "R2019002",
|
|
||||||
"date_issued": "2019-06-03"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param invoice_number: The ``invoice_number`` field of the invoice to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/billing_invoices/(invoice_number)/download/
|
|
||||||
|
|
||||||
Download an invoice in PDF format.
|
|
||||||
|
|
||||||
.. warning:: After we created the invoices, they are placed in review with our accounting department. You will
|
|
||||||
already see them in the API at this point, but you are not able to download them until they completed
|
|
||||||
review and are sent to you via email. This usually takes a few hours. If you try to download them
|
|
||||||
in this time frame, you will receive a status code :http:statuscode:`423`.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/billing_invoices/R2019002/download/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/pdf
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param invoice_number: The ``invoice_number`` field of the invoice to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
:statuscode 423: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
|
||||||
seconds.
|
|
||||||
@@ -36,20 +36,12 @@ answers list of objects Answers to user
|
|||||||
├ question_identifier string The question's ``identifier`` field
|
├ question_identifier string The question's ``identifier`` field
|
||||||
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||||
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||||
seat objects The assigned seat. Can be ``null``.
|
|
||||||
├ id integer Internal ID of the seat instance
|
|
||||||
├ name string Human-readable seat name
|
|
||||||
└ seat_guid string Identifier of the seat within the seating plan
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 1.17
|
.. versionchanged:: 1.17
|
||||||
|
|
||||||
This resource has been added.
|
This resource has been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
This ``seat`` attribute has been added.
|
|
||||||
|
|
||||||
|
|
||||||
Cart position endpoints
|
Cart position endpoints
|
||||||
-----------------------
|
-----------------------
|
||||||
@@ -95,7 +87,6 @@ Cart position endpoints
|
|||||||
"datetime": "2018-06-11T10:00:00Z",
|
"datetime": "2018-06-11T10:00:00Z",
|
||||||
"expires": "2018-06-11T10:00:00Z",
|
"expires": "2018-06-11T10:00:00Z",
|
||||||
"includes_tax": true,
|
"includes_tax": true,
|
||||||
"seat": null,
|
|
||||||
"answers": []
|
"answers": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -141,7 +132,6 @@ Cart position endpoints
|
|||||||
"datetime": "2018-06-11T10:00:00Z",
|
"datetime": "2018-06-11T10:00:00Z",
|
||||||
"expires": "2018-06-11T10:00:00Z",
|
"expires": "2018-06-11T10:00:00Z",
|
||||||
"includes_tax": true,
|
"includes_tax": true,
|
||||||
"seat": null,
|
|
||||||
"answers": []
|
"answers": []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +178,6 @@ Cart position endpoints
|
|||||||
* ``item``
|
* ``item``
|
||||||
* ``variation`` (optional)
|
* ``variation`` (optional)
|
||||||
* ``price``
|
* ``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`` (optional)
|
* ``attendee_name`` **or** ``attendee_name_parts`` (optional)
|
||||||
* ``attendee_email`` (optional)
|
* ``attendee_email`` (optional)
|
||||||
* ``subevent`` (optional)
|
* ``subevent`` (optional)
|
||||||
@@ -207,7 +196,7 @@ Cart position endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/cartpositions/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"item": 1,
|
"item": 1,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/categories/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/categories/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": {"en": "Tickets"},
|
"name": {"en": "Tickets"},
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
.. spelling:: checkin
|
|
||||||
|
|
||||||
Check-in lists
|
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).
|
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).
|
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.
|
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
|
.. 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.
|
The ``include_pending`` field has been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
|
||||||
|
|
||||||
The ``auto_checkin_sales_channels`` field has been added.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -88,10 +81,7 @@ Endpoints
|
|||||||
"all_products": true,
|
"all_products": true,
|
||||||
"limit_products": [],
|
"limit_products": [],
|
||||||
"include_pending": false,
|
"include_pending": false,
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -132,10 +122,7 @@ Endpoints
|
|||||||
"all_products": true,
|
"all_products": true,
|
||||||
"limit_products": [],
|
"limit_products": [],
|
||||||
"include_pending": false,
|
"include_pending": false,
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -222,16 +209,13 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/checkinlists/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "VIP entry",
|
"name": "VIP entry",
|
||||||
"all_products": false,
|
"all_products": false,
|
||||||
"limit_products": [1, 2],
|
"limit_products": [1, 2],
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -250,10 +234,7 @@ Endpoints
|
|||||||
"all_products": false,
|
"all_products": false,
|
||||||
"limit_products": [1, 2],
|
"limit_products": [1, 2],
|
||||||
"include_pending": false,
|
"include_pending": false,
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
: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,
|
"all_products": false,
|
||||||
"limit_products": [1, 2],
|
"limit_products": [1, 2],
|
||||||
"include_pending": false,
|
"include_pending": false,
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
: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
|
``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.
|
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/
|
.. 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
|
Returns a list of all order positions within a given event. The result is the same as
|
||||||
@@ -423,12 +396,10 @@ Order position endpoints
|
|||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"seat": null,
|
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 1,
|
"list": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z"
|
||||||
"auto_checked_in": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
@@ -534,12 +505,10 @@ Order position endpoints
|
|||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"seat": null,
|
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 1,
|
"list": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z"
|
||||||
"auto_checked_in": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
@@ -577,8 +546,6 @@ Order position endpoints
|
|||||||
you do not implement question handling in your user interface, you **must**
|
you do not implement question handling in your user interface, you **must**
|
||||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||||
to ``true``.
|
to ``true``.
|
||||||
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
|
|
||||||
returned. Otherwise, canceled orders will return ``unpaid``.
|
|
||||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||||
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
|
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
|
||||||
questions that have not been filled. Defaults to ``false``.
|
questions that have not been filled. Defaults to ``false``.
|
||||||
@@ -607,7 +574,6 @@ Order position endpoints
|
|||||||
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
||||||
"datetime": null,
|
"datetime": null,
|
||||||
"questions_supported": true,
|
"questions_supported": true,
|
||||||
"canceled_supported": true,
|
|
||||||
"answers": {
|
"answers": {
|
||||||
"4": "XS"
|
"4": "XS"
|
||||||
}
|
}
|
||||||
@@ -691,9 +657,7 @@ Order position endpoints
|
|||||||
|
|
||||||
Possible error reasons:
|
Possible error reasons:
|
||||||
|
|
||||||
* ``unpaid`` - Ticket is not paid for
|
* ``unpaid`` - Ticket is not paid for or has been refunded
|
||||||
* ``canceled`` – Ticket is canceled or expired. This reason is only sent when your request sets
|
|
||||||
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
|
|
||||||
* ``already_redeemed`` - Ticket already has been redeemed
|
* ``already_redeemed`` - Ticket already has been redeemed
|
||||||
* ``product`` - Tickets with this product may not be scanned at this device
|
* ``product`` - Tickets with this product may not be scanned at this device
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,9 @@ presale_end datetime The date at whi
|
|||||||
location multi-lingual string The event location (or ``null``)
|
location multi-lingual string The event location (or ``null``)
|
||||||
has_subevents boolean ``true`` if the event series feature is active for this
|
has_subevents boolean ``true`` if the event series feature is active for this
|
||||||
event. Cannot change after event is created.
|
event. Cannot change after event is created.
|
||||||
meta_data object Values set for organizer-specific meta data parameters.
|
meta_data dict Values set for organizer-specific meta data parameters.
|
||||||
plugins list A list of package names of the enabled plugins for this
|
plugins list A list of package names of the enabled plugins for this
|
||||||
event.
|
event.
|
||||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
|
||||||
plan. Otherwise ``null``.
|
|
||||||
seat_category_mapping object An object mapping categories of the seating plan
|
|
||||||
(strings) to items in the event (integers or ``null``).
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -54,14 +50,6 @@ seat_category_mapping object An object mappi
|
|||||||
|
|
||||||
The ``testmode`` attribute has been added.
|
The ``testmode`` attribute has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2.8
|
|
||||||
|
|
||||||
When cloning events, the ``testmode`` attribute will now be cloned, too.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -107,8 +95,6 @@ Endpoints
|
|||||||
"location": null,
|
"location": null,
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer"
|
"pretix.plugins.banktransfer"
|
||||||
"pretix.plugins.stripe"
|
"pretix.plugins.stripe"
|
||||||
@@ -126,9 +112,6 @@ Endpoints
|
|||||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
|
||||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
|
||||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
|
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned. Event series are never returned.
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date_from`` and
|
|
||||||
``slug``. Keep in mind that ``date_from`` of event series does not really tell you anything.
|
|
||||||
Default: ``slug``.
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -170,8 +153,6 @@ Endpoints
|
|||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer"
|
"pretix.plugins.banktransfer"
|
||||||
@@ -203,7 +184,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": {"en": "Sample Conference"},
|
"name": {"en": "Sample Conference"},
|
||||||
@@ -217,8 +198,6 @@ Endpoints
|
|||||||
"is_public": false,
|
"is_public": false,
|
||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"location": null,
|
"location": null,
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -249,8 +228,6 @@ Endpoints
|
|||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -269,7 +246,7 @@ Endpoints
|
|||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/clone/
|
||||||
|
|
||||||
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
|
Creates a new event with properties as set in the request body. The properties that are copied are: 'is_public',
|
||||||
`testmode`, settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
settings, plugin settings, items, variations, add-ons, quotas, categories, tax rules, questions.
|
||||||
|
|
||||||
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
|
If the 'plugins' and/or 'is_public' fields are present in the post body this will determine their value. Otherwise
|
||||||
their value will be copied from the existing event.
|
their value will be copied from the existing event.
|
||||||
@@ -285,7 +262,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/clone/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/clone/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": {"en": "Sample Conference"},
|
"name": {"en": "Sample Conference"},
|
||||||
@@ -300,8 +277,6 @@ Endpoints
|
|||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -332,8 +307,6 @@ Endpoints
|
|||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.stripe",
|
"pretix.plugins.stripe",
|
||||||
@@ -362,7 +335,7 @@ Endpoints
|
|||||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/ HTTP/1.1
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -395,8 +368,6 @@ Endpoints
|
|||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"has_subevents": false,
|
"has_subevents": false,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"pretix.plugins.banktransfer",
|
"pretix.plugins.banktransfer",
|
||||||
|
|||||||
@@ -23,5 +23,3 @@ Resources and endpoints
|
|||||||
waitinglist
|
waitinglist
|
||||||
carts
|
carts
|
||||||
webhooks
|
webhooks
|
||||||
seatingplans
|
|
||||||
billing_invoices
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/addons/ HTTP/1.1
|
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/addons/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"addon_category": 1,
|
"addon_category": 1,
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/ HTTP/1.1
|
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"bundled_item": 3,
|
"bundled_item": 3,
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/variations/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"value": {"en": "Student"},
|
"value": {"en": "Student"},
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ available_from datetime The first date
|
|||||||
(or ``null``).
|
(or ``null``).
|
||||||
available_until datetime The last date time at which this item can be bought
|
available_until datetime The last date time at which this item can be bought
|
||||||
(or ``null``).
|
(or ``null``).
|
||||||
hidden_if_available integer The internal ID of a quota object, or ``null``. If
|
|
||||||
set, this item won't be shown publicly as long as this
|
|
||||||
quota is available.
|
|
||||||
require_voucher boolean If ``true``, this item can only be bought using a
|
require_voucher boolean If ``true``, this item can only be bought using a
|
||||||
voucher that is specifically assigned to this item.
|
voucher that is specifically assigned to this item.
|
||||||
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
||||||
@@ -75,10 +72,6 @@ generate_tickets boolean If ``false``, t
|
|||||||
non-admission or add-on product, regardless of event
|
non-admission or add-on product, regardless of event
|
||||||
settings. If this is ``null``, regular ticketing
|
settings. If this is ``null``, regular ticketing
|
||||||
rules apply.
|
rules apply.
|
||||||
allow_waitinglist boolean If ``false``, no waiting list will be shown for this
|
|
||||||
product when it is sold out.
|
|
||||||
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.
|
has_variations boolean Shows whether or not this item has variations.
|
||||||
variations list of objects A list with one object for each variation of this item.
|
variations list of objects A list with one object for each variation of this item.
|
||||||
Can be empty. Only writable during creation,
|
Can be empty. Only writable during creation,
|
||||||
@@ -149,10 +142,6 @@ bundles list of objects Definition of b
|
|||||||
|
|
||||||
The ``bundles`` and ``require_bundling`` attributes have been added.
|
The ``bundles`` and ``require_bundling`` attributes have been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -210,7 +199,6 @@ Endpoints
|
|||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
"hidden_if_available": null,
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -219,8 +207,6 @@ Endpoints
|
|||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"has_variations": false,
|
"has_variations": false,
|
||||||
"generate_tickets": null,
|
"generate_tickets": null,
|
||||||
"allow_waitinglist": true,
|
|
||||||
"show_quota_left": null,
|
|
||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
"require_bundling": false,
|
"require_bundling": false,
|
||||||
"variations": [
|
"variations": [
|
||||||
@@ -304,13 +290,10 @@ Endpoints
|
|||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
"hidden_if_available": null,
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
"generate_tickets": null,
|
"generate_tickets": null,
|
||||||
"allow_waitinglist": true,
|
|
||||||
"show_quota_left": null,
|
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
@@ -359,7 +342,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/items/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/items/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -379,13 +362,10 @@ Endpoints
|
|||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
"hidden_if_available": null,
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
"generate_tickets": null,
|
"generate_tickets": null,
|
||||||
"allow_waitinglist": true,
|
|
||||||
"show_quota_left": null,
|
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
@@ -441,15 +421,12 @@ Endpoints
|
|||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
"hidden_if_available": null,
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
"generate_tickets": null,
|
"generate_tickets": null,
|
||||||
"allow_waitinglist": true,
|
|
||||||
"show_quota_left": null,
|
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
"has_variations": true,
|
"has_variations": true,
|
||||||
"require_approval": false,
|
"require_approval": false,
|
||||||
@@ -535,12 +512,9 @@ Endpoints
|
|||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
"hidden_if_available": null,
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"generate_tickets": null,
|
"generate_tickets": null,
|
||||||
"allow_waitinglist": true,
|
|
||||||
"show_quota_left": null,
|
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
"min_per_order": null,
|
"min_per_order": null,
|
||||||
"max_per_order": null,
|
"max_per_order": null,
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ invoice_address object Invoice address
|
|||||||
├ street string Customer street
|
├ street string Customer street
|
||||||
├ zipcode string Customer ZIP code
|
├ zipcode string Customer ZIP code
|
||||||
├ city string Customer city
|
├ city string Customer city
|
||||||
├ country string Customer country code
|
├ country string Customer country
|
||||||
├ state string Customer state (ISO 3166-2 code). Only supported in
|
|
||||||
AU, BR, CA, CN, MY, MX, and US.
|
|
||||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||||
├ vat_id string Customer VAT ID
|
├ vat_id string Customer VAT ID
|
||||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
└ 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
|
needs approval by an organizer before it can
|
||||||
continue. If ``true`` and the order is canceled,
|
continue. If ``true`` and the order is canceled,
|
||||||
this order has been denied by the event organizer.
|
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)
|
payments list of objects List of payment processes (see below)
|
||||||
refunds list of objects List of refund processes (see below)
|
refunds list of objects List of refund processes (see below)
|
||||||
last_modified datetime Last modification of this object
|
last_modified datetime Last modification of this object
|
||||||
@@ -140,12 +137,6 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
|
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:
|
||||||
|
|
||||||
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
|
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
|
checkins list of objects List of check-ins with this ticket
|
||||||
├ list integer Internal ID of the check-in list
|
├ list integer Internal ID of the check-in list
|
||||||
├ datetime datetime Time of check-in
|
└ datetime datetime Time of check-in
|
||||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
|
||||||
downloads list of objects List of ticket download options
|
downloads list of objects List of ticket download options
|
||||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||||
└ url string Download URL
|
└ url string Download URL
|
||||||
@@ -186,10 +176,6 @@ answers list of objects Answers to user
|
|||||||
├ question_identifier string The question's ``identifier`` field
|
├ question_identifier string The question's ``identifier`` field
|
||||||
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||||
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
|
||||||
seat objects The assigned seat. Can be ``null``.
|
|
||||||
├ id integer Internal ID of the seat instance
|
|
||||||
├ name string Human-readable seat name
|
|
||||||
└ seat_guid string Identifier of the seat within the seating plan
|
|
||||||
pdf_data object Data object required for ticket PDF generation. By default,
|
pdf_data object Data object required for ticket PDF generation. By default,
|
||||||
this field is missing. It will be added only if you add the
|
this field is missing. It will be added only if you add the
|
||||||
``pdf_data=true`` query parameter to your request.
|
``pdf_data=true`` query parameter to your request.
|
||||||
@@ -211,14 +197,6 @@ pdf_data object Data object req
|
|||||||
|
|
||||||
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
|
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
The attribute ``seat`` has been added.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
|
||||||
|
|
||||||
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
|
|
||||||
|
|
||||||
.. _order-payment-resource:
|
.. _order-payment-resource:
|
||||||
|
|
||||||
Order payment resource
|
Order payment resource
|
||||||
@@ -235,27 +213,13 @@ amount money (string) Payment amount
|
|||||||
created datetime Date and time of creation of this payment
|
created datetime Date and time of creation of this payment
|
||||||
payment_date datetime Date and time of completion of this payment (or ``null``)
|
payment_date datetime Date and time of completion of this payment (or ``null``)
|
||||||
provider string Identification string of the payment provider
|
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
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
This resource has been added.
|
This resource has been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.1
|
.. _order-payment-resource:
|
||||||
|
|
||||||
The attributes ``payment_url`` and ``details`` have been added.
|
|
||||||
|
|
||||||
.. _order-refund-resource:
|
|
||||||
|
|
||||||
Order refund resource
|
Order refund resource
|
||||||
---------------------
|
---------------------
|
||||||
@@ -316,7 +280,6 @@ List of all orders
|
|||||||
"status": "p",
|
"status": "p",
|
||||||
"testmode": false,
|
"testmode": false,
|
||||||
"secret": "k24fiuwvu8kxz3y1",
|
"secret": "k24fiuwvu8kxz3y1",
|
||||||
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
|
|
||||||
"email": "tester@example.org",
|
"email": "tester@example.org",
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"sales_channel": "web",
|
"sales_channel": "web",
|
||||||
@@ -339,8 +302,7 @@ List of all orders
|
|||||||
"street": "Test street 12",
|
"street": "Test street 12",
|
||||||
"zipcode": "12345",
|
"zipcode": "12345",
|
||||||
"city": "Testington",
|
"city": "Testington",
|
||||||
"country": "DE",
|
"country": "Testikistan",
|
||||||
"state": "",
|
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false
|
"vat_id_validated": false
|
||||||
@@ -366,12 +328,10 @@ List of all orders
|
|||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"seat": null,
|
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z"
|
||||||
"auto_checked_in": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
@@ -404,8 +364,6 @@ List of all orders
|
|||||||
"amount": "23.00",
|
"amount": "23.00",
|
||||||
"created": "2017-12-01T10:00:00Z",
|
"created": "2017-12-01T10:00:00Z",
|
||||||
"payment_date": "2017-12-04T12:13:12Z",
|
"payment_date": "2017-12-04T12:13:12Z",
|
||||||
"payment_url": null,
|
|
||||||
"details": {},
|
|
||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -464,7 +422,6 @@ Fetching individual orders
|
|||||||
"status": "p",
|
"status": "p",
|
||||||
"testmode": false,
|
"testmode": false,
|
||||||
"secret": "k24fiuwvu8kxz3y1",
|
"secret": "k24fiuwvu8kxz3y1",
|
||||||
"url": "https://test.pretix.eu/dummy/dummy/order/ABC12/k24fiuwvu8kxz3y1/",
|
|
||||||
"email": "tester@example.org",
|
"email": "tester@example.org",
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"sales_channel": "web",
|
"sales_channel": "web",
|
||||||
@@ -487,8 +444,7 @@ Fetching individual orders
|
|||||||
"street": "Test street 12",
|
"street": "Test street 12",
|
||||||
"zipcode": "12345",
|
"zipcode": "12345",
|
||||||
"city": "Testington",
|
"city": "Testington",
|
||||||
"country": "DE",
|
"country": "Testikistan",
|
||||||
"state": "",
|
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false
|
"vat_id_validated": false
|
||||||
@@ -514,12 +470,10 @@ Fetching individual orders
|
|||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"seat": null,
|
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z"
|
||||||
"auto_checked_in": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
@@ -552,8 +506,6 @@ Fetching individual orders
|
|||||||
"amount": "23.00",
|
"amount": "23.00",
|
||||||
"created": "2017-12-01T10:00:00Z",
|
"created": "2017-12-01T10:00:00Z",
|
||||||
"payment_date": "2017-12-04T12:13:12Z",
|
"payment_date": "2017-12-04T12:13:12Z",
|
||||||
"payment_url": null,
|
|
||||||
"details": {},
|
|
||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -729,8 +681,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 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.
|
:statuscode 404: The requested order does not exist.
|
||||||
|
|
||||||
.. _rest-orders-create:
|
|
||||||
|
|
||||||
Creating orders
|
Creating orders
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
@@ -738,6 +688,8 @@ Creating orders
|
|||||||
|
|
||||||
Creates a new order.
|
Creates a new order.
|
||||||
|
|
||||||
|
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend,
|
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend,
|
||||||
@@ -756,17 +708,23 @@ 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 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 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
|
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
|
||||||
module
|
module
|
||||||
|
|
||||||
|
* does not send order confirmations via email
|
||||||
|
|
||||||
|
* does not support reverse charge taxation
|
||||||
|
|
||||||
* does not support file upload questions
|
* does not support file upload questions
|
||||||
|
|
||||||
You can supply the following fields of the resource:
|
You can supply the following fields of the resource:
|
||||||
@@ -779,15 +737,14 @@ Creating orders
|
|||||||
then call the ``mark_paid`` API method.
|
then call the ``mark_paid`` API method.
|
||||||
* ``testmode`` (optional) – Defaults to ``false``
|
* ``testmode`` (optional) – Defaults to ``false``
|
||||||
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
* ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the
|
||||||
order creation is successful. Any quotas or seats that become free by this operation will be credited to your order
|
order creation is successful. Any quotas that become free by this operation will be credited to your order
|
||||||
creation.
|
creation.
|
||||||
* ``email``
|
* ``email``
|
||||||
* ``locale``
|
* ``locale``
|
||||||
* ``sales_channel``
|
* ``sales_channel``
|
||||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
* ``payment_provider`` – The identifier of the payment provider set for this order. This needs to be an existing
|
||||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all
|
||||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
orders you create as paid.
|
||||||
zero, otherwise it is required.
|
|
||||||
* ``payment_info`` (optional) – You can pass a nested JSON object that will be set as the internal ``info``
|
* ``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
|
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
|
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
|
||||||
@@ -805,22 +762,16 @@ Creating orders
|
|||||||
* ``zipcode``
|
* ``zipcode``
|
||||||
* ``city``
|
* ``city``
|
||||||
* ``country``
|
* ``country``
|
||||||
* ``state``
|
|
||||||
* ``internal_reference``
|
* ``internal_reference``
|
||||||
* ``vat_id``
|
* ``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``
|
* ``positions``
|
||||||
|
|
||||||
* ``positionid`` (optional, see below)
|
* ``positionid`` (optional, see below)
|
||||||
* ``item``
|
* ``item``
|
||||||
* ``variation``
|
* ``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``
|
* ``attendee_name`` **or** ``attendee_name_parts``
|
||||||
* ``voucher`` (optional, the ``code`` attribute of a valid voucher)
|
|
||||||
* ``attendee_email``
|
* ``attendee_email``
|
||||||
* ``secret`` (optional)
|
* ``secret`` (optional)
|
||||||
* ``addon_to`` (optional, see below)
|
* ``addon_to`` (optional, see below)
|
||||||
@@ -840,8 +791,6 @@ Creating orders
|
|||||||
* ``tax_rule``
|
* ``tax_rule``
|
||||||
|
|
||||||
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
* ``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
|
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
|
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||||
@@ -879,7 +828,6 @@ Creating orders
|
|||||||
"zipcode": "12345",
|
"zipcode": "12345",
|
||||||
"city": "Sample City",
|
"city": "Sample City",
|
||||||
"country": "UK",
|
"country": "UK",
|
||||||
"state": "",
|
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": ""
|
"vat_id": ""
|
||||||
},
|
},
|
||||||
@@ -903,7 +851,7 @@ Creating orders
|
|||||||
],
|
],
|
||||||
"subevent": null
|
"subevent": null
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -1294,11 +1242,6 @@ List of all order positions
|
|||||||
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
|
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
|
||||||
``pseudonymization_id``.
|
``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.
|
.. note:: Individually canceled order positions are currently not visible via the API at all.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
||||||
@@ -1344,14 +1287,12 @@ List of all order positions
|
|||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"seat": null,
|
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z"
|
||||||
"auto_checked_in": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
@@ -1448,12 +1389,10 @@ Fetching individual positions
|
|||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
"seat": null,
|
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z"
|
||||||
"auto_checked_in": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
@@ -1596,8 +1535,6 @@ Order payment endpoints
|
|||||||
"amount": "23.00",
|
"amount": "23.00",
|
||||||
"created": "2017-12-01T10:00:00Z",
|
"created": "2017-12-01T10:00:00Z",
|
||||||
"payment_date": "2017-12-04T12:13:12Z",
|
"payment_date": "2017-12-04T12:13:12Z",
|
||||||
"payment_url": null,
|
|
||||||
"details": {},
|
|
||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1638,8 +1575,6 @@ Order payment endpoints
|
|||||||
"amount": "23.00",
|
"amount": "23.00",
|
||||||
"created": "2017-12-01T10:00:00Z",
|
"created": "2017-12-01T10:00:00Z",
|
||||||
"payment_date": "2017-12-04T12:13:12Z",
|
"payment_date": "2017-12-04T12:13:12Z",
|
||||||
"payment_url": null,
|
|
||||||
"details": {},
|
|
||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``slug`` and
|
|
||||||
``name``. Default: ``slug``.
|
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ type string The expected ty
|
|||||||
* ``D`` – date
|
* ``D`` – date
|
||||||
* ``H`` – time
|
* ``H`` – time
|
||||||
* ``W`` – date and time
|
* ``W`` – date and time
|
||||||
* ``CC`` – country code (ISO 3666-1 alpha-2)
|
|
||||||
required boolean If ``true``, the question needs to be filled out.
|
required boolean If ``true``, the question needs to be filled out.
|
||||||
position integer An integer, used for sorting
|
position integer An integer, used for sorting
|
||||||
items list of integers List of item IDs this question is assigned to.
|
items list of integers List of item IDs this question is assigned to.
|
||||||
@@ -39,10 +38,6 @@ identifier string An arbitrary st
|
|||||||
ask_during_checkin boolean If ``true``, this question will not be asked while
|
ask_during_checkin boolean If ``true``, this question will not be asked while
|
||||||
buying the ticket, but will show up when redeeming
|
buying the ticket, but will show up when redeeming
|
||||||
the ticket instead.
|
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
|
options list of objects In case of question type ``C`` or ``M``, this lists the
|
||||||
available objects. Only writable during creation,
|
available objects. Only writable during creation,
|
||||||
use separate endpoint to modify this later.
|
use separate endpoint to modify this later.
|
||||||
@@ -56,12 +51,11 @@ dependency_question integer Internal ID of
|
|||||||
this attribute is set to the value given in
|
this attribute is set to the value given in
|
||||||
``dependency_value``. This cannot be combined with
|
``dependency_value``. This cannot be combined with
|
||||||
``ask_during_checkin``.
|
``ask_during_checkin``.
|
||||||
dependency_values list of strings If ``dependency_question`` is set to a boolean
|
dependency_value string The value ``dependency_question`` needs to be set to.
|
||||||
question, this should be ``["True"]`` or ``["False"]``.
|
If ``dependency_question`` is set to a boolean
|
||||||
Otherwise, it should be a list of ``identifier`` values
|
question, this should be ``"true"`` or ``"false"``.
|
||||||
of question options.
|
Otherwise, it should be the ``identifier`` of a
|
||||||
dependency_value string An old version of ``dependency_values`` that only allows
|
question option.
|
||||||
for one value. **Deprecated.**
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 1.12
|
.. versionchanged:: 1.12
|
||||||
@@ -74,18 +68,6 @@ dependency_value string An old version
|
|||||||
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
|
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
|
||||||
options resource. The ``position`` attribute has been added to the options resource.
|
options resource. The ``position`` attribute has been added to the options resource.
|
||||||
|
|
||||||
.. versionchanged:: 2.7
|
|
||||||
|
|
||||||
The attribute ``hidden`` and the question type ``CC`` have been added.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
The attribute ``dependency_values`` has been added.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.1
|
|
||||||
|
|
||||||
The attribute ``print_on_invoice`` has been added.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -128,11 +110,8 @@ Endpoints
|
|||||||
"position": 1,
|
"position": 1,
|
||||||
"identifier": "WY3TP9SL",
|
"identifier": "WY3TP9SL",
|
||||||
"ask_during_checkin": false,
|
"ask_during_checkin": false,
|
||||||
"hidden": false,
|
|
||||||
"print_on_invoice": false,
|
|
||||||
"dependency_question": null,
|
"dependency_question": null,
|
||||||
"dependency_value": null,
|
"dependency_value": null,
|
||||||
"dependency_values": [],
|
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -198,11 +177,8 @@ Endpoints
|
|||||||
"position": 1,
|
"position": 1,
|
||||||
"identifier": "WY3TP9SL",
|
"identifier": "WY3TP9SL",
|
||||||
"ask_during_checkin": false,
|
"ask_during_checkin": false,
|
||||||
"hidden": false,
|
|
||||||
"print_on_invoice": false,
|
|
||||||
"dependency_question": null,
|
"dependency_question": null,
|
||||||
"dependency_value": null,
|
"dependency_value": null,
|
||||||
"dependency_values": [],
|
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -243,7 +219,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/questions/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/questions/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"question": {"en": "T-Shirt size"},
|
"question": {"en": "T-Shirt size"},
|
||||||
@@ -252,10 +228,8 @@ Endpoints
|
|||||||
"items": [1, 2],
|
"items": [1, 2],
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"ask_during_checkin": false,
|
"ask_during_checkin": false,
|
||||||
"hidden": false,
|
|
||||||
"print_on_invoice": false,
|
|
||||||
"dependency_question": null,
|
"dependency_question": null,
|
||||||
"dependency_values": [],
|
"dependency_value": null,
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"answer": {"en": "S"}
|
"answer": {"en": "S"}
|
||||||
@@ -287,11 +261,8 @@ Endpoints
|
|||||||
"position": 1,
|
"position": 1,
|
||||||
"identifier": "WY3TP9SL",
|
"identifier": "WY3TP9SL",
|
||||||
"ask_during_checkin": false,
|
"ask_during_checkin": false,
|
||||||
"hidden": false,
|
|
||||||
"print_on_invoice": false,
|
|
||||||
"dependency_question": null,
|
"dependency_question": null,
|
||||||
"dependency_value": null,
|
"dependency_value": null,
|
||||||
"dependency_values": [],
|
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -361,11 +332,8 @@ Endpoints
|
|||||||
"position": 2,
|
"position": 2,
|
||||||
"identifier": "WY3TP9SL",
|
"identifier": "WY3TP9SL",
|
||||||
"ask_during_checkin": false,
|
"ask_during_checkin": false,
|
||||||
"hidden": false,
|
|
||||||
"print_on_invoice": false,
|
|
||||||
"dependency_question": null,
|
"dependency_question": null,
|
||||||
"dependency_value": null,
|
"dependency_value": null,
|
||||||
"dependency_values": [],
|
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -20,22 +20,12 @@ size integer The size of the
|
|||||||
items list of integers List of item IDs this quota acts on.
|
items list of integers List of item IDs this quota acts on.
|
||||||
variations list of integers List of item variation IDs this quota acts on.
|
variations list of integers List of item variation IDs this quota acts on.
|
||||||
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
|
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
|
||||||
close_when_sold_out boolean If ``true``, the quota will "close" as soon as it is
|
|
||||||
sold out once. Even if tickets become available again,
|
|
||||||
they will not be sold unless the quota is set to open
|
|
||||||
again.
|
|
||||||
closed boolean Whether the quota is currently closed (see above
|
|
||||||
field).
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 1.10
|
.. versionchanged:: 1.10
|
||||||
|
|
||||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
The attributes ``close_when_sold_out`` and ``closed`` have been added.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -71,9 +61,7 @@ Endpoints
|
|||||||
"size": 200,
|
"size": 200,
|
||||||
"items": [1, 2],
|
"items": [1, 2],
|
||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"close_when_sold_out": false,
|
|
||||||
"closed": false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -114,9 +102,7 @@ Endpoints
|
|||||||
"size": 200,
|
"size": 200,
|
||||||
"items": [1, 2],
|
"items": [1, 2],
|
||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"close_when_sold_out": false,
|
|
||||||
"closed": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -137,16 +123,14 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/quotas/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/quotas/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Ticket Quota",
|
"name": "Ticket Quota",
|
||||||
"size": 200,
|
"size": 200,
|
||||||
"items": [1, 2],
|
"items": [1, 2],
|
||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"close_when_sold_out": false,
|
|
||||||
"closed": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -163,9 +147,7 @@ Endpoints
|
|||||||
"size": 200,
|
"size": 200,
|
||||||
"items": [1, 2],
|
"items": [1, 2],
|
||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"close_when_sold_out": false,
|
|
||||||
"closed": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
||||||
@@ -218,9 +200,7 @@ Endpoints
|
|||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
"subevent": null,
|
"subevent": null
|
||||||
"close_when_sold_out": false,
|
|
||||||
"closed": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
.. _`rest-seatingplans`:
|
|
||||||
|
|
||||||
Seating plans
|
|
||||||
=============
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
The seating plan resource contains the following public fields:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Internal ID of the plan
|
|
||||||
name string Human-readable name of the plan
|
|
||||||
layout object JSON representation of the seating plan. These
|
|
||||||
representations follow a JSON schema that currently
|
|
||||||
still evolves. The version in use can be found `here`_.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
This endpoint has been added.
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/seatingplans/
|
|
||||||
|
|
||||||
Returns a list of all seating plans within a given organizer.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/seatingplans/ 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": 2,
|
|
||||||
"name": "Main plan",
|
|
||||||
"layout": { … }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
: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)/seatingplans/(id)/
|
|
||||||
|
|
||||||
Returns information on one plan, identified by its ID.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/seatingplans/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": 2,
|
|
||||||
"name": "Main plan",
|
|
||||||
"layout": { … }
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param id: The ``id`` field of the seating plan 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)/seatingplans/
|
|
||||||
|
|
||||||
Creates a new seating plan
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/seatingplans/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Main plan",
|
|
||||||
"layout": { … }
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 201 Created
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "Main plan",
|
|
||||||
"layout": { … }
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a seating plan for
|
|
||||||
:statuscode 201: no error
|
|
||||||
:statuscode 400: The seating plan 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)/seatingplans/(id)/
|
|
||||||
|
|
||||||
Update a plan. 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`` field. **You can not change a plan while it is in use for
|
|
||||||
any events.**
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
PATCH /api/v1/organizers/bigevents/seatingplans/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
Content-Length: 94
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Old plan"
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Old plan",
|
|
||||||
"layout": { … }
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
|
||||||
:param id: The ``id`` field of the plan to modify
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: The plan 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 **or** the plan is currently in use.
|
|
||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/seatingplans/(id)/
|
|
||||||
|
|
||||||
Delete a plan. You can not delete plans which are currently in use by any events.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
DELETE /api/v1/organizers/bigevents/seatingplans/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
Vary: Accept
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
|
||||||
:param id: The ``id`` field of the plan to delete
|
|
||||||
:statuscode 204: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the plan is currently in use.
|
|
||||||
|
|
||||||
|
|
||||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/seating/seating-plan.schema.json
|
|
||||||
@@ -36,11 +36,7 @@ variation_price_overrides list of objects List of variati
|
|||||||
the default price
|
the default price
|
||||||
├ variation integer The internal variation ID
|
├ variation integer The internal variation ID
|
||||||
└ price money (string) The price or ``null`` for the default price
|
└ price money (string) The price or ``null`` for the default price
|
||||||
meta_data object Values set for organizer-specific meta data parameters.
|
meta_data dict Values set for organizer-specific meta data parameters.
|
||||||
seating_plan integer If reserved seating is in use, the ID of a seating
|
|
||||||
plan. Otherwise ``null``.
|
|
||||||
seat_category_mapping object An object mapping categories of the seating plan
|
|
||||||
(strings) to items in the event (integers or ``null``).
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
@@ -58,10 +54,6 @@ seat_category_mapping object An object mappi
|
|||||||
|
|
||||||
The attribute ``is_public`` has been added.
|
The attribute ``is_public`` has been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -101,8 +93,6 @@ Endpoints
|
|||||||
"date_admission": null,
|
"date_admission": null,
|
||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"location": null,
|
"location": null,
|
||||||
"item_price_overrides": [
|
"item_price_overrides": [
|
||||||
{
|
{
|
||||||
@@ -140,7 +130,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": {"en": "First Sample Conference"},
|
"name": {"en": "First Sample Conference"},
|
||||||
@@ -152,8 +142,6 @@ Endpoints
|
|||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"item_price_overrides": [
|
"item_price_overrides": [
|
||||||
{
|
{
|
||||||
"item": 2,
|
"item": 2,
|
||||||
@@ -184,8 +172,6 @@ Endpoints
|
|||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"item_price_overrides": [
|
"item_price_overrides": [
|
||||||
{
|
{
|
||||||
"item": 2,
|
"item": 2,
|
||||||
@@ -237,8 +223,6 @@ Endpoints
|
|||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"item_price_overrides": [
|
"item_price_overrides": [
|
||||||
{
|
{
|
||||||
"item": 2,
|
"item": 2,
|
||||||
@@ -271,7 +255,7 @@ Endpoints
|
|||||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": {"en": "New Subevent Name"},
|
"name": {"en": "New Subevent Name"},
|
||||||
@@ -303,8 +287,6 @@ Endpoints
|
|||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"item_price_overrides": [
|
"item_price_overrides": [
|
||||||
{
|
{
|
||||||
"item": 2,
|
"item": 2,
|
||||||
@@ -389,8 +371,6 @@ Endpoints
|
|||||||
"presale_start": null,
|
"presale_start": null,
|
||||||
"presale_end": null,
|
"presale_end": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
"seating_plan": null,
|
|
||||||
"seat_category_mapping": {},
|
|
||||||
"item_price_overrides": [
|
"item_price_overrides": [
|
||||||
{
|
{
|
||||||
"item": 2,
|
"item": 2,
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ quota integer An ID of a quot
|
|||||||
tag string A string that is used for grouping vouchers
|
tag string A string that is used for grouping vouchers
|
||||||
comment string An internal comment on the voucher
|
comment string An internal comment on the voucher
|
||||||
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
||||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -49,10 +48,6 @@ show_hidden_items boolean Only if set to
|
|||||||
|
|
||||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
|
|
||||||
The attribute ``show_hidden_items`` has been added.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ Endpoints
|
|||||||
POST /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
|
POST /api/v1/organizers/bigevents/webhooks/ HTTP/1.1
|
||||||
Host: pretix.eu
|
Host: pretix.eu
|
||||||
Accept: application/json, text/javascript
|
Accept: application/json, text/javascript
|
||||||
Content-Type: application/json
|
Content: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ source_suffix = '.rst'
|
|||||||
#source_encoding = 'utf-8-sig'
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'index'
|
master_doc = 'contents'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'pretix'
|
project = 'pretix'
|
||||||
@@ -234,7 +234,7 @@ latex_elements = {
|
|||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
('index', 'pretix.tex', 'pretix Documentation',
|
('contents', 'pretix.tex', 'pretix Documentation',
|
||||||
'Raphael Michel', 'manual'),
|
'Raphael Michel', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -101,12 +101,9 @@ The template is passed the following context variables:
|
|||||||
The ``Event`` object
|
The ``Event`` object
|
||||||
|
|
||||||
``signature`` (optional, only if configured)
|
``signature`` (optional, only if configured)
|
||||||
The signature with event organizer contact details as markdown (render with ``{{ signature|safe }}``)
|
The body as markdown (render with ``{{ signature|safe }}``)
|
||||||
|
|
||||||
``order`` (optional, only if applicable)
|
``order`` (optional, only if applicable)
|
||||||
The ``Order`` object
|
The ``Order`` object
|
||||||
|
|
||||||
``position`` (optional, only if applicable)
|
|
||||||
The ``OrderPosition`` object
|
|
||||||
|
|
||||||
.. _inlinestyler: https://pypi.org/project/inlinestyler/
|
.. _inlinestyler: https://pypi.org/project/inlinestyler/
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Core
|
|||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
: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
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -20,13 +20,13 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: validate_cart, 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
|
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_canceled, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info
|
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
@@ -49,11 +49,11 @@ Backend
|
|||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
||||||
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms, item_formsets
|
order_info, event_settings_widget, oauth_application_registered, order_position_buttons
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events
|
:members: logentry_display, logentry_object_link, requiredaction_display
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ Contents:
|
|||||||
payment
|
payment
|
||||||
payment_2.0
|
payment_2.0
|
||||||
email
|
email
|
||||||
placeholder
|
|
||||||
invoice
|
invoice
|
||||||
shredder
|
shredder
|
||||||
customview
|
customview
|
||||||
|
|||||||
@@ -108,8 +108,6 @@ The provider class
|
|||||||
|
|
||||||
.. automethod:: execute_refund
|
.. automethod:: execute_refund
|
||||||
|
|
||||||
.. automethod:: api_payment_details
|
|
||||||
|
|
||||||
.. automethod:: shred_payment_info
|
.. automethod:: shred_payment_info
|
||||||
|
|
||||||
.. autoattribute:: is_implicit
|
.. autoattribute:: is_implicit
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
.. highlight:: python
|
|
||||||
:linenothreshold: 5
|
|
||||||
|
|
||||||
Writing an HTML 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'
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -49,19 +49,15 @@ description string A more verbose description of what your
|
|||||||
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
visible boolean (optional) ``True`` by default, can hide a plugin so it cannot be normally activated.
|
||||||
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
restricted boolean (optional) ``False`` by default, restricts a plugin such that it can only be enabled
|
||||||
for an event by system administrators / superusers.
|
for an event by system administrators / superusers.
|
||||||
compatibility string Specifier for compatible pretix versions.
|
|
||||||
================== ==================== ===========================================================
|
================== ==================== ===========================================================
|
||||||
|
|
||||||
A working example would be::
|
A working example would be::
|
||||||
|
|
||||||
try:
|
from django.apps import AppConfig
|
||||||
from pretix.base.plugins import PluginConfig
|
|
||||||
except ImportError:
|
|
||||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class PaypalApp(PluginConfig):
|
class PaypalApp(AppConfig):
|
||||||
name = 'pretix_paypal'
|
name = 'pretix_paypal'
|
||||||
verbose_name = _("PayPal")
|
verbose_name = _("PayPal")
|
||||||
|
|
||||||
@@ -72,7 +68,6 @@ A working example would be::
|
|||||||
visible = True
|
visible = True
|
||||||
restricted = False
|
restricted = False
|
||||||
description = _("This plugin allows you to receive payments via PayPal")
|
description = _("This plugin allows you to receive payments via PayPal")
|
||||||
compatibility = "pretix>=2.7.0"
|
|
||||||
|
|
||||||
|
|
||||||
default_app_config = 'pretix_paypal.PaypalApp'
|
default_app_config = 'pretix_paypal.PaypalApp'
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ The shredder class
|
|||||||
|
|
||||||
.. class:: pretix.base.shredder.BaseDataShredder
|
.. 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
|
The default constructor sets this property to the event we are currently
|
||||||
working for.
|
working for.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Organizers and events
|
|||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.Event
|
.. autoclass:: pretix.base.models.Event
|
||||||
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, invoice_renderer, settings
|
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.SubEvent
|
.. autoclass:: pretix.base.models.SubEvent
|
||||||
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running
|
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running
|
||||||
|
|||||||
@@ -21,12 +21,10 @@ Your should install the following on your system:
|
|||||||
* Python 3.5 or newer
|
* Python 3.5 or newer
|
||||||
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
||||||
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
||||||
* On Debian/Ubuntu: ``python-venv`` for Python 3 (Debian package: ``python3-venv``)
|
|
||||||
* ``libffi`` (Debian package: ``libffi-dev``)
|
* ``libffi`` (Debian package: ``libffi-dev``)
|
||||||
* ``libssl`` (Debian package: ``libssl-dev``)
|
* ``libssl`` (Debian package: ``libssl-dev``)
|
||||||
* ``libxml2`` (Debian package ``libxml2-dev``)
|
* ``libxml2`` (Debian package ``libxml2-dev``)
|
||||||
* ``libxslt`` (Debian package ``libxslt1-dev``)
|
* ``libxslt`` (Debian package ``libxslt1-dev``)
|
||||||
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
|
|
||||||
* ``msgfmt`` (Debian package ``gettext``)
|
* ``msgfmt`` (Debian package ``gettext``)
|
||||||
* ``git``
|
* ``git``
|
||||||
|
|
||||||
@@ -65,7 +63,9 @@ Then, create the local database::
|
|||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
|
|
||||||
A first user with username ``admin@localhost`` and password ``admin`` will be automatically
|
A first user with username ``admin@localhost`` and password ``admin`` will be automatically
|
||||||
created.
|
created. If you want to generate more test data, run::
|
||||||
|
|
||||||
|
python make_testdata.py
|
||||||
|
|
||||||
If you want to see pretix in a different language than English, you have to compile our language
|
If you want to see pretix in a different language than English, you have to compile our language
|
||||||
files::
|
files::
|
||||||
@@ -81,7 +81,8 @@ To run the local development webserver, execute::
|
|||||||
and head to http://localhost:8000/
|
and head to http://localhost:8000/
|
||||||
|
|
||||||
As we did not implement an overall front page yet, you need to go directly to
|
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
|
.. 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
|
port (for example because you develop on `pretixdroid`_), you can check
|
||||||
|
|||||||
@@ -36,17 +36,13 @@ eu
|
|||||||
filename
|
filename
|
||||||
filesystem
|
filesystem
|
||||||
fontawesome
|
fontawesome
|
||||||
formset
|
|
||||||
formsets
|
|
||||||
frontend
|
frontend
|
||||||
frontpage
|
frontpage
|
||||||
gettext
|
gettext
|
||||||
gunicorn
|
gunicorn
|
||||||
guid
|
|
||||||
hardcoded
|
hardcoded
|
||||||
hostname
|
hostname
|
||||||
idempotency
|
idempotency
|
||||||
iframe
|
|
||||||
incrementing
|
incrementing
|
||||||
inofficial
|
inofficial
|
||||||
invalidations
|
invalidations
|
||||||
@@ -100,12 +96,10 @@ renderer
|
|||||||
renderers
|
renderers
|
||||||
reportlab
|
reportlab
|
||||||
SaaS
|
SaaS
|
||||||
scalability
|
|
||||||
screenshot
|
screenshot
|
||||||
scss
|
scss
|
||||||
searchable
|
searchable
|
||||||
selectable
|
selectable
|
||||||
serializable
|
|
||||||
serializers
|
serializers
|
||||||
serializers
|
serializers
|
||||||
sexualized
|
sexualized
|
||||||
@@ -139,7 +133,6 @@ versa
|
|||||||
versioning
|
versioning
|
||||||
viewset
|
viewset
|
||||||
viewsets
|
viewsets
|
||||||
waitinglist
|
|
||||||
webhook
|
webhook
|
||||||
webhooks
|
webhooks
|
||||||
webserver
|
webserver
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ In addition, you will need quotas. If you do not care how many of your tickets a
|
|||||||
|
|
||||||
If you want to limit the number of student tickets to 50 to ensure a certain minimum revenue, but do not want to limit the number of regular tickets artificially, we suggest you to create the same quota of 200 that is linked to both products, and then create a **second quota** of 50 that is only linked to the student ticket. This way, the system will reduce both quotas whenever a student ticket is sold and only the larger quota when a regular ticket is sold.
|
If you want to limit the number of student tickets to 50 to ensure a certain minimum revenue, but do not want to limit the number of regular tickets artificially, we suggest you to create the same quota of 200 that is linked to both products, and then create a **second quota** of 50 that is only linked to the student ticket. This way, the system will reduce both quotas whenever a student ticket is sold and only the larger quota when a regular ticket is sold.
|
||||||
|
|
||||||
Use case: Early-bird tiers based on dates
|
Use case: Early-bird tiers
|
||||||
-----------------------------------------
|
--------------------------
|
||||||
|
|
||||||
Let's say you run a conference that has the following pricing scheme:
|
Let's say you run a conference that has the following pricing scheme:
|
||||||
|
|
||||||
@@ -58,53 +58,9 @@ Of course, you could just set up one product and change its price at the given d
|
|||||||
|
|
||||||
Create three products (e.g. "super early bird", "early bird", "regular ticket") with the respective prices and one shared quota of your total event capacity. Then, set the **available from** and **available until** configuration fields of the products to automatically turn them on and off based on the current date.
|
Create three products (e.g. "super early bird", "early bird", "regular ticket") with the respective prices and one shared quota of your total event capacity. Then, set the **available from** and **available until** configuration fields of the products to automatically turn them on and off based on the current date.
|
||||||
|
|
||||||
Use case: Early-bird tiers based on ticket numbers
|
.. note::
|
||||||
--------------------------------------------------
|
|
||||||
|
|
||||||
Let's say you run a conference with 400 tickets that has the following pricing scheme:
|
pretix currently can't do early-bird tiers based on **ticket number** instead of time. We're planning this feature for later in 2019. For now, you'll need to monitor that manually.
|
||||||
|
|
||||||
* First 100 tickets ("super early bird"): € 450
|
|
||||||
* Next 100 tickets ("early bird"): € 550
|
|
||||||
* Remaining tickets ("regular"): € 650
|
|
||||||
|
|
||||||
First of all, create three products:
|
|
||||||
|
|
||||||
* "Super early bird ticket"
|
|
||||||
* "Early bird ticket"
|
|
||||||
* "Regular ticket"
|
|
||||||
|
|
||||||
Then, create three quotas:
|
|
||||||
|
|
||||||
* "Super early bird" with a **size of 100** and the "Super early bird ticket" product selected. At "Advanced options",
|
|
||||||
select the box "Close this quota permanently once it is sold out".
|
|
||||||
|
|
||||||
* "Early bird and lower" with a **size of 200** and both of the "Super early bird ticket" and "Early bird ticket"
|
|
||||||
products selected. At "Advanced options", select the box "Close this quota permanently once it is sold out".
|
|
||||||
|
|
||||||
* "All participants" with a **size of 400**, all three products selected and **no additional options**.
|
|
||||||
|
|
||||||
Next, modify the product "Regular ticket". In the section "Availability", you should look for the option "Only show
|
|
||||||
after sellout of" and select your quota "Early bird and lower". Do the same for the "Early bird ticket" with the quota
|
|
||||||
"Super early bird ticket".
|
|
||||||
|
|
||||||
This will ensure the following things:
|
|
||||||
|
|
||||||
* Each ticket level is only visible after the previous level is sold out.
|
|
||||||
|
|
||||||
* As soon as one level is really sold out, it's not coming back, because the quota "closes", i.e. locks in place.
|
|
||||||
|
|
||||||
* By creating a total quota of 400 with all tickets included, you can still make sure to sell the maximum number of
|
|
||||||
tickets, even if e.g. early-bird tickets are canceled.
|
|
||||||
|
|
||||||
Optionally, if you want to hide the early bird prices once they are sold out, go to "Settings", then "Display" and
|
|
||||||
select "Hide all products that are sold out". Of course, it might be a nice idea to keep showing the prices to remind
|
|
||||||
people to buy earlier next time ;)
|
|
||||||
|
|
||||||
Please note that there might be short time intervals where the prices switch back and forth: When the last early bird
|
|
||||||
tickets are in someone's cart (but not yet sold!), the early bird tickets will show as "Reserved" and the regular
|
|
||||||
tickets start showing up. However, if the customers holding the reservations do not complete their order,
|
|
||||||
the early bird tickets will become available again. This is not avoidable if we want to prevent malicious users
|
|
||||||
from blocking all the cheap tickets without an actual sale happening.
|
|
||||||
|
|
||||||
Use case: Up-selling of ticket extras
|
Use case: Up-selling of ticket extras
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
@@ -129,14 +85,8 @@ Use case: Conference with workshops
|
|||||||
|
|
||||||
When running a conference, you might also organize a number of workshops with smaller capacity. To be able to plan, it would be great to know which workshops an attendee plans to attend.
|
When running a conference, you might also organize a number of workshops with smaller capacity. To be able to plan, it would be great to know which workshops an attendee plans to attend.
|
||||||
|
|
||||||
Option A: Questions
|
|
||||||
"""""""""""""""""""
|
|
||||||
|
|
||||||
Your first and simplest option is to just create a multiple-choice question. This has the upside of making it easy for users to change their mind later on, but will not allow you to restrict the number of attendees signing up for a given workshop – or even charge extra for a given workshop.
|
Your first and simplest option is to just create a multiple-choice question. This has the upside of making it easy for users to change their mind later on, but will not allow you to restrict the number of attendees signing up for a given workshop – or even charge extra for a given workshop.
|
||||||
|
|
||||||
Option B: Add-on products with fixed time slots
|
|
||||||
"""""""""""""""""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
The usually better option is to go with add-on products. Let's take for example the following conference schedule, in which the lecture can be attended by anyone, but the workshops only have space for 20 persons each:
|
The usually better option is to go with add-on products. Let's take for example the following conference schedule, in which the lecture can be attended by anyone, but the workshops only have space for 20 persons each:
|
||||||
|
|
||||||
==================== =================================== ===================================
|
==================== =================================== ===================================
|
||||||
@@ -167,42 +117,6 @@ Assuming you already created one or more products for your general conference ad
|
|||||||
|
|
||||||
* One add-on configuration on your base product that allows users to choose between 0 and 2 products from the category "Workshops"
|
* One add-on configuration on your base product that allows users to choose between 0 and 2 products from the category "Workshops"
|
||||||
|
|
||||||
Option C: Add-on products with variable time slots
|
|
||||||
""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
The above option only works if your conference uses fixed time slots and every workshop uses exactly one time slot. If
|
|
||||||
your schedule looks like this, it's not going to work great:
|
|
||||||
|
|
||||||
+-------------+------------+-----------+
|
|
||||||
| Time | Room A | Room B |
|
|
||||||
+=============+============+===========+
|
|
||||||
| 09:00-11:00 | Talk 1 | Long |
|
|
||||||
+-------------+------------+ Workshop 1|
|
|
||||||
| 11:00-13:00 | Talk 2 | |
|
|
||||||
+-------------+------------+-----------+
|
|
||||||
| 14:00-16:00 | Long | Talk 3 |
|
|
||||||
+-------------+ workshop 2 +-----------+
|
|
||||||
| 16:00-18:00 | | Talk 4 |
|
|
||||||
+-------------+------------+-----------+
|
|
||||||
|
|
||||||
In this case, we recommend that you go to *Settings*, then *Plugins* and activate the plugin **Agenda constraints**.
|
|
||||||
|
|
||||||
Then, create a product (without variations) for every single part that should be bookable (talks 1-4 and long workshops
|
|
||||||
1 and 2) as well as appropriate quotas for each of them.
|
|
||||||
|
|
||||||
All of these products should be part of the same category. In your base product (e.g. your conference ticket), you
|
|
||||||
can then create an add-on product configuration allowing users to add products from this category.
|
|
||||||
|
|
||||||
If you edit these products, you will be able to enter the "Start date" and "End date" of the talk or workshop close
|
|
||||||
to the bottom of the page. If you fill in these values, pretix will automatically ensure no overlapping talks are
|
|
||||||
booked.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This option is currently only available on pretix Hosted. If you are interested in using it with pretix Enterprise,
|
|
||||||
please contact sales@pretix.eu.
|
|
||||||
|
|
||||||
|
|
||||||
Use case: Discounted packages
|
Use case: Discounted packages
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -143,11 +143,6 @@ You can see an example here:
|
|||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
You can filter events by meta data attributes. You can create those attributes in your order profile and set their values in both event and series date
|
|
||||||
settings. For example, if you set up a meta data property called "Promoted" that you set to "Yes" on some events, you can pass a filter like this::
|
|
||||||
|
|
||||||
<pretix-widget event="https://pretix.eu/demo/series/" style="list" filter="attr[Promoted]=Yes"></pretix-widget>
|
|
||||||
|
|
||||||
pretix Button
|
pretix Button
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -274,9 +269,6 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
|||||||
};
|
};
|
||||||
</script>
|
</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
|
.. versionchanged:: 2.3
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,3 @@ recursive-include pretix/plugins/ticketoutputpdf/templates *
|
|||||||
recursive-include pretix/plugins/ticketoutputpdf/static *
|
recursive-include pretix/plugins/ticketoutputpdf/static *
|
||||||
recursive-include pretix/plugins/badges/templates *
|
recursive-include pretix/plugins/badges/templates *
|
||||||
recursive-include pretix/plugins/badges/static *
|
recursive-include pretix/plugins/badges/static *
|
||||||
recursive-include pretix/plugins/returnurl/templates *
|
|
||||||
recursive-include pretix/plugins/returnurl/static *
|
|
||||||
|
|||||||
71
src/make_testdata.py
Normal file
71
src/make_testdata.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from pretix.base.models import * # NOQA
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
if Organizer.objects.exists():
|
||||||
|
print("There already is data in your DB!")
|
||||||
|
sys.exit(0)
|
||||||
|
user = User.objects.get_or_create(
|
||||||
|
email='admin@localhost',
|
||||||
|
)[0]
|
||||||
|
user.set_password('admin')
|
||||||
|
user.save()
|
||||||
|
organizer = Organizer.objects.create(
|
||||||
|
name='BigEvents LLC', slug='bigevents'
|
||||||
|
)
|
||||||
|
year = now().year + 1
|
||||||
|
event = Event.objects.create(
|
||||||
|
organizer=organizer, name='Demo Conference {}'.format(year),
|
||||||
|
slug=year, currency='EUR', live=True,
|
||||||
|
date_from=datetime(year, 9, 4, 17, 0, 0),
|
||||||
|
date_to=datetime(year, 9, 6, 17, 0, 0),
|
||||||
|
)
|
||||||
|
t = Team.objects.get_or_create(
|
||||||
|
organizer=organizer, name='Admin Team',
|
||||||
|
all_events=True, can_create_events=True, can_change_teams=True,
|
||||||
|
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
|
||||||
|
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
|
||||||
|
)
|
||||||
|
t[0].members.add(user)
|
||||||
|
cat_tickets = ItemCategory.objects.create(
|
||||||
|
event=event, name='Tickets'
|
||||||
|
)
|
||||||
|
cat_merch = ItemCategory.objects.create(
|
||||||
|
event=event, name='Merchandise'
|
||||||
|
)
|
||||||
|
question = Question.objects.create(
|
||||||
|
event=event, question='Age',
|
||||||
|
type=Question.TYPE_NUMBER, required=False
|
||||||
|
)
|
||||||
|
tr19 = event.tax_rules.create(rate=19)
|
||||||
|
item_ticket = Item.objects.create(
|
||||||
|
event=event, category=cat_tickets, name='Ticket',
|
||||||
|
default_price=23, tax_rule=tr19, admission=True
|
||||||
|
)
|
||||||
|
item_ticket.questions.add(question)
|
||||||
|
item_shirt = Item.objects.create(
|
||||||
|
event=event, category=cat_merch, name='T-Shirt',
|
||||||
|
default_price=15, tax_rule=tr19
|
||||||
|
)
|
||||||
|
var_s = ItemVariation.objects.create(item=item_shirt, value='S')
|
||||||
|
var_m = ItemVariation.objects.create(item=item_shirt, value='M')
|
||||||
|
var_l = ItemVariation.objects.create(item=item_shirt, value='L')
|
||||||
|
ticket_quota = Quota.objects.create(
|
||||||
|
event=event, name='Ticket quota', size=400,
|
||||||
|
)
|
||||||
|
ticket_quota.items.add(item_ticket)
|
||||||
|
ticket_shirts = Quota.objects.create(
|
||||||
|
event=event, name='Shirt quota', size=200,
|
||||||
|
)
|
||||||
|
ticket_quota.items.add(item_shirt)
|
||||||
|
ticket_quota.variations.add(var_s, var_m, var_l)
|
||||||
@@ -1 +1 @@
|
|||||||
__version__ = "3.3.0.dev0"
|
__version__ = "2.6.0"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
def authenticate_credentials(self, key):
|
def authenticate_credentials(self, key):
|
||||||
model = self.get_model()
|
model = self.get_model()
|
||||||
try:
|
try:
|
||||||
with scopes_disabled():
|
|
||||||
device = model.objects.select_related('organizer').get(api_token=key)
|
device = model.objects.select_related('organizer').get(api_token=key)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
|||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.base.models import Device, Event, User
|
from pretix.base.models import Device, Event, User
|
||||||
from pretix.base.models.auth import SuperuserPermissionSet
|
from pretix.base.models.auth import SuperuserPermissionSet
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||||
)
|
)
|
||||||
@@ -50,6 +50,9 @@ class EventPermission(BasePermission):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
elif 'organizer' in request.resolver_match.kwargs:
|
elif 'organizer' in request.resolver_match.kwargs:
|
||||||
|
request.organizer = Organizer.objects.filter(
|
||||||
|
slug=request.resolver_match.kwargs['organizer'],
|
||||||
|
).first()
|
||||||
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
|
if not request.organizer or not perm_holder.has_organizer_permission(request.organizer, request=request):
|
||||||
return False
|
return False
|
||||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ from hashlib import sha1
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
from django.urls import resolve
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_scopes import scope
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from pretix.api.models import ApiCall
|
from pretix.api.models import ApiCall
|
||||||
from pretix.base.models import Organizer
|
|
||||||
|
|
||||||
|
|
||||||
class IdempotencyMiddleware:
|
class IdempotencyMiddleware:
|
||||||
@@ -92,21 +89,3 @@ class IdempotencyMiddleware:
|
|||||||
for k, v in json.loads(call.response_headers).values():
|
for k, v in json.loads(call.response_headers).values():
|
||||||
r[k] = v
|
r[k] = v
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
class ApiScopeMiddleware:
|
|
||||||
def __init__(self, get_response):
|
|
||||||
self.get_response = get_response
|
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest):
|
|
||||||
if not request.path.startswith('/api/'):
|
|
||||||
return self.get_response(request)
|
|
||||||
|
|
||||||
url = resolve(request.path_info)
|
|
||||||
if 'organizer' in url.kwargs:
|
|
||||||
request.organizer = Organizer.objects.filter(
|
|
||||||
slug=url.kwargs['organizer'],
|
|
||||||
).first()
|
|
||||||
|
|
||||||
with scope(organizer=getattr(request, 'organizer', None)):
|
|
||||||
return self.get_response(request)
|
|
||||||
|
|||||||
@@ -8,33 +8,31 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
AnswerCreateSerializer, AnswerSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Quota, Seat
|
from pretix.base.models import Quota
|
||||||
from pretix.base.models.orders import CartPosition
|
from pretix.base.models.orders import CartPosition
|
||||||
|
|
||||||
|
|
||||||
class CartPositionSerializer(I18nAwareModelSerializer):
|
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
seat = InlineSeatSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CartPosition
|
model = CartPosition
|
||||||
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
'attendee_email', 'voucher', 'addon_to', 'subevent', 'datetime', 'expires', 'includes_tax',
|
||||||
'answers', 'seat')
|
'answers',)
|
||||||
|
|
||||||
|
|
||||||
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||||
answers = AnswerCreateSerializer(many=True, required=False)
|
answers = AnswerCreateSerializer(many=True, required=False)
|
||||||
expires = serializers.DateTimeField(required=False)
|
expires = serializers.DateTimeField(required=False)
|
||||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||||
seat = serializers.CharField(required=False, allow_null=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CartPosition
|
model = CartPosition
|
||||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat')
|
'subevent', 'expires', 'includes_tax', 'answers',)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
answers_data = validated_data.pop('answers')
|
answers_data = validated_data.pop('answers')
|
||||||
@@ -73,24 +71,6 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
validated_data['attendee_name_parts'] = {
|
validated_data['attendee_name_parts'] = {
|
||||||
'_legacy': attendee_name
|
'_legacy': attendee_name
|
||||||
}
|
}
|
||||||
|
|
||||||
seated = validated_data.get('item').seat_category_mappings.filter(subevent=validated_data.get('subevent')).exists()
|
|
||||||
if validated_data.get('seat'):
|
|
||||||
if not seated:
|
|
||||||
raise ValidationError('The specified product does not allow to choose a seat.')
|
|
||||||
try:
|
|
||||||
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():
|
|
||||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
|
||||||
elif seated:
|
|
||||||
raise ValidationError('The specified product requires to choose a seat.')
|
|
||||||
|
|
||||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||||
|
|
||||||
for answ_data in answers_data:
|
for answ_data in answers_data:
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from rest_framework import serializers
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.channels import get_all_sales_channels
|
|
||||||
from pretix.base.models import CheckinList
|
from pretix.base.models import CheckinList
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||||
'include_pending', 'auto_checkin_sales_channels')
|
'include_pending')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -36,8 +35,4 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('subevent'):
|
if full_data.get('subevent'):
|
||||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
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
|
return data
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|||||||
from pretix.base.models import Event, TaxRule
|
from pretix.base.models import Event, TaxRule
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||||
from pretix.base.services.seating import (
|
|
||||||
SeatProtected, generate_seats, validate_plan_change,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MetaDataField(Field):
|
class MetaDataField(Field):
|
||||||
@@ -29,22 +26,6 @@ class MetaDataField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SeatCategoryMappingField(Field):
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
qs = value.seat_category_mappings.all()
|
|
||||||
if isinstance(value, Event):
|
|
||||||
qs = qs.filter(subevent=None)
|
|
||||||
return {
|
|
||||||
v.layout_category: v.product_id for v in qs
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
return {
|
|
||||||
'seat_category_mapping': data or {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PluginsField(Field):
|
class PluginsField(Field):
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
@@ -64,14 +45,12 @@ class PluginsField(Field):
|
|||||||
class EventSerializer(I18nAwareModelSerializer):
|
class EventSerializer(I18nAwareModelSerializer):
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
plugins = PluginsField(required=False, source='*')
|
plugins = PluginsField(required=False, source='*')
|
||||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||||
'presale_end', 'location', 'has_subevents', 'meta_data', 'seating_plan',
|
'presale_end', 'location', 'has_subevents', 'meta_data', 'plugins')
|
||||||
'plugins', 'seat_category_mapping')
|
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -82,9 +61,6 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
||||||
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
||||||
|
|
||||||
if full_data.get('has_subevents') and full_data.get('seating_plan'):
|
|
||||||
raise ValidationError('Event series should not directly be assigned a seating plan.')
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_has_subevents(self, value):
|
def validate_has_subevents(self, value):
|
||||||
@@ -116,27 +92,6 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_seating_plan(self, value):
|
|
||||||
if value and value.organizer != self.context['request'].organizer:
|
|
||||||
raise ValidationError('Invalid seating plan.')
|
|
||||||
if self.instance and self.instance.pk:
|
|
||||||
try:
|
|
||||||
validate_plan_change(self.instance, None, value)
|
|
||||||
except SeatProtected as e:
|
|
||||||
raise ValidationError(str(e))
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate_seat_category_mapping(self, value):
|
|
||||||
if value and value['seat_category_mapping'] and (not self.instance or not self.instance.pk):
|
|
||||||
raise ValidationError('You cannot specify seat category mappings on event creation.')
|
|
||||||
item_cache = {i.pk: i for i in self.instance.items.all()}
|
|
||||||
result = {}
|
|
||||||
for k, item in value['seat_category_mapping'].items():
|
|
||||||
if item not in item_cache:
|
|
||||||
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
|
|
||||||
result[k] = item_cache[item]
|
|
||||||
return {'seat_category_mapping': result}
|
|
||||||
|
|
||||||
def validate_plugins(self, value):
|
def validate_plugins(self, value):
|
||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
@@ -154,7 +109,6 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
validated_data.pop('seat_category_mapping', None)
|
|
||||||
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||||
event = super().create(validated_data)
|
event = super().create(validated_data)
|
||||||
|
|
||||||
@@ -166,10 +120,6 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
value=value
|
value=value
|
||||||
)
|
)
|
||||||
|
|
||||||
# Seats
|
|
||||||
if event.seating_plan:
|
|
||||||
generate_seats(event, None, event.seating_plan, {})
|
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
event.set_active_plugins(plugins)
|
event.set_active_plugins(plugins)
|
||||||
@@ -181,7 +131,6 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
plugins = validated_data.pop('plugins', None)
|
plugins = validated_data.pop('plugins', None)
|
||||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
|
||||||
event = super().update(instance, validated_data)
|
event = super().update(instance, validated_data)
|
||||||
|
|
||||||
# Meta data
|
# Meta data
|
||||||
@@ -202,29 +151,6 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
if prop.name not in meta_data:
|
if prop.name not in meta_data:
|
||||||
current_object.delete()
|
current_object.delete()
|
||||||
|
|
||||||
# Seats
|
|
||||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
|
||||||
current_mappings = {
|
|
||||||
m.layout_category: m
|
|
||||||
for m in event.seat_category_mappings.filter(subevent=None)
|
|
||||||
}
|
|
||||||
if not event.seating_plan:
|
|
||||||
seat_category_mapping = {}
|
|
||||||
for key, value in seat_category_mapping.items():
|
|
||||||
if key in current_mappings:
|
|
||||||
m = current_mappings.pop(key)
|
|
||||||
m.product = value
|
|
||||||
m.save()
|
|
||||||
else:
|
|
||||||
event.seat_category_mappings.create(product=value, layout_category=key)
|
|
||||||
for m in current_mappings.values():
|
|
||||||
m.delete()
|
|
||||||
if 'seating_plan' in validated_data or seat_category_mapping is not None:
|
|
||||||
generate_seats(event, None, event.seating_plan, {
|
|
||||||
m.layout_category: m.product
|
|
||||||
for m in event.seat_category_mappings.select_related('product').filter(subevent=None)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
event.set_active_plugins(plugins)
|
event.set_active_plugins(plugins)
|
||||||
@@ -238,7 +164,6 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
plugins = validated_data.pop('plugins', None)
|
plugins = validated_data.pop('plugins', None)
|
||||||
is_public = validated_data.pop('is_public', None)
|
is_public = validated_data.pop('is_public', None)
|
||||||
testmode = validated_data.pop('testmode', None)
|
|
||||||
new_event = super().create(validated_data)
|
new_event = super().create(validated_data)
|
||||||
|
|
||||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||||
@@ -248,8 +173,6 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
new_event.set_active_plugins(plugins)
|
new_event.set_active_plugins(plugins)
|
||||||
if is_public is not None:
|
if is_public is not None:
|
||||||
new_event.is_public = is_public
|
new_event.is_public = is_public
|
||||||
if testmode is not None:
|
|
||||||
new_event.testmode = testmode
|
|
||||||
new_event.save()
|
new_event.save()
|
||||||
|
|
||||||
return new_event
|
return new_event
|
||||||
@@ -270,15 +193,14 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
class SubEventSerializer(I18nAwareModelSerializer):
|
class SubEventSerializer(I18nAwareModelSerializer):
|
||||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
||||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
||||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
|
||||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
meta_data = MetaDataField(source='*')
|
meta_data = MetaDataField(source='*')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||||
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
|
'presale_start', 'presale_end', 'location', 'event', 'is_public',
|
||||||
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
|
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -300,25 +222,6 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
def validate_variation_price_overrides(self, data):
|
def validate_variation_price_overrides(self, data):
|
||||||
return list(filter(lambda i: 'variation' in i, data))
|
return list(filter(lambda i: 'variation' in i, data))
|
||||||
|
|
||||||
def validate_seating_plan(self, value):
|
|
||||||
if value and value.organizer != self.context['request'].organizer:
|
|
||||||
raise ValidationError('Invalid seating plan.')
|
|
||||||
if self.instance and self.instance.pk:
|
|
||||||
try:
|
|
||||||
validate_plan_change(self.context['request'].event, self.instance, value)
|
|
||||||
except SeatProtected as e:
|
|
||||||
raise ValidationError(str(e))
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate_seat_category_mapping(self, value):
|
|
||||||
item_cache = {i.pk: i for i in self.context['request'].event.items.all()}
|
|
||||||
result = {}
|
|
||||||
for k, item in value['seat_category_mapping'].items():
|
|
||||||
if item not in item_cache:
|
|
||||||
raise ValidationError('Item \'{id}\' does not exist.'.format(id=item))
|
|
||||||
result[k] = item_cache[item]
|
|
||||||
return {'seat_category_mapping': result}
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def meta_properties(self):
|
def meta_properties(self):
|
||||||
return {
|
return {
|
||||||
@@ -336,7 +239,6 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
|
||||||
subevent = super().create(validated_data)
|
subevent = super().create(validated_data)
|
||||||
|
|
||||||
for item_price_override_data in item_price_overrides_data:
|
for item_price_override_data in item_price_overrides_data:
|
||||||
@@ -352,18 +254,6 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
value=value
|
value=value
|
||||||
)
|
)
|
||||||
|
|
||||||
# Seats
|
|
||||||
if subevent.seating_plan:
|
|
||||||
if seat_category_mapping is not None:
|
|
||||||
for key, value in seat_category_mapping.items():
|
|
||||||
self.context['request'].event.seat_category_mappings.create(
|
|
||||||
product=value, layout_category=key, subevent=subevent
|
|
||||||
)
|
|
||||||
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
|
|
||||||
m.layout_category: m.product
|
|
||||||
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
|
|
||||||
})
|
|
||||||
|
|
||||||
return subevent
|
return subevent
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@@ -371,7 +261,6 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
|
||||||
subevent = super().update(instance, validated_data)
|
subevent = super().update(instance, validated_data)
|
||||||
|
|
||||||
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
||||||
@@ -408,31 +297,6 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
if prop.name not in meta_data:
|
if prop.name not in meta_data:
|
||||||
current_object.delete()
|
current_object.delete()
|
||||||
|
|
||||||
# Seats
|
|
||||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
|
||||||
current_mappings = {
|
|
||||||
m.layout_category: m
|
|
||||||
for m in self.context['request'].event.seat_category_mappings.filter(subevent=subevent)
|
|
||||||
}
|
|
||||||
if not subevent.seating_plan:
|
|
||||||
seat_category_mapping = {}
|
|
||||||
for key, value in seat_category_mapping.items():
|
|
||||||
if key in current_mappings:
|
|
||||||
m = current_mappings.pop(key)
|
|
||||||
m.product = value
|
|
||||||
m.save()
|
|
||||||
else:
|
|
||||||
self.context['request'].event.seat_category_mappings.create(
|
|
||||||
product=value, layout_category=key, subevent=subevent
|
|
||||||
)
|
|
||||||
for m in current_mappings.values():
|
|
||||||
m.delete()
|
|
||||||
if 'seating_plan' in validated_data or seat_category_mapping is not None:
|
|
||||||
generate_seats(self.context['request'].event, subevent, subevent.seating_plan, {
|
|
||||||
m.layout_category: m.product
|
|
||||||
for m in self.context['request'].event.seat_category_mappings.select_related('product').filter(subevent=subevent)
|
|
||||||
})
|
|
||||||
|
|
||||||
return subevent
|
return subevent
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
'position', 'picture', 'available_from', 'available_until',
|
'position', 'picture', 'available_from', 'available_until',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets')
|
||||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist')
|
|
||||||
read_only_fields = ('has_variations', 'picture')
|
read_only_fields = ('has_variations', 'picture')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
@@ -201,25 +200,14 @@ class InlineQuestionOptionSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'identifier', 'answer', 'position')
|
fields = ('id', 'identifier', 'answer', 'position')
|
||||||
|
|
||||||
|
|
||||||
class LegacyDependencyValueField(serializers.CharField):
|
|
||||||
|
|
||||||
def to_representation(self, obj):
|
|
||||||
return obj[0] if obj else None
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
return [data] if data else []
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionSerializer(I18nAwareModelSerializer):
|
class QuestionSerializer(I18nAwareModelSerializer):
|
||||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||||
identifier = serializers.CharField(allow_null=True)
|
identifier = serializers.CharField(allow_null=True)
|
||||||
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value')
|
||||||
'hidden', 'dependency_value', 'print_on_invoice')
|
|
||||||
|
|
||||||
def validate_identifier(self, value):
|
def validate_identifier(self, value):
|
||||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||||
@@ -273,7 +261,6 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||||
items = validated_data.pop('items')
|
items = validated_data.pop('items')
|
||||||
|
|
||||||
question = Question.objects.create(**validated_data)
|
question = Question.objects.create(**validated_data)
|
||||||
question.items.set(items)
|
question.items.set(items)
|
||||||
for opt_data in options_data:
|
for opt_data in options_data:
|
||||||
@@ -285,7 +272,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Quota
|
model = Quota
|
||||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out')
|
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import json
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pycountry
|
|
||||||
from django.db.models import F, Q
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy
|
from django.utils.translation import ugettext_lazy
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
@@ -17,17 +15,13 @@ from pretix.base.channels import get_all_sales_channels
|
|||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
||||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, Voucher,
|
OrderPosition, Question, QuestionAnswer, SubEvent,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
)
|
)
|
||||||
from pretix.base.pdf import get_variables
|
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.base.signals import register_ticket_outputs
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
|
||||||
|
|
||||||
|
|
||||||
class CompatibleCountryField(serializers.Field):
|
class CompatibleCountryField(serializers.Field):
|
||||||
@@ -48,8 +42,8 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||||
'state', 'vat_id', 'vat_id_validated', 'internal_reference')
|
'vat_id', 'vat_id_validated', 'internal_reference')
|
||||||
read_only_fields = ('last_modified',)
|
read_only_fields = ('last_modified', 'vat_id_validated')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -64,24 +58,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
||||||
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
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
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -95,13 +71,6 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
|||||||
return [o.identifier for o in instance.options.all()]
|
return [o.identifier for o in instance.options.all()]
|
||||||
|
|
||||||
|
|
||||||
class InlineSeatSerializer(I18nAwareModelSerializer):
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Seat
|
|
||||||
fields = ('id', 'name', 'seat_guid')
|
|
||||||
|
|
||||||
|
|
||||||
class AnswerSerializer(I18nAwareModelSerializer):
|
class AnswerSerializer(I18nAwareModelSerializer):
|
||||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||||
@@ -114,7 +83,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
|||||||
class CheckinSerializer(I18nAwareModelSerializer):
|
class CheckinSerializer(I18nAwareModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Checkin
|
model = Checkin
|
||||||
fields = ('datetime', 'list', 'auto_checked_in')
|
fields = ('datetime', 'list')
|
||||||
|
|
||||||
|
|
||||||
class OrderDownloadsField(serializers.Field):
|
class OrderDownloadsField(serializers.Field):
|
||||||
@@ -197,13 +166,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
downloads = PositionDownloadsField(source='*')
|
downloads = PositionDownloadsField(source='*')
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
pdf_data = PdfDataSerializer(source='*')
|
pdf_data = PdfDataSerializer(source='*')
|
||||||
seat = InlineSeatSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat')
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -286,33 +254,10 @@ class OrderFeeSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
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):
|
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||||
payment_url = PaymentURLField(source='*', allow_null=True, read_only=True)
|
|
||||||
details = PaymentDetailsField(source='*', allow_null=True, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPayment
|
model = OrderPayment
|
||||||
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url',
|
fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider')
|
||||||
'details')
|
|
||||||
|
|
||||||
|
|
||||||
class OrderRefundSerializer(I18nAwareModelSerializer):
|
class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||||
@@ -323,14 +268,6 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
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):
|
class OrderSerializer(I18nAwareModelSerializer):
|
||||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||||
@@ -340,15 +277,13 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
refunds = OrderRefundSerializer(many=True, read_only=True)
|
refunds = OrderRefundSerializer(many=True, read_only=True)
|
||||||
payment_date = OrderPaymentDateField(source='*', read_only=True)
|
payment_date = OrderPaymentDateField(source='*', read_only=True)
|
||||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||||
url = OrderURLField(source='*', read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = (
|
fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
||||||
'url'
|
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
@@ -370,6 +305,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||||
update_fields = ['comment', 'checkin_attention', 'email', 'locale']
|
update_fields = ['comment', 'checkin_attention', 'email', 'locale']
|
||||||
|
print(validated_data)
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -387,7 +323,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
ia = instance.invoice_address
|
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
|
ia.vat_id_validated = False
|
||||||
self.fields['invoice_address'].update(ia, iadata)
|
self.fields['invoice_address'].update(ia, iadata)
|
||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
@@ -494,16 +430,11 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
addon_to = serializers.IntegerField(required=False, allow_null=True)
|
||||||
secret = serializers.CharField(required=False)
|
secret = serializers.CharField(required=False)
|
||||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
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:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher')
|
'secret', 'addon_to', 'subevent', 'answers')
|
||||||
|
|
||||||
def validate_secret(self, secret):
|
def validate_secret(self, secret):
|
||||||
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
|
||||||
@@ -577,7 +508,7 @@ class CompatibleJSONField(serializers.JSONField):
|
|||||||
|
|
||||||
class OrderCreateSerializer(I18nAwareModelSerializer):
|
class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||||
invoice_address = InvoiceAddressSerializer(required=False)
|
invoice_address = InvoiceAddressSerializer(required=False)
|
||||||
positions = OrderPositionCreateSerializer(many=True, required=True)
|
positions = OrderPositionCreateSerializer(many=True, required=False)
|
||||||
fees = OrderFeeCreateSerializer(many=True, required=False)
|
fees = OrderFeeCreateSerializer(many=True, required=False)
|
||||||
status = serializers.ChoiceField(choices=(
|
status = serializers.ChoiceField(choices=(
|
||||||
('n', Order.STATUS_PENDING),
|
('n', Order.STATUS_PENDING),
|
||||||
@@ -589,26 +520,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
min_length=5
|
min_length=5
|
||||||
)
|
)
|
||||||
comment = serializers.CharField(required=False, allow_blank=True)
|
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)
|
payment_info = CompatibleJSONField(required=False)
|
||||||
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
consume_carts = serializers.ListField(child=serializers.CharField(), required=False)
|
||||||
force = serializers.BooleanField(default=False, required=False)
|
force = serializers.BooleanField(default=False, required=False)
|
||||||
payment_date = serializers.DateTimeField(required=False, allow_null=True)
|
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:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force')
|
||||||
'force', 'send_mail')
|
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
|
||||||
return None
|
|
||||||
if pp not in self.context['event'].get_payment_providers():
|
if pp not in self.context['event'].get_payment_providers():
|
||||||
raise ValidationError('The given payment provider is not known.')
|
raise ValidationError('The given payment provider is not known.')
|
||||||
return pp
|
return pp
|
||||||
@@ -667,9 +590,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
|
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
|
||||||
for p in data
|
for p in data
|
||||||
]
|
]
|
||||||
else:
|
|
||||||
for i, p in enumerate(data):
|
|
||||||
p['positionid'] = i + 1
|
|
||||||
|
|
||||||
if any(errs):
|
if any(errs):
|
||||||
raise ValidationError(errs)
|
raise ValidationError(errs)
|
||||||
@@ -678,11 +598,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||||
positions_data = validated_data.pop('positions') if 'positions' 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_info = validated_data.pop('payment_info', '{}')
|
||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
self._send_mail = validated_data.pop('send_mail', False)
|
|
||||||
|
|
||||||
if 'invoice_address' in validated_data:
|
if 'invoice_address' in validated_data:
|
||||||
iadata = validated_data.pop('invoice_address')
|
iadata = validated_data.pop('invoice_address')
|
||||||
@@ -696,16 +615,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
ia = None
|
ia = None
|
||||||
|
|
||||||
with self.context['event'].lock() as now_dt:
|
with self.context['event'].lock() as now_dt:
|
||||||
free_seats = set()
|
quotadiff = Counter()
|
||||||
seats_seen = set()
|
|
||||||
consume_carts = validated_data.pop('consume_carts', [])
|
consume_carts = validated_data.pop('consume_carts', [])
|
||||||
delete_cps = []
|
delete_cps = []
|
||||||
quota_avail_cache = {}
|
quota_avail_cache = {}
|
||||||
voucher_usage = Counter()
|
|
||||||
if consume_carts:
|
if consume_carts:
|
||||||
for cp in CartPosition.objects.filter(
|
for cp in CartPosition.objects.filter(event=self.context['event'], cart_id__in=consume_carts):
|
||||||
event=self.context['event'], cart_id__in=consume_carts, expires__gt=now()
|
|
||||||
):
|
|
||||||
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
quotas = (cp.variation.quotas.filter(subevent=cp.subevent)
|
||||||
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
if cp.variation else cp.item.quotas.filter(subevent=cp.subevent))
|
||||||
for quota in quotas:
|
for quota in quotas:
|
||||||
@@ -713,64 +629,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
quota_avail_cache[quota] = list(quota.availability())
|
quota_avail_cache[quota] = list(quota.availability())
|
||||||
if quota_avail_cache[quota][1] is not None:
|
if quota_avail_cache[quota][1] is not None:
|
||||||
quota_avail_cache[quota][1] += 1
|
quota_avail_cache[quota][1] += 1
|
||||||
if cp.voucher:
|
|
||||||
voucher_usage[cp.voucher] -= 1
|
|
||||||
if cp.expires > now_dt:
|
if cp.expires > now_dt:
|
||||||
if cp.seat:
|
quotadiff.subtract(quotas)
|
||||||
free_seats.add(cp.seat)
|
|
||||||
delete_cps.append(cp)
|
delete_cps.append(cp)
|
||||||
|
|
||||||
errs = [{} for p in positions_data]
|
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:
|
if not force:
|
||||||
for i, pos_data in enumerate(positions_data):
|
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'))
|
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
|
||||||
if pos_data.get('variation')
|
if pos_data.get('variation')
|
||||||
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
|
||||||
@@ -792,6 +658,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
quotadiff.update(new_quotas)
|
||||||
|
|
||||||
if any(errs):
|
if any(errs):
|
||||||
raise ValidationError({'positions': errs})
|
raise ValidationError({'positions': errs})
|
||||||
|
|
||||||
@@ -799,68 +667,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
validated_data['locale'] = self.context['event'].settings.locale
|
validated_data['locale'] = self.context['event'].settings.locale
|
||||||
order = Order(event=self.context['event'], **validated_data)
|
order = Order(event=self.context['event'], **validated_data)
|
||||||
order.set_expires(subevents=[p.get('subevent') for p in positions_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.meta_info = "{}"
|
||||||
order.total = Decimal('0.00')
|
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
if ia:
|
|
||||||
ia.order = order
|
|
||||||
ia.save()
|
|
||||||
|
|
||||||
pos_map = {}
|
|
||||||
for pos_data in positions_data:
|
|
||||||
answers_data = pos_data.pop('answers', [])
|
|
||||||
addon_to = pos_data.pop('addon_to', None)
|
|
||||||
attendee_name = pos_data.pop('attendee_name', '')
|
|
||||||
if attendee_name and not pos_data.get('attendee_name_parts'):
|
|
||||||
pos_data['attendee_name_parts'] = {
|
|
||||||
'_legacy': attendee_name
|
|
||||||
}
|
|
||||||
pos = OrderPosition(**pos_data)
|
|
||||||
pos.order = order
|
|
||||||
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:
|
|
||||||
options = answ_data.pop('options', [])
|
|
||||||
answ = pos.answers.create(**answ_data)
|
|
||||||
answ.options.add(*options)
|
|
||||||
|
|
||||||
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:
|
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||||
order.status = Order.STATUS_PAID
|
order.status = Order.STATUS_PAID
|
||||||
order.save()
|
order.save()
|
||||||
@@ -871,8 +681,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
elif payment_provider == "free" and order.total != Decimal('0.00'):
|
||||||
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
|
||||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
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(
|
order.payments.create(
|
||||||
amount=order.total,
|
amount=order.total,
|
||||||
provider=payment_provider,
|
provider=payment_provider,
|
||||||
@@ -888,6 +696,38 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
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', [])
|
||||||
|
addon_to = pos_data.pop('addon_to', None)
|
||||||
|
attendee_name = pos_data.pop('attendee_name', '')
|
||||||
|
if attendee_name and not pos_data.get('attendee_name_parts'):
|
||||||
|
pos_data['attendee_name_parts'] = {
|
||||||
|
'_legacy': attendee_name
|
||||||
|
}
|
||||||
|
pos = OrderPosition(**pos_data)
|
||||||
|
pos.order = order
|
||||||
|
pos._calculate_tax()
|
||||||
|
if addon_to:
|
||||||
|
pos.addon_to = pos_map[addon_to]
|
||||||
|
pos.save()
|
||||||
|
pos_map[pos.positionid] = pos
|
||||||
|
for answ_data in answers_data:
|
||||||
|
options = answ_data.pop('options', [])
|
||||||
|
answ = pos.answers.create(**answ_data)
|
||||||
|
answ.options.add(*options)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.base.models import Organizer
|
||||||
from pretix.base.models import Organizer, SeatingPlan
|
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organizer
|
model = Organizer
|
||||||
fields = ('name', 'slug')
|
fields = ('name', 'slug')
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
|
||||||
layout = CompatibleJSONField(
|
|
||||||
validators=[SeatingPlanLayoutValidator()]
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = SeatingPlan
|
|
||||||
fields = ('id', 'name', 'layout')
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
|||||||
model = Voucher
|
model = Voucher
|
||||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
|
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
|
||||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||||
'tag', 'comment', 'subevent', 'show_hidden_items')
|
'tag', 'comment', 'subevent')
|
||||||
read_only_fields = ('id', 'redeemed')
|
read_only_fields = ('id', 'redeemed')
|
||||||
list_serializer_class = VoucherListSerializer
|
list_serializer_class = VoucherListSerializer
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.dispatch import Signal, receiver
|
from django.dispatch import Signal, receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
|
|
||||||
from pretix.api.models import ApiCall, WebHookCall
|
from pretix.api.models import ApiCall, WebHookCall
|
||||||
from pretix.base.signals import periodic_task
|
from pretix.base.signals import periodic_task
|
||||||
@@ -18,12 +17,10 @@ instances.
|
|||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task)
|
@receiver(periodic_task)
|
||||||
@scopes_disabled()
|
|
||||||
def cleanup_webhook_logs(sender, **kwargs):
|
def cleanup_webhook_logs(sender, **kwargs):
|
||||||
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task)
|
@receiver(periodic_task)
|
||||||
@scopes_disabled()
|
|
||||||
def cleanup_api_logs(sender, **kwargs):
|
def cleanup_api_logs(sender, **kwargs):
|
||||||
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete()
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ orga_router = routers.DefaultRouter()
|
|||||||
orga_router.register(r'events', event.EventViewSet)
|
orga_router.register(r'events', event.EventViewSet)
|
||||||
orga_router.register(r'subevents', event.SubEventViewSet)
|
orga_router.register(r'subevents', event.SubEventViewSet)
|
||||||
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
|
||||||
|
|
||||||
event_router = routers.DefaultRouter()
|
event_router = routers.DefaultRouter()
|
||||||
event_router.register(r'subevents', event.SubEventViewSet)
|
event_router.register(r'subevents', event.SubEventViewSet)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
return CartPosition.objects.filter(
|
return CartPosition.objects.filter(
|
||||||
event=self.request.event,
|
event=self.request.event,
|
||||||
cart_id__endswith="@api"
|
cart_id__endswith="@api"
|
||||||
).select_related('seat').prefetch_related('answers')
|
)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
@@ -25,7 +24,7 @@ from pretix.base.services.checkin import (
|
|||||||
)
|
)
|
||||||
from pretix.helpers.database import FixedOrderBy
|
from pretix.helpers.database import FixedOrderBy
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class CheckinListFilter(FilterSet):
|
class CheckinListFilter(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
@@ -44,6 +43,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
qs = self.request.event.checkin_lists.prefetch_related(
|
qs = self.request.event.checkin_lists.prefetch_related(
|
||||||
'limit_products',
|
'limit_products',
|
||||||
)
|
)
|
||||||
|
qs = CheckinList.annotate_with_numbers(qs, self.request.event)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
@@ -92,7 +92,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
if not clist.all_products:
|
if not clist.all_products:
|
||||||
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
||||||
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
|
|
||||||
|
|
||||||
ev = clist.subevent or clist.event
|
ev = clist.subevent or clist.event
|
||||||
response = {
|
response = {
|
||||||
@@ -147,7 +146,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class CheckinOrderPositionFilter(OrderPositionFilter):
|
class CheckinOrderPositionFilter(OrderPositionFilter):
|
||||||
|
|
||||||
def has_checkin_qs(self, queryset, name, value):
|
def has_checkin_qs(self, queryset, name, value):
|
||||||
@@ -156,7 +154,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = CheckinListOrderPositionSerializer
|
serializer_class = CheckinListOrderPositionSerializer
|
||||||
queryset = OrderPosition.all.none()
|
queryset = OrderPosition.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
||||||
ordering = ('attendee_name_cached', 'positionid')
|
ordering = ('attendee_name_cached', 'positionid')
|
||||||
ordering_fields = (
|
ordering_fields = (
|
||||||
@@ -231,7 +229,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
)
|
)
|
||||||
))
|
))
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
|
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
@@ -241,7 +239,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
),
|
),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order')
|
||||||
|
|
||||||
if not self.checkinlist.all_products:
|
if not self.checkinlist.all_products:
|
||||||
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True))
|
||||||
@@ -280,7 +278,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
datetime=dt,
|
datetime=dt,
|
||||||
questions_supported=self.request.data.get('questions_supported', True),
|
questions_supported=self.request.data.get('questions_supported', True),
|
||||||
canceled_supported=self.request.data.get('canceled_supported', False),
|
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.db import transaction
|
|||||||
from django.db.models import ProtectedError, Q
|
from django.db.models import ProtectedError, Q
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from rest_framework import filters, viewsets
|
from rest_framework import filters, viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class EventFilter(FilterSet):
|
class EventFilter(FilterSet):
|
||||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||||
@@ -73,8 +72,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
lookup_url_kwarg = 'event'
|
lookup_url_kwarg = 'event'
|
||||||
permission_classes = (EventCRUDPermission,)
|
permission_classes = (EventCRUDPermission,)
|
||||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
ordering = ('slug',)
|
|
||||||
ordering_fields = ('date_from', 'slug')
|
|
||||||
filterset_class = EventFilter
|
filterset_class = EventFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -86,7 +83,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return qs.prefetch_related(
|
return qs.prefetch_related(
|
||||||
'meta_values', 'meta_values__property', 'seat_category_mappings'
|
'meta_values', 'meta_values__property'
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@@ -183,7 +180,6 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class SubEventFilter(FilterSet):
|
class SubEventFilter(FilterSet):
|
||||||
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
|
||||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||||
@@ -242,18 +238,12 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
event__in=self.request.user.get_events_with_any_permission()
|
event__in=self.request.user.get_events_with_any_permission()
|
||||||
)
|
)
|
||||||
return qs.prefetch_related(
|
return qs.prefetch_related(
|
||||||
'subeventitem_set', 'subeventitemvariation_set', 'seat_category_mappings'
|
'subeventitem_set', 'subeventitemvariation_set'
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
original_data = self.get_serializer(instance=serializer.instance).data
|
|
||||||
super().perform_update(serializer)
|
super().perform_update(serializer)
|
||||||
|
|
||||||
if serializer.data == original_data:
|
|
||||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
|
||||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
|
||||||
return
|
|
||||||
|
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.subevent.changed',
|
'pretix.subevent.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from django.db.models import Q
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
@@ -22,7 +21,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class ItemFilter(FilterSet):
|
class ItemFilter(FilterSet):
|
||||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||||
|
|
||||||
@@ -66,14 +65,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
original_data = self.get_serializer(instance=serializer.instance).data
|
|
||||||
|
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
|
|
||||||
if serializer.data == original_data:
|
|
||||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
|
||||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
|
||||||
return
|
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.item.changed',
|
'pretix.event.item.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -320,7 +312,6 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class QuestionFilter(FilterSet):
|
class QuestionFilter(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
@@ -420,7 +411,6 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class QuotaFilter(FilterSet):
|
class QuotaFilter(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Quota
|
model = Quota
|
||||||
@@ -462,30 +452,9 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
original_data = self.get_serializer(instance=serializer.instance).data
|
|
||||||
|
|
||||||
current_subevent = serializer.instance.subevent
|
current_subevent = serializer.instance.subevent
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
request_subevent = serializer.instance.subevent
|
request_subevent = serializer.instance.subevent
|
||||||
|
|
||||||
if serializer.data == original_data:
|
|
||||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
|
||||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
|
||||||
return
|
|
||||||
|
|
||||||
if original_data['closed'] is True and serializer.instance.closed is False:
|
|
||||||
serializer.instance.log_action(
|
|
||||||
'pretix.event.quota.opened',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
)
|
|
||||||
elif original_data['closed'] is False and serializer.instance.closed is True:
|
|
||||||
serializer.instance.log_action(
|
|
||||||
'pretix.event.quota.closed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.quota.changed',
|
'pretix.event.quota.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from rest_framework import mixins, serializers, status, viewsets
|
from rest_framework import mixins, serializers, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import (
|
from rest_framework.exceptions import (
|
||||||
@@ -41,8 +40,7 @@ from pretix.base.services.invoices import (
|
|||||||
)
|
)
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
OrderChangeManager, OrderError, _order_placed_email,
|
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
|
||||||
extend_order, mark_order_expired, mark_order_refunded,
|
extend_order, mark_order_expired, mark_order_refunded,
|
||||||
)
|
)
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
@@ -52,7 +50,7 @@ from pretix.base.signals import (
|
|||||||
)
|
)
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class OrderFilter(FilterSet):
|
class OrderFilter(FilterSet):
|
||||||
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
email = django_filters.CharFilter(field_name='email', lookup_expr='iexact')
|
||||||
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
code = django_filters.CharFilter(field_name='code', lookup_expr='iexact')
|
||||||
@@ -94,8 +92,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
'positions',
|
'positions',
|
||||||
OrderPosition.objects.all().prefetch_related(
|
OrderPosition.objects.all().prefetch_related(
|
||||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
'item__category', 'addon_to', 'seat',
|
'item__category', 'addon_to',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation', 'seat'))
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -104,7 +102,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
Prefetch(
|
Prefetch(
|
||||||
'positions',
|
'positions',
|
||||||
OrderPosition.objects.all().prefetch_related(
|
OrderPosition.objects.all().prefetch_related(
|
||||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
|
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -432,7 +430,6 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
self.perform_create(serializer)
|
self.perform_create(serializer)
|
||||||
send_mail = serializer._send_mail
|
|
||||||
order = serializer.instance
|
order = serializer.instance
|
||||||
serializer = OrderSerializer(order, context=serializer.context)
|
serializer = OrderSerializer(order, context=serializer.context)
|
||||||
|
|
||||||
@@ -447,42 +444,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
(order.event.settings.get('invoice_generate') == 'True') or
|
(order.event.settings.get('invoice_generate') == 'True') or
|
||||||
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
|
||||||
) and not order.invoices.last()
|
) and not order.invoices.last()
|
||||||
invoice = None
|
|
||||||
if gen_invoice:
|
if gen_invoice:
|
||||||
invoice = generate_invoice(order, trigger_pdf=True)
|
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)
|
|
||||||
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
@@ -519,7 +482,6 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
|
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
|
||||||
serializer.instance.email_known_to_work = False
|
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.order.contact.changed',
|
'pretix.event.order.contact.changed',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -568,7 +530,6 @@ class OrderViewSet(viewsets.ModelViewSet):
|
|||||||
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth)
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class OrderPositionFilter(FilterSet):
|
class OrderPositionFilter(FilterSet):
|
||||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs')
|
||||||
@@ -610,7 +571,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrderPositionSerializer
|
serializer_class = OrderPositionSerializer
|
||||||
queryset = OrderPosition.all.none()
|
queryset = OrderPosition.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('order__datetime', 'positionid')
|
ordering = ('order__datetime', 'positionid')
|
||||||
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
||||||
@@ -647,13 +608,13 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
|||||||
)
|
)
|
||||||
))
|
))
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'variation', 'item__category', 'addon_to', 'seat'
|
'item', 'variation', 'item__category', 'addon_to'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
'checkins', 'answers', 'answers__options', 'answers__question'
|
'checkins', 'answers', 'answers__options', 'answers__question'
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
'item', 'order', 'order__event', 'order__event__organizer'
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -998,7 +959,6 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class InvoiceFilter(FilterSet):
|
class InvoiceFilter(FilterSet):
|
||||||
refers = django_filters.CharFilter(method='refers_qs')
|
refers = django_filters.CharFilter(method='refers_qs')
|
||||||
number = django_filters.CharFilter(method='nr_qs')
|
number = django_filters.CharFilter(method='nr_qs')
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
from rest_framework import filters, viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.api.serializers.organizer import (
|
from pretix.api.serializers.organizer import OrganizerSerializer
|
||||||
OrganizerSerializer, SeatingPlanSerializer,
|
from pretix.base.models import Organizer
|
||||||
)
|
|
||||||
from pretix.base.models import Organizer, SeatingPlan
|
|
||||||
from pretix.helpers.dicts import merge_dicts
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
@@ -14,9 +10,6 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
queryset = Organizer.objects.none()
|
queryset = Organizer.objects.none()
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
lookup_url_kwarg = 'organizer'
|
lookup_url_kwarg = 'organizer'
|
||||||
filter_backends = (filters.OrderingFilter,)
|
|
||||||
ordering = ('slug',)
|
|
||||||
ordering_fields = ('name', 'slug')
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
@@ -34,50 +27,3 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
|
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
|
||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = SeatingPlanSerializer
|
|
||||||
queryset = SeatingPlan.objects.none()
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
write_permission = 'can_change_organizer_settings'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.request.organizer.seating_plans.all()
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx['organizer'] = self.request.organizer
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
inst = serializer.save(organizer=self.request.organizer)
|
|
||||||
self.request.organizer.log_action(
|
|
||||||
'pretix.seatingplan.added',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
if serializer.instance.events.exists() or serializer.instance.subevents.exists():
|
|
||||||
raise PermissionDenied('This plan can not be changed while it is in use for an event.')
|
|
||||||
inst = serializer.save(organizer=self.request.organizer)
|
|
||||||
self.request.organizer.log_action(
|
|
||||||
'pretix.seatingplan.changed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
|
||||||
)
|
|
||||||
return inst
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
if instance.events.exists() or instance.subevents.exists():
|
|
||||||
raise PermissionDenied('This plan can not be deleted while it is in use for an event.')
|
|
||||||
instance.log_action(
|
|
||||||
'pretix.seatingplan.deleted',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data={'id': instance.pk}
|
|
||||||
)
|
|
||||||
instance.delete()
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from django.utils.timezone import now
|
|||||||
from django_filters.rest_framework import (
|
from django_filters.rest_framework import (
|
||||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||||
)
|
)
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
@@ -16,7 +15,7 @@ from rest_framework.response import Response
|
|||||||
from pretix.api.serializers.voucher import VoucherSerializer
|
from pretix.api.serializers.voucher import VoucherSerializer
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class VoucherFilter(FilterSet):
|
class VoucherFilter(FilterSet):
|
||||||
active = BooleanFilter(method='filter_active')
|
active = BooleanFilter(method='filter_active')
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||||
@@ -11,7 +10,7 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
|
|||||||
from pretix.base.models import WaitingListEntry
|
from pretix.base.models import WaitingListEntry
|
||||||
from pretix.base.models.waitinglist import WaitingListException
|
from pretix.base.models.waitinglist import WaitingListException
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class WaitingListFilter(FilterSet):
|
class WaitingListFilter(FilterSet):
|
||||||
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
has_voucher = django_filters.rest_framework.BooleanFilter(method='has_voucher_qs')
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from celery.exceptions import MaxRetriesExceededError
|
|||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django_scopes import scope, scopes_disabled
|
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
||||||
@@ -204,10 +203,9 @@ def notify_webhooks(logentry_id: int):
|
|||||||
@app.task(base=ProfiledTask, bind=True, max_retries=9)
|
@app.task(base=ProfiledTask, bind=True, max_retries=9)
|
||||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||||
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||||
with scopes_disabled():
|
|
||||||
webhook = WebHook.objects.get(id=webhook_id)
|
|
||||||
with scope(organizer=webhook.organizer):
|
|
||||||
logentry = LogEntry.all.get(id=logentry_id)
|
logentry = LogEntry.all.get(id=logentry_id)
|
||||||
|
webhook = WebHook.objects.get(id=webhook_id)
|
||||||
|
|
||||||
types = get_all_webhook_events()
|
types = get_all_webhook_events()
|
||||||
event_type = types.get(action_type)
|
event_type = types.get(action_type)
|
||||||
if not event_type or not webhook.enabled:
|
if not event_type or not webhook.enabled:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class PretixBaseConfig(AppConfig):
|
|||||||
from . import invoice # NOQA
|
from . import invoice # NOQA
|
||||||
from . import notifications # NOQA
|
from . import notifications # NOQA
|
||||||
from . import email # NOQA
|
from . import email # NOQA
|
||||||
from .services import auth, checkin, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .celery_app import app as celery_app # NOQA
|
from .celery_app import app as celery_app # NOQA
|
||||||
|
|||||||
@@ -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()))
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
def contextprocessor(request):
|
|
||||||
ctx = {}
|
|
||||||
if settings.DEBUG and 'runserver' not in sys.argv:
|
|
||||||
ctx['debug_warning'] = True
|
|
||||||
elif 'runserver' in sys.argv:
|
|
||||||
ctx['development_warning'] = True
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
@@ -1,23 +1,15 @@
|
|||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
from decimal import Decimal
|
|
||||||
from smtplib import SMTPResponseException
|
from smtplib import SMTPResponseException
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail.backends.smtp import EmailBackend
|
from django.core.mail.backends.smtp import EmailBackend
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from inlinestyler.utils import inline_css
|
from inlinestyler.utils import inline_css
|
||||||
|
|
||||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
from pretix.base.models import Event, Order
|
||||||
from pretix.base.models import Event
|
from pretix.base.signals import register_html_mail_renderers
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
|
||||||
from pretix.base.signals import (
|
|
||||||
register_html_mail_renderers, register_mail_placeholders,
|
|
||||||
)
|
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.email')
|
logger = logging.getLogger('pretix.base.email')
|
||||||
@@ -52,8 +44,7 @@ class BaseHTMLMailRenderer:
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.identifier
|
return self.identifier
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
|
||||||
position=None) -> str:
|
|
||||||
"""
|
"""
|
||||||
This method should generate the HTML part of the email.
|
This method should generate the HTML part of the email.
|
||||||
|
|
||||||
@@ -61,7 +52,6 @@ class BaseHTMLMailRenderer:
|
|||||||
:param plain_signature: The signature with event organizer contact details in plain text.
|
:param plain_signature: The signature with event organizer contact details in plain text.
|
||||||
:param subject: The email subject.
|
:param subject: The email subject.
|
||||||
:param order: The order if this email is connected to one, otherwise ``None``.
|
:param order: The order if this email is connected to one, otherwise ``None``.
|
||||||
:param position: The order position if this email is connected to one, otherwise ``None``.
|
|
||||||
:return: An HTML string
|
:return: An HTML string
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -105,7 +95,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
def template_name(self):
|
def template_name(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
|
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
|
||||||
body_md = markdown_compile_email(plain_body)
|
body_md = markdown_compile_email(plain_body)
|
||||||
htmlctx = {
|
htmlctx = {
|
||||||
'site': settings.PRETIX_INSTANCE_NAME,
|
'site': settings.PRETIX_INSTANCE_NAME,
|
||||||
@@ -126,9 +116,6 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
if order:
|
if order:
|
||||||
htmlctx['order'] = order
|
htmlctx['order'] = order
|
||||||
|
|
||||||
if position:
|
|
||||||
htmlctx['position'] = position
|
|
||||||
|
|
||||||
tpl = get_template(self.template_name)
|
tpl = get_template(self.template_name)
|
||||||
body_html = inline_css(tpl.render(htmlctx))
|
body_html = inline_css(tpl.render(htmlctx))
|
||||||
return body_html
|
return body_html
|
||||||
@@ -144,285 +131,3 @@ class ClassicMailRenderer(TemplateBasedMailRenderer):
|
|||||||
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
|
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
|
||||||
def base_renderers(sender, **kwargs):
|
def base_renderers(sender, **kwargs):
|
||||||
return [ClassicMailRenderer]
|
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(
|
|
||||||
'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(
|
|
||||||
'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
|
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ class BaseExporter:
|
|||||||
|
|
||||||
:type form_data: dict
|
:type form_data: dict
|
||||||
:param form_data: The form data of the export details form
|
:param form_data: The form data of the export details form
|
||||||
:param output_file: You can optionally accept a parameter that will be given a file handle to write the
|
|
||||||
output to. In this case, you can return None instead of the file content.
|
|
||||||
|
|
||||||
Note: If you use a ``ModelChoiceField`` (or a ``ModelMultipleChoiceField``), the
|
Note: If you use a ``ModelChoiceField`` (or a ``ModelMultipleChoiceField``), the
|
||||||
``form_data`` will not contain the model instance but only it's primary key (or
|
``form_data`` will not contain the model instance but only it's primary key (or
|
||||||
@@ -111,22 +109,16 @@ class ListExporter(BaseExporter):
|
|||||||
raise NotImplementedError() # noqa
|
raise NotImplementedError() # noqa
|
||||||
|
|
||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
return 'export'
|
return 'export.csv'
|
||||||
|
|
||||||
def _render_csv(self, form_data, output_file=None, **kwargs):
|
def _render_csv(self, form_data, **kwargs):
|
||||||
if output_file:
|
|
||||||
writer = csv.writer(output_file, **kwargs)
|
|
||||||
for line in self.iterate_list(form_data):
|
|
||||||
writer.writerow(line)
|
|
||||||
return self.get_filename() + '.csv', 'text/csv', None
|
|
||||||
else:
|
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output, **kwargs)
|
writer = csv.writer(output, **kwargs)
|
||||||
for line in self.iterate_list(form_data):
|
for line in self.iterate_list(form_data):
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||||
|
|
||||||
def _render_xlsx(self, form_data, output_file=None):
|
def _render_xlsx(self, form_data):
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.get_active_sheet()
|
ws = wb.get_active_sheet()
|
||||||
try:
|
try:
|
||||||
@@ -137,24 +129,20 @@ class ListExporter(BaseExporter):
|
|||||||
for j, val in enumerate(line):
|
for j, val in enumerate(line):
|
||||||
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
|
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||||
|
|
||||||
if output_file:
|
|
||||||
wb.save(output_file)
|
|
||||||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
|
|
||||||
else:
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
|
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
|
||||||
wb.save(f.name)
|
wb.save(f.name)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
|
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
|
||||||
|
|
||||||
def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
|
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
|
||||||
if form_data.get('_format') == 'xlsx':
|
if form_data.get('_format') == 'xlsx':
|
||||||
return self._render_xlsx(form_data, output_file=output_file)
|
return self._render_xlsx(form_data)
|
||||||
elif form_data.get('_format') == 'default':
|
elif form_data.get('_format') == 'default':
|
||||||
return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',', output_file=output_file)
|
return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
|
||||||
elif form_data.get('_format') == 'csv-excel':
|
elif form_data.get('_format') == 'csv-excel':
|
||||||
return self._render_csv(form_data, dialect='excel', output_file=output_file)
|
return self._render_csv(form_data, dialect='excel')
|
||||||
elif form_data.get('_format') == 'semicolon':
|
elif form_data.get('_format') == 'semicolon':
|
||||||
return self._render_csv(form_data, dialect='excel', delimiter=';', output_file=output_file)
|
return self._render_csv(form_data, dialect='excel', delimiter=';')
|
||||||
|
|
||||||
|
|
||||||
class MultiSheetListExporter(ListExporter):
|
class MultiSheetListExporter(ListExporter):
|
||||||
@@ -192,20 +180,14 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
def iterate_sheet(self, form_data, sheet):
|
def iterate_sheet(self, form_data, sheet):
|
||||||
raise NotImplementedError() # noqa
|
raise NotImplementedError() # noqa
|
||||||
|
|
||||||
def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
|
def _render_sheet_csv(self, form_data, sheet, **kwargs):
|
||||||
if output_file:
|
|
||||||
writer = csv.writer(output_file, **kwargs)
|
|
||||||
for line in self.iterate_sheet(form_data, sheet):
|
|
||||||
writer.writerow(line)
|
|
||||||
return self.get_filename() + '.csv', 'text/csv', None
|
|
||||||
else:
|
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output, **kwargs)
|
writer = csv.writer(output, **kwargs)
|
||||||
for line in self.iterate_sheet(form_data, sheet):
|
for line in self.iterate_sheet(form_data, sheet):
|
||||||
writer.writerow(line)
|
writer.writerow(line)
|
||||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||||
|
|
||||||
def _render_xlsx(self, form_data, output_file=None):
|
def _render_xlsx(self, form_data):
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.get_active_sheet()
|
ws = wb.get_active_sheet()
|
||||||
wb.remove(ws)
|
wb.remove(ws)
|
||||||
@@ -215,24 +197,19 @@ class MultiSheetListExporter(ListExporter):
|
|||||||
for j, val in enumerate(line):
|
for j, val in enumerate(line):
|
||||||
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
|
ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||||
|
|
||||||
if output_file:
|
|
||||||
wb.save(output_file)
|
|
||||||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
|
|
||||||
else:
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
|
with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
|
||||||
wb.save(f.name)
|
wb.save(f.name)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
|
return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()
|
||||||
|
|
||||||
def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
|
def render(self, form_data: dict) -> Tuple[str, str, bytes]:
|
||||||
if form_data.get('_format') == 'xlsx':
|
if form_data.get('_format') == 'xlsx':
|
||||||
return self._render_xlsx(form_data, output_file=output_file)
|
return self._render_xlsx(form_data)
|
||||||
elif ':' in form_data.get('_format'):
|
elif ':' in form_data.get('_format'):
|
||||||
sheet, f = form_data.get('_format').split(':')
|
sheet, f = form_data.get('_format').split(':')
|
||||||
if f == 'default':
|
if f == 'default':
|
||||||
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',',
|
return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',')
|
||||||
output_file=output_file)
|
|
||||||
elif f == 'excel':
|
elif f == 'excel':
|
||||||
return self._render_sheet_csv(form_data, sheet, dialect='excel', output_file=output_file)
|
return self._render_sheet_csv(form_data, sheet, dialect='excel')
|
||||||
elif f == 'semicolon':
|
elif f == 'semicolon':
|
||||||
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';', output_file=output_file)
|
return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';')
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class AnswerFilesExporter(BaseExporter):
|
|||||||
if form_data.get('questions'):
|
if form_data.get('questions'):
|
||||||
qs = qs.filter(question__in=form_data['questions'])
|
qs = qs.filter(question__in=form_data['questions'])
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
any = False
|
|
||||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||||
for i in qs:
|
for i in qs:
|
||||||
if i.file:
|
if i.file:
|
||||||
@@ -52,12 +51,9 @@ class AnswerFilesExporter(BaseExporter):
|
|||||||
i.question.pk,
|
i.question.pk,
|
||||||
os.path.basename(i.file.name).split('.', 1)[1]
|
os.path.basename(i.file.name).split('.', 1)[1]
|
||||||
)
|
)
|
||||||
any = True
|
|
||||||
zipf.writestr(fname, i.file.read())
|
zipf.writestr(fname, i.file.read())
|
||||||
i.file.close()
|
i.file.close()
|
||||||
|
|
||||||
if not any:
|
|
||||||
return None
|
|
||||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||||
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
for l in invoice.lines.all():
|
for l in invoice.lines.all():
|
||||||
positions.append({
|
positions.append({
|
||||||
'ADes': l.description.replace("<br />", "\n"),
|
'ADes': l.description.replace("<br />", "\n"),
|
||||||
'ANetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
|
'ANetA': round(float(l.net_value), 2),
|
||||||
'ANo': self.event.slug,
|
'ANo': self.event.slug,
|
||||||
'AQ': -1 if invoice.is_cancellation else 1,
|
'AQ': -1 if invoice.is_cancellation else 1,
|
||||||
'AVatP': round(float(l.tax_rate), 2),
|
'AVatP': round(float(l.tax_rate), 2),
|
||||||
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
|
'DIDt': (l.subevent or invoice.order.event).date_from.isoformat().replace('Z', '+00:00'),
|
||||||
'PosGrossA': round(float(l.gross_value), 2),
|
'PosGrossA': round(float((-1 if invoice.is_cancellation else 1) * l.gross_value), 2),
|
||||||
'PosNetA': round(float(l.net_value), 2),
|
'PosNetA': round(float((-1 if invoice.is_cancellation else 1) * l.net_value), 2),
|
||||||
})
|
})
|
||||||
gross_total += l.gross_value
|
gross_total += l.gross_value
|
||||||
net_total += l.net_value
|
net_total += l.net_value
|
||||||
@@ -50,7 +50,7 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
if p.provider == 'paypal':
|
if p.provider == 'paypal':
|
||||||
paypal_email = p.info_data.get('payer', {}).get('payer_info', {}).get('email')
|
paypal_email = p.info_data.get('payer', {}).get('payer_info', {}).get('email')
|
||||||
try:
|
try:
|
||||||
ppid = p.info_data['transactions'][0]['related_resources'][0]['sale']['id']
|
ppid = p.info_data['transactions'][0]['related_resources']['sale']['id']
|
||||||
except:
|
except:
|
||||||
ppid = p.info_data.get('id')
|
ppid = p.info_data.get('id')
|
||||||
payments.append({
|
payments.append({
|
||||||
@@ -93,7 +93,7 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
'PTNo15': p.full_id or '',
|
'PTNo15': p.full_id or '',
|
||||||
})
|
})
|
||||||
elif p.provider.startswith('stripe'):
|
elif p.provider.startswith('stripe'):
|
||||||
src = p.info_data.get("source", p.info_data)
|
src = p.info_data.get("source", "{}")
|
||||||
payments.append({
|
payments.append({
|
||||||
'PTID': '81',
|
'PTID': '81',
|
||||||
'PTN': 'Stripe',
|
'PTN': 'Stripe',
|
||||||
@@ -129,11 +129,8 @@ class DekodiNREIExporter(BaseExporter):
|
|||||||
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
'DIDt': invoice.order.datetime.isoformat().replace('Z', '+00:00'),
|
||||||
'DT': '30' if invoice.is_cancellation else '10',
|
'DT': '30' if invoice.is_cancellation else '10',
|
||||||
'EM': invoice.order.email,
|
'EM': invoice.order.email,
|
||||||
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1] if invoice.invoice_to_name else '',
|
'FamN': invoice.invoice_to_name.rsplit(' ', 1)[-1],
|
||||||
'FN': (
|
'FN': invoice.invoice_to_name.rsplit(' ', 1)[0] if ' ' in invoice.invoice_to_name else '',
|
||||||
invoice.invoice_to_name.rsplit(' ', 1)[0]
|
|
||||||
if invoice.invoice_to_name and ' ' in invoice.invoice_to_name else ''
|
|
||||||
),
|
|
||||||
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
|
'IDt': invoice.date.isoformat() + 'T08:00:00+01:00',
|
||||||
'INo': invoice.full_invoice_no,
|
'INo': invoice.full_invoice_no,
|
||||||
'IsNet': invoice.reverse_charge,
|
'IsNet': invoice.reverse_charge,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class InvoiceExporter(BaseExporter):
|
|||||||
identifier = 'invoices'
|
identifier = 'invoices'
|
||||||
verbose_name = _('All invoices')
|
verbose_name = _('All invoices')
|
||||||
|
|
||||||
def render(self, form_data: dict, output_file=None):
|
def render(self, form_data: dict):
|
||||||
qs = self.event.invoices.filter(shredded=False)
|
qs = self.event.invoices.filter(shredded=False)
|
||||||
|
|
||||||
if form_data.get('payment_provider'):
|
if form_data.get('payment_provider'):
|
||||||
@@ -46,8 +46,7 @@ class InvoiceExporter(BaseExporter):
|
|||||||
qs = qs.filter(date__lte=date_value)
|
qs = qs.filter(date__lte=date_value)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
any = False
|
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||||
with ZipFile(output_file or os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
|
||||||
for i in qs:
|
for i in qs:
|
||||||
try:
|
try:
|
||||||
if not i.file:
|
if not i.file:
|
||||||
@@ -55,22 +54,14 @@ class InvoiceExporter(BaseExporter):
|
|||||||
i.refresh_from_db()
|
i.refresh_from_db()
|
||||||
i.file.open('rb')
|
i.file.open('rb')
|
||||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||||
any = True
|
|
||||||
i.file.close()
|
i.file.close()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
invoice_pdf_task.apply(args=(i.pk,))
|
invoice_pdf_task.apply(args=(i.pk,))
|
||||||
i.refresh_from_db()
|
i.refresh_from_db()
|
||||||
i.file.open('rb')
|
i.file.open('rb')
|
||||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||||
any = True
|
|
||||||
i.file.close()
|
i.file.close()
|
||||||
|
|
||||||
if not any:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if output_file:
|
|
||||||
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', None
|
|
||||||
else:
|
|
||||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||||
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.utils.formats import date_format, localize
|
|||||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
||||||
|
|
||||||
from pretix.base.models import (
|
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.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
@@ -96,7 +96,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
for k, label, w in name_scheme['fields']:
|
for k, label, w in name_scheme['fields']:
|
||||||
headers.append(label)
|
headers.append(label)
|
||||||
headers += [
|
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')
|
_('Date of last payment'), _('Fees'), _('Order locale')
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -109,8 +109,6 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
|
|
||||||
headers.append(_('Invoice numbers'))
|
headers.append(_('Invoice numbers'))
|
||||||
headers.append(_('Sales channel'))
|
headers.append(_('Sales channel'))
|
||||||
headers.append(_('Requires special attention'))
|
|
||||||
headers.append(_('Comment'))
|
|
||||||
|
|
||||||
yield headers
|
yield headers
|
||||||
|
|
||||||
@@ -155,11 +153,10 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
order.invoice_address.city,
|
order.invoice_address.city,
|
||||||
order.invoice_address.country if order.invoice_address.country else
|
order.invoice_address.country if order.invoice_address.country else
|
||||||
order.invoice_address.country_old,
|
order.invoice_address.country_old,
|
||||||
order.invoice_address.state,
|
|
||||||
order.invoice_address.vat_id,
|
order.invoice_address.vat_id,
|
||||||
]
|
]
|
||||||
except InvoiceAddress.DoesNotExist:
|
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 += [
|
row += [
|
||||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
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(', '.join([i.number for i in order.invoices.all()]))
|
||||||
row.append(order.sales_channel)
|
row.append(order.sales_channel)
|
||||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
|
||||||
row.append(order.comment or "")
|
|
||||||
yield row
|
yield row
|
||||||
|
|
||||||
def iterate_fees(self, form_data: dict):
|
def iterate_fees(self, form_data: dict):
|
||||||
@@ -213,7 +208,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
for k, label, w in name_scheme['fields']:
|
for k, label, w in name_scheme['fields']:
|
||||||
headers.append(_('Invoice address name') + ': ' + str(label))
|
headers.append(_('Invoice address name') + ': ' + str(label))
|
||||||
headers += [
|
headers += [
|
||||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||||
]
|
]
|
||||||
|
|
||||||
yield headers
|
yield headers
|
||||||
@@ -248,11 +243,10 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
order.invoice_address.city,
|
order.invoice_address.city,
|
||||||
order.invoice_address.country if order.invoice_address.country else
|
order.invoice_address.country if order.invoice_address.country else
|
||||||
order.invoice_address.country_old,
|
order.invoice_address.country_old,
|
||||||
order.invoice_address.state,
|
|
||||||
order.invoice_address.vat_id,
|
order.invoice_address.vat_id,
|
||||||
]
|
]
|
||||||
except InvoiceAddress.DoesNotExist:
|
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
|
yield row
|
||||||
|
|
||||||
def iterate_positions(self, form_data: dict):
|
def iterate_positions(self, form_data: dict):
|
||||||
@@ -307,7 +301,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
for k, label, w in name_scheme['fields']:
|
for k, label, w in name_scheme['fields']:
|
||||||
headers.append(_('Invoice address name') + ': ' + str(label))
|
headers.append(_('Invoice address name') + ': ' + str(label))
|
||||||
headers += [
|
headers += [
|
||||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||||
]
|
]
|
||||||
headers.append(_('Sales channel'))
|
headers.append(_('Sales channel'))
|
||||||
|
|
||||||
@@ -345,11 +339,6 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]
|
]
|
||||||
acache = {}
|
acache = {}
|
||||||
for a in op.answers.all():
|
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:
|
for q in questions:
|
||||||
row.append(acache.get(q.pk, ''))
|
row.append(acache.get(q.pk, ''))
|
||||||
@@ -369,11 +358,10 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
order.invoice_address.city,
|
order.invoice_address.city,
|
||||||
order.invoice_address.country if order.invoice_address.country else
|
order.invoice_address.country if order.invoice_address.country else
|
||||||
order.invoice_address.country_old,
|
order.invoice_address.country_old,
|
||||||
order.invoice_address.state,
|
|
||||||
order.invoice_address.vat_id,
|
order.invoice_address.vat_id,
|
||||||
]
|
]
|
||||||
except InvoiceAddress.DoesNotExist:
|
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)
|
row.append(order.sales_channel)
|
||||||
yield row
|
yield row
|
||||||
|
|
||||||
@@ -515,7 +503,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
_('Invoice recipient:') + ' ' + _('ZIP code'),
|
_('Invoice recipient:') + ' ' + _('ZIP code'),
|
||||||
_('Invoice recipient:') + ' ' + _('City'),
|
_('Invoice recipient:') + ' ' + _('City'),
|
||||||
_('Invoice recipient:') + ' ' + _('Country'),
|
_('Invoice recipient:') + ' ' + _('Country'),
|
||||||
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
|
|
||||||
_('Invoice recipient:') + ' ' + _('VAT ID'),
|
_('Invoice recipient:') + ' ' + _('VAT ID'),
|
||||||
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
||||||
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
||||||
@@ -565,7 +552,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
i.invoice_to_zipcode,
|
i.invoice_to_zipcode,
|
||||||
i.invoice_to_city,
|
i.invoice_to_city,
|
||||||
i.invoice_to_country,
|
i.invoice_to_country,
|
||||||
i.invoice_to_state,
|
|
||||||
i.invoice_to_vat_id,
|
i.invoice_to_vat_id,
|
||||||
i.invoice_to_beneficiary,
|
i.invoice_to_beneficiary,
|
||||||
i.internal_reference,
|
i.internal_reference,
|
||||||
@@ -605,7 +591,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
_('Invoice recipient:') + ' ' + _('ZIP code'),
|
_('Invoice recipient:') + ' ' + _('ZIP code'),
|
||||||
_('Invoice recipient:') + ' ' + _('City'),
|
_('Invoice recipient:') + ' ' + _('City'),
|
||||||
_('Invoice recipient:') + ' ' + _('Country'),
|
_('Invoice recipient:') + ' ' + _('Country'),
|
||||||
_('Invoice recipient:') + ' ' + pgettext('address', 'State'),
|
|
||||||
_('Invoice recipient:') + ' ' + _('VAT ID'),
|
_('Invoice recipient:') + ' ' + _('VAT ID'),
|
||||||
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
_('Invoice recipient:') + ' ' + _('Beneficiary'),
|
||||||
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
_('Invoice recipient:') + ' ' + _('Internal reference'),
|
||||||
@@ -645,7 +630,6 @@ class InvoiceDataExporter(MultiSheetListExporter):
|
|||||||
i.invoice_to_zipcode,
|
i.invoice_to_zipcode,
|
||||||
i.invoice_to_city,
|
i.invoice_to_city,
|
||||||
i.invoice_to_country,
|
i.invoice_to_country,
|
||||||
i.invoice_to_state,
|
|
||||||
i.invoice_to_vat_id,
|
i.invoice_to_vat_id,
|
||||||
i.invoice_to_beneficiary,
|
i.invoice_to_beneficiary,
|
||||||
i.internal_reference,
|
i.internal_reference,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class LoginForm(forms.Form):
|
|||||||
Base class for authenticating users. Extend this to get a form that accepts
|
Base class for authenticating users. Extend this to get a form that accepts
|
||||||
username/password logins.
|
username/password logins.
|
||||||
"""
|
"""
|
||||||
email = forms.EmailField(label=_("E-mail"), max_length=254, widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))
|
email = forms.EmailField(label=_("E-mail"), max_length=254)
|
||||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,27 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from urllib.error import HTTPError
|
|
||||||
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import pycountry
|
|
||||||
import pytz
|
import pytz
|
||||||
import vat_moss.errors
|
import vat_moss.errors
|
||||||
import vat_moss.id
|
import vat_moss.id
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import QuerySet
|
|
||||||
from django.forms import Select
|
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import (
|
from django.utils.translation import ugettext_lazy as _
|
||||||
get_language, pgettext_lazy, ugettext_lazy as _,
|
|
||||||
)
|
|
||||||
from django_countries import countries
|
|
||||||
from django_countries.fields import Country, CountryField
|
|
||||||
|
|
||||||
from pretix.base.forms.widgets import (
|
from pretix.base.forms.widgets import (
|
||||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||||
TimePickerWidget, UploadedFileWidget,
|
TimePickerWidget, UploadedFileWidget,
|
||||||
)
|
)
|
||||||
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
||||||
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
from pretix.base.models.tax import EU_COUNTRIES
|
||||||
from pretix.base.settings import (
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
|
|
||||||
PERSON_NAME_TITLE_GROUPS,
|
|
||||||
)
|
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
from pretix.control.forms import SplitDateTimeField
|
from pretix.control.forms import SplitDateTimeField
|
||||||
from pretix.helpers.escapejson import escapejson_attr
|
|
||||||
from pretix.helpers.i18n import get_format_without_seconds
|
from pretix.helpers.i18n import get_format_without_seconds
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
|
|
||||||
@@ -43,26 +30,14 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class NamePartsWidget(forms.MultiWidget):
|
class NamePartsWidget(forms.MultiWidget):
|
||||||
widget = forms.TextInput
|
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):
|
def __init__(self, scheme: dict, field: forms.Field, attrs=None):
|
||||||
widgets = []
|
widgets = []
|
||||||
self.scheme = scheme
|
self.scheme = scheme
|
||||||
self.field = field
|
self.field = field
|
||||||
self.titles = titles
|
|
||||||
for fname, label, size in self.scheme['fields']:
|
for fname, label, size in self.scheme['fields']:
|
||||||
a = copy.copy(attrs) or {}
|
a = copy.copy(attrs) or {}
|
||||||
a['data-fname'] = fname
|
a['data-fname'] = fname
|
||||||
if fname == 'title' and self.titles:
|
|
||||||
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
|
|
||||||
else:
|
|
||||||
widgets.append(self.widget(attrs=a))
|
widgets.append(self.widget(attrs=a))
|
||||||
super().__init__(widgets, attrs)
|
super().__init__(widgets, attrs)
|
||||||
|
|
||||||
@@ -97,7 +72,6 @@ class NamePartsWidget(forms.MultiWidget):
|
|||||||
title=self.scheme['fields'][i][1],
|
title=self.scheme['fields'][i][1],
|
||||||
placeholder=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]
|
final_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||||
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
|
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs, renderer=renderer))
|
||||||
return mark_safe(self.format_output(output))
|
return mark_safe(self.format_output(output))
|
||||||
@@ -123,31 +97,16 @@ class NamePartsFormField(forms.MultiValueField):
|
|||||||
'max_length': kwargs.pop('max_length', None),
|
'max_length': kwargs.pop('max_length', None),
|
||||||
}
|
}
|
||||||
self.scheme_name = kwargs.pop('scheme')
|
self.scheme_name = kwargs.pop('scheme')
|
||||||
self.titles = kwargs.pop('titles')
|
|
||||||
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
|
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
|
||||||
if self.titles:
|
|
||||||
self.scheme_titles = PERSON_NAME_TITLE_GROUPS.get(self.titles)
|
|
||||||
else:
|
|
||||||
self.scheme_titles = None
|
|
||||||
self.one_required = kwargs.get('required', True)
|
self.one_required = kwargs.get('required', True)
|
||||||
require_all_fields = kwargs.pop('require_all_fields', False)
|
require_all_fields = kwargs.pop('require_all_fields', False)
|
||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
kwargs['widget'] = (kwargs.get('widget') or self.widget)(
|
kwargs['widget'] = (kwargs.get('widget') or self.widget)(
|
||||||
scheme=self.scheme, titles=self.scheme_titles, field=self, **kwargs.pop('widget_kwargs', {})
|
scheme=self.scheme, field=self, **kwargs.pop('widget_kwargs', {})
|
||||||
)
|
)
|
||||||
defaults.update(**kwargs)
|
defaults.update(**kwargs)
|
||||||
for fname, label, size in self.scheme['fields']:
|
for fname, label, size in self.scheme['fields']:
|
||||||
defaults['label'] = label
|
defaults['label'] = label
|
||||||
if fname == 'title' and self.scheme_titles:
|
|
||||||
d = dict(defaults)
|
|
||||||
d.pop('max_length', None)
|
|
||||||
field = forms.ChoiceField(
|
|
||||||
**d,
|
|
||||||
choices=[('', '')] + [(d, d) for d in self.scheme_titles[1]]
|
|
||||||
)
|
|
||||||
field.part_name = fname
|
|
||||||
fields.append(field)
|
|
||||||
else:
|
|
||||||
field = forms.CharField(**defaults)
|
field = forms.CharField(**defaults)
|
||||||
field.part_name = fname
|
field.part_name = fname
|
||||||
fields.append(field)
|
fields.append(field)
|
||||||
@@ -195,7 +154,6 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
required=event.settings.attendee_names_required,
|
required=event.settings.attendee_names_required,
|
||||||
scheme=event.settings.name_scheme,
|
scheme=event.settings.name_scheme,
|
||||||
titles=event.settings.name_scheme_titles,
|
|
||||||
label=_('Attendee name'),
|
label=_('Attendee name'),
|
||||||
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
|
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
|
||||||
)
|
)
|
||||||
@@ -203,12 +161,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
self.fields['attendee_email'] = forms.EmailField(
|
self.fields['attendee_email'] = forms.EmailField(
|
||||||
required=event.settings.attendee_emails_required,
|
required=event.settings.attendee_emails_required,
|
||||||
label=_('Attendee email'),
|
label=_('Attendee email'),
|
||||||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
|
||||||
widget=forms.EmailInput(
|
|
||||||
attrs={
|
|
||||||
'autocomplete': 'email'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for q in questions:
|
for q in questions:
|
||||||
@@ -260,14 +213,6 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
widget=forms.Textarea,
|
widget=forms.Textarea,
|
||||||
initial=initial.answer if initial else None,
|
initial=initial.answer if initial else None,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
|
||||||
field = CountryField().formfield(
|
|
||||||
label=label, required=required,
|
|
||||||
help_text=help_text,
|
|
||||||
widget=forms.Select,
|
|
||||||
empty_label='',
|
|
||||||
initial=initial.answer if initial else None,
|
|
||||||
)
|
|
||||||
elif q.type == Question.TYPE_CHOICE:
|
elif q.type == Question.TYPE_CHOICE:
|
||||||
field = forms.ModelChoiceField(
|
field = forms.ModelChoiceField(
|
||||||
queryset=q.options,
|
queryset=q.options,
|
||||||
@@ -322,7 +267,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
|
|
||||||
if q.dependency_question_id:
|
if q.dependency_question_id:
|
||||||
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
|
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
|
||||||
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values))
|
field.widget.attrs['data-question-dependency-value'] = q.dependency_value
|
||||||
if q.type != 'M':
|
if q.type != 'M':
|
||||||
field.widget.attrs['required'] = q.required and not self.all_optional
|
field.widget.attrs['required'] = q.required and not self.all_optional
|
||||||
field._required = q.required and not self.all_optional
|
field._required = q.required and not self.all_optional
|
||||||
@@ -338,33 +283,31 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
self.fields[key] = value
|
self.fields[key] = value
|
||||||
value.initial = data.get('question_form_data', {}).get(key)
|
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):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|
||||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||||
|
|
||||||
def question_is_visible(parentid, qvals):
|
def question_is_visible(parentid, qval):
|
||||||
parentq = question_cache[parentid]
|
parentq = question_cache[parentid]
|
||||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value):
|
||||||
return False
|
return False
|
||||||
if 'question_%d' % parentid not in d:
|
if 'question_%d' % parentid not in d:
|
||||||
return False
|
return False
|
||||||
dval = d.get('question_%d' % parentid)
|
dval = d.get('question_%d' % parentid)
|
||||||
return (
|
if qval == 'True':
|
||||||
('True' in qvals and dval)
|
return dval
|
||||||
or ('False' in qvals and not dval)
|
elif qval == 'False':
|
||||||
or (isinstance(dval, QuestionOption) and dval.identifier in qvals)
|
return not dval
|
||||||
or (isinstance(dval, (list, QuerySet)) and any(qval in [o.identifier for o in dval] for qval in qvals))
|
elif isinstance(dval, QuestionOption):
|
||||||
)
|
return dval.identifier == qval
|
||||||
|
else:
|
||||||
|
return qval in [o.identifier for o in dval]
|
||||||
|
|
||||||
def question_is_required(q):
|
def question_is_required(q):
|
||||||
return (
|
return (
|
||||||
q.required and
|
q.required and
|
||||||
(not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values))
|
(not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_value))
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.all_optional:
|
if not self.all_optional:
|
||||||
@@ -380,29 +323,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state',
|
fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||||
'vat_id', 'internal_reference', 'beneficiary')
|
'internal_reference', 'beneficiary')
|
||||||
widgets = {
|
widgets = {
|
||||||
'is_business': BusinessBooleanRadio,
|
'is_business': BusinessBooleanRadio,
|
||||||
'street': forms.Textarea(attrs={
|
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
|
||||||
'rows': 2,
|
|
||||||
'placeholder': _('Street and Number'),
|
|
||||||
'autocomplete': 'street-address'
|
|
||||||
}),
|
|
||||||
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
||||||
'country': forms.Select(attrs={
|
'company': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||||
'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',
|
|
||||||
}),
|
|
||||||
'vat_id': 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,
|
'internal_reference': forms.TextInput,
|
||||||
}
|
}
|
||||||
@@ -415,58 +342,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
self.request = kwargs.pop('request', None)
|
self.request = kwargs.pop('request', None)
|
||||||
self.validate_vat_id = kwargs.pop('validate_vat_id')
|
self.validate_vat_id = kwargs.pop('validate_vat_id')
|
||||||
self.all_optional = kwargs.pop('all_optional', False)
|
self.all_optional = kwargs.pop('all_optional', False)
|
||||||
|
|
||||||
kwargs.setdefault('initial', {})
|
|
||||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
|
||||||
# Try to guess the initial country from either the country of the merchant
|
|
||||||
# or the locale. This will hopefully save at least some users some scrolling :)
|
|
||||||
locale = get_language()
|
|
||||||
country = event.settings.invoice_address_from_country
|
|
||||||
if not country:
|
|
||||||
valid_countries = countries.countries
|
|
||||||
if '-' in locale:
|
|
||||||
parts = locale.split('-')
|
|
||||||
if parts[1].upper() in valid_countries:
|
|
||||||
country = Country(parts[1].upper())
|
|
||||||
elif parts[0].upper() in valid_countries:
|
|
||||||
country = Country(parts[0].upper())
|
|
||||||
else:
|
|
||||||
if locale in valid_countries:
|
|
||||||
country = Country(locale.upper())
|
|
||||||
|
|
||||||
kwargs['initial']['country'] = country
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not event.settings.invoice_address_vatid:
|
if not event.settings.invoice_address_vatid:
|
||||||
del self.fields['vat_id']
|
del self.fields['vat_id']
|
||||||
|
|
||||||
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:
|
if not event.settings.invoice_address_required or self.all_optional:
|
||||||
for k, f in self.fields.items():
|
for k, f in self.fields.items():
|
||||||
f.required = False
|
f.required = False
|
||||||
@@ -488,7 +367,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
required=event.settings.invoice_name_required and not self.all_optional,
|
required=event.settings.invoice_name_required and not self.all_optional,
|
||||||
scheme=event.settings.name_scheme,
|
scheme=event.settings.name_scheme,
|
||||||
titles=event.settings.name_scheme_titles,
|
|
||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
|
initial=(self.instance.name_parts if self.instance else self.instance.name_parts),
|
||||||
)
|
)
|
||||||
@@ -500,10 +378,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
if not event.settings.invoice_address_beneficiary:
|
if not event.settings.invoice_address_beneficiary:
|
||||||
del self.fields['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):
|
def clean(self):
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
if not data.get('is_business'):
|
if not data.get('is_business'):
|
||||||
@@ -517,22 +391,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||||
self.instance.vat_id_validated = False
|
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')
|
self.instance.name_parts = data.get('name_parts')
|
||||||
|
|
||||||
if all(
|
|
||||||
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
|
||||||
) and len(data.get('name_parts', {})) == 1:
|
|
||||||
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
|
||||||
self.cleaned_data['country'] = ''
|
|
||||||
|
|
||||||
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||||
pass
|
pass
|
||||||
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):
|
||||||
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.'))
|
raise ValidationError(_('Your VAT ID does not match the selected country.'))
|
||||||
try:
|
try:
|
||||||
result = vat_moss.id.validate(data.get('vat_id'))
|
result = vat_moss.id.validate(data.get('vat_id'))
|
||||||
@@ -550,7 +414,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'your country is currently not available. We will therefore '
|
'your country is currently not available. We will therefore '
|
||||||
'need to charge VAT on your invoice. You can get the tax amount '
|
'need to charge VAT on your invoice. You can get the tax amount '
|
||||||
'back via the VAT reimbursement process.'))
|
'back via the VAT reimbursement process.'))
|
||||||
except (vat_moss.errors.WebServiceError, HTTPError):
|
except vat_moss.errors.WebServiceError:
|
||||||
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
|
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
|
||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
if self.request and self.vat_warning:
|
if self.request and self.vat_warning:
|
||||||
@@ -563,8 +427,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
|
class BaseInvoiceNameForm(BaseInvoiceAddressForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
for f in list(self.fields.keys()):
|
for f in list(self.fields.keys()):
|
||||||
if f != 'name_parts':
|
if f != 'name':
|
||||||
del self.fields[f]
|
del self.fields[f]
|
||||||
|
|||||||
@@ -115,5 +115,5 @@ class User2FADeviceAddForm(forms.Form):
|
|||||||
name = forms.CharField(label=_('Device name'), max_length=64)
|
name = forms.CharField(label=_('Device name'), max_length=64)
|
||||||
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
||||||
('totp', _('Smartphone with the Authenticator application')),
|
('totp', _('Smartphone with the Authenticator application')),
|
||||||
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
('u2f', _('U2F-compatible hardware token (e.g. Yubikey)')),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class PlaceholderValidator(BaseValidator):
|
|||||||
if value.count('{') != value.count('}'):
|
if value.count('{') != value.count('}'):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
_('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))
|
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
||||||
@@ -37,7 +37,7 @@ class PlaceholderValidator(BaseValidator):
|
|||||||
if invalid_placeholders:
|
if invalid_placeholders:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Invalid placeholder(s): %(value)s'),
|
_('Invalid placeholder(s): %(value)s'),
|
||||||
code='invalid_placeholders',
|
code='invalid',
|
||||||
params={'value': ", ".join(invalid_placeholders,)})
|
params={'value': ", ".join(invalid_placeholders,)})
|
||||||
|
|
||||||
def clean(self, x):
|
def clean(self, x):
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.utils.timezone import override
|
|
||||||
from django_scopes import scope
|
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
|
||||||
from pretix.base.models import Event, Organizer
|
|
||||||
from pretix.base.signals import register_data_exporters
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Run an exporter to get data out of pretix"
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument('organizer_slug', nargs=1, type=str)
|
|
||||||
parser.add_argument('event_slug', nargs=1, type=str)
|
|
||||||
parser.add_argument('export_provider', nargs=1, type=str)
|
|
||||||
parser.add_argument('output_file', nargs=1, type=str)
|
|
||||||
parser.add_argument('--parameters', action='store', type=str, help='JSON-formatted parameters')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
try:
|
|
||||||
o = Organizer.objects.get(slug=options['organizer_slug'][0])
|
|
||||||
except Organizer.DoesNotExist:
|
|
||||||
self.stderr.write(self.style.ERROR('Organizer not found.'))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
with scope(organizer=o):
|
|
||||||
try:
|
|
||||||
e = o.events.get(slug=options['event_slug'][0])
|
|
||||||
except Event.DoesNotExist:
|
|
||||||
self.stderr.write(self.style.ERROR('Event not found.'))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
with language(e.settings.locale), override(e.settings.timezone):
|
|
||||||
responses = register_data_exporters.send(e)
|
|
||||||
for receiver, response in responses:
|
|
||||||
ex = response(e)
|
|
||||||
if ex.identifier == options['export_provider'][0]:
|
|
||||||
params = json.loads(options.get('parameters') or '{}')
|
|
||||||
with open(options['output_file'][0], 'wb') as f:
|
|
||||||
try:
|
|
||||||
ex.render(form_data=params, output_file=f)
|
|
||||||
except TypeError:
|
|
||||||
self.stderr.write(self.style.WARNING(
|
|
||||||
'Provider does not support direct file writing, need to buffer export in memory.'))
|
|
||||||
d = ex.render(form_data=params)
|
|
||||||
if d is None:
|
|
||||||
self.stderr.write(self.style.ERROR('Empty export.'))
|
|
||||||
sys.exit(2)
|
|
||||||
f.write(d[2])
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
self.stderr.write(self.style.ERROR('Export provider not found.'))
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
Django, for theoretically very valid reasons, creates migrations for *every single thing*
|
|
||||||
we change on a model. Even the `help_text`! This makes sense, as we don't know if any
|
|
||||||
database backend unknown to us might actually use this information for its database schema.
|
|
||||||
|
|
||||||
However, pretix only supports PostgreSQL, MySQL, MariaDB and SQLite and we can be pretty
|
|
||||||
certain that some changes to models will never require a change to the database. In this case,
|
|
||||||
not creating a migration for certain changes will save us some performance while applying them
|
|
||||||
*and* allow for a cleaner git history. Win-win!
|
|
||||||
|
|
||||||
Only caveat is that we need to do some dirty monkeypatching to achieve it...
|
|
||||||
"""
|
|
||||||
from django.core.management.commands.makemigrations import Command as Parent
|
|
||||||
from django.db import models
|
|
||||||
from django.db.migrations.operations import models as modelops
|
|
||||||
from django_countries.fields import CountryField
|
|
||||||
|
|
||||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name")
|
|
||||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("verbose_name_plural")
|
|
||||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("ordering")
|
|
||||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("get_latest_by")
|
|
||||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_manager_name")
|
|
||||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("permissions")
|
|
||||||
modelops.AlterModelOptions.ALTER_OPTION_KEYS.remove("default_permissions")
|
|
||||||
IGNORED_ATTRS = [
|
|
||||||
# (field type, attribute name, blacklist of field sub-types)
|
|
||||||
(models.Field, 'verbose_name', []),
|
|
||||||
(models.Field, 'help_text', []),
|
|
||||||
(models.Field, 'validators', []),
|
|
||||||
(models.Field, 'editable', [models.DateField, models.DateTimeField, models.DateField, models.BinaryField]),
|
|
||||||
(models.Field, 'blank', [models.DateField, models.DateTimeField, models.AutoField, models.NullBooleanField,
|
|
||||||
models.TimeField]),
|
|
||||||
(models.CharField, 'choices', [CountryField])
|
|
||||||
]
|
|
||||||
|
|
||||||
original_deconstruct = models.Field.deconstruct
|
|
||||||
|
|
||||||
|
|
||||||
def new_deconstruct(self):
|
|
||||||
name, path, args, kwargs = original_deconstruct(self)
|
|
||||||
for ftype, attr, blacklist in IGNORED_ATTRS:
|
|
||||||
if isinstance(self, ftype) and not any(isinstance(self, ft) for ft in blacklist):
|
|
||||||
kwargs.pop(attr, None)
|
|
||||||
return name, path, args, kwargs
|
|
||||||
|
|
||||||
|
|
||||||
models.Field.deconstruct = new_deconstruct
|
|
||||||
|
|
||||||
|
|
||||||
class Command(Parent):
|
|
||||||
pass
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""
|
|
||||||
Django tries to be helpful by suggesting to run "makemigrations" in red font on every "migrate"
|
|
||||||
run when there are things we have no migrations for. Usually, this is intended, and running
|
|
||||||
"makemigrations" can really screw up the environment of a user, so we want to prevent novice
|
|
||||||
users from doing that by going really dirty and filtering it from the output.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.core.management.base import OutputWrapper
|
|
||||||
from django.core.management.commands.migrate import Command as Parent
|
|
||||||
|
|
||||||
|
|
||||||
class OutputFilter(OutputWrapper):
|
|
||||||
blacklist = (
|
|
||||||
"Your models have changes that are not yet reflected",
|
|
||||||
"Run 'manage.py makemigrations' to make new "
|
|
||||||
)
|
|
||||||
|
|
||||||
def write(self, msg, style_func=None, ending=None):
|
|
||||||
if any(b in msg for b in self.blacklist):
|
|
||||||
return
|
|
||||||
super().write(msg, style_func, ending)
|
|
||||||
|
|
||||||
|
|
||||||
class Command(Parent):
|
|
||||||
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
|
|
||||||
super().__init__(stdout, stderr, no_color, force_color)
|
|
||||||
self.stdout = OutputFilter(stdout or sys.stdout)
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
@@ -9,12 +8,5 @@ class Command(BaseCommand):
|
|||||||
help = "Run periodic tasks"
|
help = "Run periodic tasks"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
for recv, resp in periodic_task.send_robust(self):
|
periodic_task.send(self)
|
||||||
if isinstance(resp, Exception):
|
|
||||||
if settings.SENTRY_ENABLED:
|
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
capture_exception(resp)
|
|
||||||
else:
|
|
||||||
raise resp
|
|
||||||
|
|
||||||
call_command('clearsessions')
|
call_command('clearsessions')
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.core.management import call_command
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django_scopes import scope, scopes_disabled
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
def create_parser(self, *args, **kwargs):
|
|
||||||
parser = super().create_parser(*args, **kwargs)
|
|
||||||
parser.parse_args = lambda x: parser.parse_known_args(x)[0]
|
|
||||||
return parser
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
parser = self.create_parser(sys.argv[0], sys.argv[1])
|
|
||||||
flags = parser.parse_known_args(sys.argv[2:])[1]
|
|
||||||
if "--override" in flags:
|
|
||||||
with scopes_disabled():
|
|
||||||
return call_command("shell_plus", *args, **options)
|
|
||||||
|
|
||||||
lookups = {}
|
|
||||||
for flag in flags:
|
|
||||||
lookup, value = flag.lstrip("-").split("=")
|
|
||||||
lookup = lookup.split("__", maxsplit=1)
|
|
||||||
lookups[lookup[0]] = {
|
|
||||||
lookup[1] if len(lookup) > 1 else "pk": value
|
|
||||||
}
|
|
||||||
models = {
|
|
||||||
model_name.split(".")[-1]: model_class
|
|
||||||
for app_name, app_content in apps.all_models.items()
|
|
||||||
for (model_name, model_class) in app_content.items()
|
|
||||||
}
|
|
||||||
scope_options = {
|
|
||||||
app_name: models[app_name].objects.get(**app_value)
|
|
||||||
for app_name, app_value in lookups.items()
|
|
||||||
}
|
|
||||||
with scope(**scope_options):
|
|
||||||
return call_command("shell_plus", *args, **options)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 2.2 on 2019-05-09 06:54
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import jsonfallback.fields
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0118_auto_20190423_0839'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='question',
|
|
||||||
name='hidden',
|
|
||||||
field=models.BooleanField(default=False, help_text='This question will only show up in the backend.', verbose_name='Hidden question'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Generated by Django 2.2 on 2019-05-09 07:36
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import jsonfallback.fields
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0119_auto_20190509_0654'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='cartposition',
|
|
||||||
name='attendee_name_parts',
|
|
||||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='cartposition',
|
|
||||||
name='subevent',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='cartposition',
|
|
||||||
name='voucher',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Voucher'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='event',
|
|
||||||
name='is_public',
|
|
||||||
field=models.BooleanField(default=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='invoiceaddress',
|
|
||||||
name='name_parts',
|
|
||||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='item',
|
|
||||||
name='sales_channels',
|
|
||||||
field=pretix.base.models.fields.MultiStringField(default=['web']),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='sales_channel',
|
|
||||||
field=models.CharField(default='web', max_length=190),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderposition',
|
|
||||||
name='attendee_name_parts',
|
|
||||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderposition',
|
|
||||||
name='subevent',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.SubEvent'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderposition',
|
|
||||||
name='voucher',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Voucher'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='staffsessionauditlog',
|
|
||||||
name='method',
|
|
||||||
field=models.CharField(max_length=255),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='user',
|
|
||||||
name='email',
|
|
||||||
field=models.EmailField(db_index=True, max_length=190, null=True, unique=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-05-15 05:05
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0120_auto_20190509_0736'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='email_known_to_work',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-05-15 13:23
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.orders
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0121_order_email_known_to_work'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderposition',
|
|
||||||
name='web_secret',
|
|
||||||
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_secret, max_length=32),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-05-30 10:35
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.base
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0122_orderposition_web_secret'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SeatingPlan',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
('name', models.CharField(max_length=190)),
|
|
||||||
('layout', models.TextField()),
|
|
||||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seating_plans', to='pretixbase.Organizer')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SeatCategoryMapping',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
('layout_category', models.CharField(max_length=190)),
|
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seat_category_mappings', to='pretixbase.Event')),
|
|
||||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seat_category_mappings', to='pretixbase.Item')),
|
|
||||||
('subevent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seat_category_mappings', to='pretixbase.SubEvent')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Seat',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
|
||||||
('name', models.CharField(max_length=190)),
|
|
||||||
('blocked', models.BooleanField(default=False)),
|
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='seats', to='pretixbase.Event')),
|
|
||||||
('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seats', to='pretixbase.Item')),
|
|
||||||
('subevent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seats', to='pretixbase.SubEvent')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='cartposition',
|
|
||||||
name='seat',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Seat'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='event',
|
|
||||||
name='seating_plan',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='events', to='pretixbase.SeatingPlan'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderposition',
|
|
||||||
name='seat',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Seat'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='subevent',
|
|
||||||
name='seating_plan',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='subevents', to='pretixbase.SeatingPlan'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-05-30 11:10
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0123_auto_20190530_1035'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='seat',
|
|
||||||
name='seat_guid',
|
|
||||||
field=models.CharField(db_index=True, default=None, max_length=190),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-07-07 10:10
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def set_show_hidden_items(apps, schema_editor):
|
|
||||||
Voucher = apps.get_model('pretixbase', 'Voucher')
|
|
||||||
Voucher.objects.filter(quota__isnull=False).update(show_hidden_items=False)
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0124_seat_seat_guid'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='voucher',
|
|
||||||
name='show_hidden_items',
|
|
||||||
field=models.BooleanField(default=True),
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
set_show_hidden_items,
|
|
||||||
migrations.RunPython.noop,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-07-10 13:45
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0125_voucher_show_hidden_items'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='item',
|
|
||||||
name='show_quota_left',
|
|
||||||
field=models.NullBooleanField(),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-07-11 07:05
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0126_item_show_quota_left'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='question',
|
|
||||||
old_name='dependency_value',
|
|
||||||
new_name='dependency_values',
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='question',
|
|
||||||
name='dependency_values',
|
|
||||||
field=pretix.base.models.fields.MultiStringField(default=['']),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-07-15 15:10
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0127_auto_20190711_0705'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='quota',
|
|
||||||
name='close_when_sold_out',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='quota',
|
|
||||||
name='closed',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-07-24 15:48
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0128_auto_20190715_1510'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='item',
|
|
||||||
name='hidden_if_available',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='pretixbase.Quota'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-07-29 13:11
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0129_auto_20190724_1548'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='seat',
|
|
||||||
name='row_name',
|
|
||||||
field=models.CharField(default='', max_length=190),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='seat',
|
|
||||||
name='seat_number',
|
|
||||||
field=models.CharField(default='', max_length=190),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='seat',
|
|
||||||
name='zone_name',
|
|
||||||
field=models.CharField(default='', max_length=190),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 2.2.1 on 2019-07-29 14:22
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0130_auto_20190729_1311'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='item',
|
|
||||||
name='allow_waitinglist',
|
|
||||||
field=models.BooleanField(default=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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)
|
|
||||||
]
|
|
||||||
@@ -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=[]),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from ..settings import GlobalSettingsObject_SettingsStore
|
from ..settings import GlobalSettingsObject_SettingsStore
|
||||||
from .auth import U2FDevice, User, WebAuthnDevice
|
from .auth import U2FDevice, User
|
||||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||||
from .checkin import Checkin, CheckinList
|
from .checkin import Checkin, CheckinList
|
||||||
from .devices import Device
|
from .devices import Device
|
||||||
@@ -24,7 +24,6 @@ from .orders import (
|
|||||||
from .organizer import (
|
from .organizer import (
|
||||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||||
)
|
)
|
||||||
from .seating import Seat, SeatCategoryMapping, SeatingPlan
|
|
||||||
from .tax import TaxRule
|
from .tax import TaxRule
|
||||||
from .vouchers import Voucher
|
from .vouchers import Voucher
|
||||||
from .waitinglist import WaitingListEntry
|
from .waitinglist import WaitingListEntry
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user