mirror of
https://github.com/pretix/pretix.git
synced 2025-12-10 01:12:28 +00:00
Compare commits
3 Commits
django31
...
validate-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5a2286a4 | ||
|
|
3d66bfee7f | ||
|
|
0b495a4070 |
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Please only create issues for bug reports. Feature requests or general questions
|
||||
should start as a "Discussion" on GitHub.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please only create issues for bug reports. Feature requests or general questions should start as a "Discussion" on GitHub. -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell aspell-en
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur doc/requirements.txt
|
||||
run: pip3 install --no-use-pep517 -Ur doc/requirements.txt
|
||||
- name: Spellcheck docs
|
||||
run: make spelling
|
||||
working-directory: ./doc
|
||||
|
||||
4
.github/workflows/strings.yml
vendored
4
.github/workflows/strings.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur src/requirements.txt
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements.txt
|
||||
- name: Compile messages
|
||||
run: python manage.py compilemessages
|
||||
working-directory: ./src
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur src/requirements/dev.txt
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Spellcheck translations
|
||||
run: potypo
|
||||
working-directory: ./src
|
||||
|
||||
4
.github/workflows/style.yml
vendored
4
.github/workflows/style.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur src/requirements/dev.txt
|
||||
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Run isort
|
||||
run: isort -c .
|
||||
working-directory: ./src
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
|
||||
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
- name: Run flake8
|
||||
run: flake8 .
|
||||
working-directory: ./src
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install gettext mysql-client
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
|
||||
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
file=/tmp/supervisor.sock
|
||||
|
||||
[supervisord]
|
||||
logfile=/dev/stdout
|
||||
logfile_maxbytes=0
|
||||
logfile=/tmp/supervisord.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=10
|
||||
loglevel=info
|
||||
pidfile=/tmp/supervisord.pid
|
||||
nodaemon=false
|
||||
|
||||
@@ -3,7 +3,5 @@ command=/usr/sbin/nginx
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
|
||||
@@ -4,7 +4,3 @@ autostart=true
|
||||
autorestart=true
|
||||
priority=5
|
||||
user=pretixuser
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
@@ -5,7 +5,3 @@ autorestart=true
|
||||
priority=5
|
||||
user=pretixuser
|
||||
environment=HOME=/pretix
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
@@ -60,7 +60,6 @@ Here is the currently recommended set of commands::
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
|
||||
ON pretixbase_invoiceaddress
|
||||
USING gin (upper("company") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email_upper ON public.pretixbase_orderposition (upper((attendee_email)::text));
|
||||
|
||||
|
||||
Also, if you use our ``pretix-shipping`` plugin::
|
||||
|
||||
@@ -95,12 +95,6 @@ pretix_model_instances
|
||||
the ``model`` name. Starting with pretix 3.11, these numbers might only be approximate for
|
||||
most tables when running on PostgreSQL to mitigate performance impact.
|
||||
|
||||
pretix_celery_tasks_queued_count
|
||||
The number of background tasks in the worker queue, labeled with ``queue``.
|
||||
|
||||
pretix_celery_tasks_queued_age_seconds
|
||||
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
||||
|
||||
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||
.. _Prometheus: https://prometheus.io/
|
||||
.. _cProfile: https://docs.python.org/3/library/profile.html
|
||||
|
||||
@@ -183,9 +183,6 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
|
||||
constructed from a number of
|
||||
days before the base point
|
||||
and the base point.
|
||||
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
|
||||
specifiers in requests
|
||||
(see below).
|
||||
===================== ============================ ===================================
|
||||
|
||||
Query parameters
|
||||
@@ -230,48 +227,4 @@ We store idempotency keys for 24 hours, so you should never retry a request afte
|
||||
All ``POST``, ``PUT``, ``PATCH``, or ``DELETE`` api calls support idempotency keys. Adding an idempotency key to a
|
||||
``GET``, ``HEAD``, or ``OPTIONS`` request has no effect.
|
||||
|
||||
|
||||
File upload
|
||||
-----------
|
||||
|
||||
In some places, the API supports working with files, for example when setting the picture of a product. In this case,
|
||||
you will first need to make a separate request to our file upload endpoint:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/upload HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
|
||||
Content-Type: image/png
|
||||
Content-Disposition: attachment; filename="logo.png"
|
||||
Content-Length: 1234
|
||||
|
||||
<raw file content>
|
||||
|
||||
Note that the ``Content-Type`` and ``Content-Disposition`` headers are required. If the upload was successful, you will
|
||||
receive a JSON response with the ID of the file:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
|
||||
}
|
||||
|
||||
You can then use this file ID in the request you want to use it in. File IDs are currently valid for 24 hours and can only
|
||||
be used using the same authorization method and user that was used to upload them.
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/test/events/test/items/3/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"picture": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
|
||||
}
|
||||
|
||||
|
||||
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
||||
|
||||
@@ -8,5 +8,4 @@ This part of the documentation contains how-to guides on some special use cases
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
order_lifecycle
|
||||
custom_checkout
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
Understanding the life cycle of orders
|
||||
======================================
|
||||
|
||||
When integrating pretix with other systems, it is important that you understand how orders and related objects
|
||||
such as order positions, fees, payments, refunds, and invoices work together, in order to react to their changes
|
||||
properly and map them to processes in your system.
|
||||
|
||||
Order states
|
||||
------------
|
||||
|
||||
Generally, an order can be in six states. For compatibility reasons, the ``status`` field only allows four values
|
||||
and the two remaining states are modeled through the ``require_approval`` field and the number of positions within
|
||||
an order. The states and their allowed changes are shown in the following graph:
|
||||
|
||||
.. image:: /images/order_states.png
|
||||
|
||||
|
||||
Object types
|
||||
------------
|
||||
|
||||
Order
|
||||
One order represents one purchase. It's the main object you interact with and bundles all the other objects
|
||||
together. Orders can change in many ways during their lifetime, but will never be deleted (unless ``testmode``
|
||||
is set to ``true``).
|
||||
|
||||
Order position
|
||||
An order position represents one product contained in the order. Orders can usually have multiple positions.
|
||||
There might be a parent-child relation between order positions if one position is an add-on to another position.
|
||||
Order positions can change in many ways during their lifetime, and can also be removed or added to an order.
|
||||
|
||||
Order fees
|
||||
A fee represents a charge that is not related to a product. Examples include shipping fees, service fees, and
|
||||
cancellation fees.
|
||||
Order fees can change in many ways during their lifetime, and can also be removed or added to an order.
|
||||
|
||||
Order payment
|
||||
An order payment represents one payment attempt with a specific payment method and amount. An order can have
|
||||
multiple payments attached.
|
||||
Order payments have their own state diagram. Apart from their state and their meta information (e.g. used
|
||||
credit card, …) they usually don't change. They may be added at any time, but will never be deleted.
|
||||
|
||||
Order refund
|
||||
An order payment represents one refund attempt with a specific payment method and amount. An order can have
|
||||
multiple refunds attached.
|
||||
Order refunds have their own state diagram. Apart from their state and their meta information (e.g. used
|
||||
credit card, …) they usually don't change. They may be added at any time, but will never be deleted.
|
||||
|
||||
Invoice
|
||||
An invoice represents a legal document stating the contents of an order. While the backend technically allows
|
||||
to update an invoice in some situations, invoices are generally considered immutable. Once they are issued,
|
||||
they no longer change. If the order changes substantially (e.g. prices change), an invoice is canceled through
|
||||
creation of a new invoice with the opposite amount, plus the issuance of a new invoice.
|
||||
|
||||
Here's an example of how they all play together:
|
||||
|
||||
.. image:: /images/order_objects.png
|
||||
@@ -42,6 +42,10 @@ seat objects The assigned se
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.17
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
This ``seat`` attribute has been added.
|
||||
|
||||
@@ -25,6 +25,14 @@ is_addon boolean If ``true``, it
|
||||
defining add-ons for other products.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.14
|
||||
|
||||
The operations POST, PATCH, PUT and DELETE have been added.
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
The field ``internal_name`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -36,6 +36,22 @@ rules object Custom check-in
|
||||
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 1.11
|
||||
|
||||
The ``positions`` endpoints have been added.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The ``include_pending`` field has been added.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``auto_checkin_sales_channels`` field has been added.
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
|
||||
@@ -52,6 +68,10 @@ exit_all_at datetime Automatically c
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The ``../status/`` detail endpoint has been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
|
||||
|
||||
Returns a list of all check-in lists within a given event.
|
||||
@@ -360,6 +380,29 @@ Endpoints
|
||||
Order position endpoints
|
||||
------------------------
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The order positions endpoint has been extended by the filter queries ``item__in``, ``variation__in``,
|
||||
``order__status__in``, ``subevent__in``, ``addon_to__in``, and ``search``. The search for attendee names and order
|
||||
codes is now case-insensitive.
|
||||
|
||||
The ``.../redeem/`` endpoint has been added.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
The resource now contains the new attributes ``require_attention`` and ``order__status`` and accepts the new
|
||||
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
|
||||
returns ``400`` instead of ``404`` on tickets which are known but not paid.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed
|
||||
automatically by the system.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
|
||||
|
||||
Returns a list of all order positions within a given event. The result is the same as
|
||||
|
||||
@@ -52,6 +52,31 @@ sales_channels list A list of sales
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The ``meta_data`` field has been added.
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The ``plugins`` field has been added.
|
||||
The operations POST, PATCH, PUT and DELETE have been added.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
Filters have been added to the list of events.
|
||||
|
||||
.. versionchanged:: 2.5
|
||||
|
||||
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.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
@@ -15,24 +15,8 @@ number string Invoice number
|
||||
order string Order code of the order this invoice belongs to
|
||||
is_cancellation boolean ``true``, if this invoice is the cancellation of a
|
||||
different invoice.
|
||||
invoice_from_name string Sender address: Name
|
||||
invoice_from string Sender address: Address lines
|
||||
invoice_from_zipcode string Sender address: ZIP code
|
||||
invoice_from_city string Sender address: City
|
||||
invoice_from_country string Sender address: Country code
|
||||
invoice_from_tax_id string Sender address: Local Tax ID
|
||||
invoice_from_vat_id string Sender address: EU VAT ID
|
||||
invoice_to string Full recipient address
|
||||
invoice_to_company string Recipient address: Company name
|
||||
invoice_to_name string Recipient address: Person name
|
||||
invoice_to_street string Recipient address: Address lines
|
||||
invoice_to_zipcode string Recipient address: ZIP code
|
||||
invoice_to_city string Recipient address: City
|
||||
invoice_to_state string Recipient address: State (only used in some countries)
|
||||
invoice_to_country string Recipient address: Country code
|
||||
invoice_to_vat_id string Recipient address: EU VAT ID
|
||||
invoice_to_beneficiary string Invoice beneficiary
|
||||
custom_field string Custom invoice address field
|
||||
invoice_from string Sender address
|
||||
invoice_to string Receiver address
|
||||
date date Invoice date
|
||||
refers string Invoice number of an invoice this invoice refers to
|
||||
(for example a cancellation refers to the invoice it
|
||||
@@ -46,31 +30,6 @@ footer_text string Text to be prin
|
||||
lines list of objects The actual invoice contents
|
||||
├ position integer Number of the line within an invoice.
|
||||
├ description string Text representing the invoice line (e.g. product name)
|
||||
├ item integer Product used to create this line. Note that everything
|
||||
about the product might have changed since the creation
|
||||
of the invoice. Can be ``null`` for all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a product (e.g. a shipping or
|
||||
cancellation fee).
|
||||
├ variation integer Product variation used to create this line. Note that everything
|
||||
about the product might have changed since the creation
|
||||
of the invoice. Can be ``null`` for all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a product (e.g. a shipping or
|
||||
cancellation fee).
|
||||
├ event_date_from datetime Start date of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees).
|
||||
├ event_date_to datetime End date of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees) as well as whenever the respective (sub)event
|
||||
has no end date set.
|
||||
├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
|
||||
name was set or if names are configured to not be added to invoices.
|
||||
├ gross_value money (string) Price including taxes
|
||||
├ tax_value money (string) Tax amount included
|
||||
├ tax_name string Name of used tax rate (e.g. "VAT")
|
||||
@@ -87,16 +46,28 @@ internal_reference string Customer's refe
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
|
||||
The attribute ``invoice_no`` has been dropped in favor of ``number`` which includes the number including the prefix,
|
||||
since the prefix can now vary. Also, invoices now need to be identified by their ``number`` instead of the raw
|
||||
number.
|
||||
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attributes ``lines.tax_name``, ``foreign_currency_display``, ``foreign_currency_rate``, and
|
||||
``foreign_currency_rate_date`` have been added.
|
||||
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
The attribute ``internal_reference`` has been added.
|
||||
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``lines.number`` has been added.
|
||||
|
||||
.. versionchanged:: 3.17
|
||||
|
||||
The attribute ``invoice_to_*``, ``invoice_from_*``, ``custom_field``, ``lines.item``, ``lines.variation``, ``lines.event_date_from``,
|
||||
``lines.event_date_to``, and ``lines.attendee_name`` have been added.
|
||||
``refers`` now returns an invoice number including the prefix.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -130,24 +101,8 @@ Endpoints
|
||||
"number": "SAMPLECONF-00001",
|
||||
"order": "ABC12",
|
||||
"is_cancellation": false,
|
||||
"invoice_from_name": "Big Events LLC",
|
||||
"invoice_from": "Demo street 12",
|
||||
"invoice_from_zipcode":"",
|
||||
"invoice_from_city":"Demo town",
|
||||
"invoice_from_country":"US",
|
||||
"invoice_from_tax_id":"",
|
||||
"invoice_from_vat_id":"",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||
"invoice_to_company": "Sample company",
|
||||
"invoice_to_name": "John Doe",
|
||||
"invoice_to_street": "Test street 12",
|
||||
"invoice_to_zipcode": "12345",
|
||||
"invoice_to_city": "Testington",
|
||||
"invoice_to_state": null,
|
||||
"invoice_to_country": "TE",
|
||||
"invoice_to_vat_id": "EU123456789",
|
||||
"invoice_to_beneficiary": "",
|
||||
"custom_field": null,
|
||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
|
||||
"date": "2017-12-01",
|
||||
"refers": null,
|
||||
"locale": "en",
|
||||
@@ -160,11 +115,6 @@ Endpoints
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"item": 1234,
|
||||
"variation": 245,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
@@ -216,24 +166,8 @@ Endpoints
|
||||
"number": "SAMPLECONF-00001",
|
||||
"order": "ABC12",
|
||||
"is_cancellation": false,
|
||||
"invoice_from_name": "Big Events LLC",
|
||||
"invoice_from": "Demo street 12",
|
||||
"invoice_from_zipcode":"",
|
||||
"invoice_from_city":"Demo town",
|
||||
"invoice_from_country":"US",
|
||||
"invoice_from_tax_id":"",
|
||||
"invoice_from_vat_id":"",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||
"invoice_to_company": "Sample company",
|
||||
"invoice_to_name": "John Doe",
|
||||
"invoice_to_street": "Test street 12",
|
||||
"invoice_to_zipcode": "12345",
|
||||
"invoice_to_city": "Testington",
|
||||
"invoice_to_state": null,
|
||||
"invoice_to_country": "TE",
|
||||
"invoice_to_vat_id": "EU123456789",
|
||||
"invoice_to_beneficiary": "",
|
||||
"custom_field": null,
|
||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
|
||||
"date": "2017-12-01",
|
||||
"refers": null,
|
||||
"locale": "en",
|
||||
@@ -246,11 +180,6 @@ Endpoints
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"item": 1234,
|
||||
"variation": 245,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
|
||||
@@ -28,6 +28,10 @@ multi_allowed boolean Adding the same
|
||||
price_included boolean Adding this add-on to the item is free
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ designated_price money (string) Designated pric
|
||||
taxation. This is not added to the price.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -26,6 +26,14 @@ description multi-lingual string A public descri
|
||||
position integer An integer, used for sorting
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
The attribute ``original_price`` has been added.
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ admission boolean ``true`` for it
|
||||
(such as primary tickets) and ``false`` for others
|
||||
(such as add-ons or merchandise).
|
||||
position integer An integer, used for sorting
|
||||
picture file A product picture to be displayed in the shop
|
||||
(can be ``null``).
|
||||
picture string A product picture to be displayed in the shop
|
||||
(read-only, can be ``null``).
|
||||
sales_channels list of strings Sales channels this product is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
@@ -118,6 +118,44 @@ bundles list of objects Definition of b
|
||||
meta_data object Values set for event-specific meta data parameters.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
The attribute ``original_price`` has been added for ``variations``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
|
||||
``checkin_attention`` has been added.
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
The attribute ``price_included`` has been added to ``addons``.
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
The ``internal_name`` and ``original_price`` fields have been added.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
The field ``require_approval`` has been added.
|
||||
|
||||
.. versionchanged:: 2.3
|
||||
|
||||
The ``sales_channels`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2.4
|
||||
|
||||
The ``generate_tickets`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
|
||||
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.
|
||||
|
||||
.. versionchanged:: 3.7
|
||||
|
||||
The attribute ``meta_data`` has been added.
|
||||
|
||||
@@ -94,6 +94,60 @@ last_modified datetime Last modificati
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
|
||||
The ``invoice_address.country`` attribute contains a two-letter country code for all new orders. For old orders,
|
||||
a custom text might still be returned.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
|
||||
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
|
||||
deprecated in favor of the new ``fees`` attribute but will still be served and removed in 1.9.
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
|
||||
The attribute ``invoice_address.internal_reference`` has been added.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The field ``checkin_attention`` has been added.
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate``, ``order.payment_fee_tax_value`` and
|
||||
``order.payment_fee_tax_rule`` have finally been removed.
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
|
||||
An endpoint for order creation as well as ``…/mark_refunded/`` has been added.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
The ``order.payment_date`` and ``order.payment_provider`` attributes have been deprecated in favor of the new
|
||||
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2. The ``require_approval``
|
||||
attribute has been added, as have been the ``…/approve/`` and ``…/deny/`` endpoints.
|
||||
|
||||
.. versionchanged:: 2.3
|
||||
|
||||
The ``sales_channel`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2.4
|
||||
|
||||
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
|
||||
``…/mark_refunded/`` has been deprecated.
|
||||
|
||||
.. versionchanged:: 2.5
|
||||
|
||||
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.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``order.fees.canceled`` attribute has been added.
|
||||
@@ -166,7 +220,7 @@ downloads list of objects List of ticket
|
||||
└ url string Download URL
|
||||
answers list of objects Answers to user-defined questions
|
||||
├ question integer Internal ID of the answered question
|
||||
├ answer string Text representation of the answer (URL if answer is a file)
|
||||
├ answer string Text representation of the answer
|
||||
├ question_identifier string The question's ``identifier`` field
|
||||
├ 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
|
||||
@@ -179,6 +233,30 @@ pdf_data object Data object req
|
||||
``pdf_data=true`` query parameter to your request.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attribute ``tax_rule`` has been added.
|
||||
|
||||
.. versionchanged:: 1.11
|
||||
|
||||
The attribute ``checkins.list`` has been added.
|
||||
|
||||
.. versionchanged:: 1.14
|
||||
|
||||
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
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.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
|
||||
@@ -196,10 +274,6 @@ pdf_data object Data object req
|
||||
|
||||
The ``checkin.type`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 3.16
|
||||
|
||||
Answers to file questions are now returned as an URL.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -228,6 +302,14 @@ details object Payment-specifi
|
||||
the object is empty.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The attributes ``payment_url`` and ``details`` have been added.
|
||||
|
||||
.. _order-refund-resource:
|
||||
|
||||
Order refund resource
|
||||
@@ -243,14 +325,21 @@ state string Payment state,
|
||||
source string How this refund has been created, one of ``buyer``, ``admin``, or ``external``
|
||||
amount money (string) Payment amount
|
||||
created datetime Date and time of creation of this payment
|
||||
comment string Reason for refund (shown to the customer in some cases, can be ``null``).
|
||||
execution_date datetime Date and time of completion of this refund (or ``null``)
|
||||
payment_date datetime Date and time of completion of this payment (or ``null``)
|
||||
provider string Identification string of the payment provider
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
This resource has been added.
|
||||
|
||||
List of all orders
|
||||
------------------
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
Filtering for emails or order codes is now case-insensitive.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
@@ -1356,6 +1445,21 @@ Sending e-mails
|
||||
List of all order positions
|
||||
---------------------------
|
||||
|
||||
.. versionchanged:: 1.15
|
||||
|
||||
The order positions endpoint has been extended by the filter queries ``item__in``, ``variation__in``,
|
||||
``order__status__in``, ``subevent__in``, ``addon_to__in`` and ``search``. The search for attendee names and order
|
||||
codes is now case-insensitive.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
|
||||
``pseudonymization_id``.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
@@ -1602,67 +1706,6 @@ Order position ticket download
|
||||
Manipulating individual positions
|
||||
---------------------------------
|
||||
|
||||
.. versionchanged:: 3.15
|
||||
|
||||
The ``PATCH`` method has been added for individual positions.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||
|
||||
Updates specific fields on an order position. Currently, only the following fields are supported:
|
||||
|
||||
* ``attendee_email``
|
||||
|
||||
* ``attendee_name_parts`` or ``attendee_name``
|
||||
|
||||
* ``company``
|
||||
|
||||
* ``street``
|
||||
|
||||
* ``zipcode``
|
||||
|
||||
* ``city``
|
||||
|
||||
* ``country``
|
||||
|
||||
* ``state``
|
||||
|
||||
* ``answers``: If specified, you will need to provide **all** answers for this order position.
|
||||
Validation is handled the same way as when creating orders through the API. You are therefore
|
||||
expected to provide ``question``, ``answer``, and possibly ``options``. ``question_identifier``
|
||||
and ``option_identifiers`` will be ignored.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"attendee_email": "other@example.org"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full order resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param id: The ``id`` field of the order position to update
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||
|
||||
Deletes an order position, identified by its internal ID.
|
||||
@@ -1695,6 +1738,10 @@ Manipulating individual positions
|
||||
Order payment endpoints
|
||||
-----------------------
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
These endpoints have been added.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Payments can now be created through the API.
|
||||
@@ -1974,6 +2021,10 @@ Order payment endpoints
|
||||
Order refund endpoints
|
||||
----------------------
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
|
||||
These endpoints have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
|
||||
|
||||
Returns a list of all refunds for an order.
|
||||
@@ -2007,7 +2058,6 @@ Order refund endpoints
|
||||
"payment": 1,
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": "2017-12-04T12:13:12Z",
|
||||
"comment": "Cancellation",
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
]
|
||||
@@ -2050,7 +2100,6 @@ Order refund endpoints
|
||||
"payment": 1,
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": "2017-12-04T12:13:12Z",
|
||||
"comment": "Cancellation",
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
|
||||
@@ -2085,7 +2134,6 @@ Order refund endpoints
|
||||
"amount": "23.00",
|
||||
"payment": 1,
|
||||
"execution_date": null,
|
||||
"comment": "Cancellation",
|
||||
"provider": "manual",
|
||||
"mark_canceled": false,
|
||||
"mark_pending": true
|
||||
@@ -2107,7 +2155,6 @@ Order refund endpoints
|
||||
"payment": 1,
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": null,
|
||||
"comment": "Cancellation",
|
||||
"provider": "manual"
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ identifier string An arbitrary st
|
||||
answer multi-lingual string The displayed value of this option
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -75,6 +75,28 @@ dependency_value string An old version
|
||||
for one value. **Deprecated.**
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
|
||||
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed and the ``ask_during_checkin`` field has
|
||||
been added.
|
||||
|
||||
.. versionchanged:: 1.14
|
||||
|
||||
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
|
||||
options resource. The ``position`` attribute has been added to the options resource.
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
The attribute ``hidden`` and the question type ``CC`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attribute ``dependency_values`` has been added.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
|
||||
The attribute ``print_on_invoice`` has been added.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``help_text`` has been added.
|
||||
|
||||
@@ -30,6 +30,14 @@ release_after_exit boolean Whether the quo
|
||||
have been scanned at an exit.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
|
||||
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.
|
||||
|
||||
.. versionchanged:: 3.10
|
||||
|
||||
The attribute ``release_after_exit`` has been added.
|
||||
|
||||
@@ -20,6 +20,10 @@ layout object JSON representa
|
||||
still evolves. The version in use can be found `here`_.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
This endpoint has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ date_to datetime The sub-event's
|
||||
date_admission datetime The sub-event's admission date (or ``null``)
|
||||
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
|
||||
frontpage_text multi-lingual string The description of the event (or ``null``)
|
||||
location multi-lingual string The sub-event location (or ``null``)
|
||||
geo_lat float Latitude of the location (or ``null``)
|
||||
geo_lon float Longitude of the location (or ``null``)
|
||||
@@ -55,6 +54,25 @@ seat_category_mapping object An object mappi
|
||||
last_modified datetime Last modification of this object
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The ``meta_data`` field has been added.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
.. versionchanged:: 2.7
|
||||
|
||||
The attribute ``is_public`` has been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
@@ -24,6 +24,14 @@ home_country string Merchant countr
|
||||
``null`` or empty string
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -46,6 +46,14 @@ show_hidden_items boolean Only if set to
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
The attribute ``show_hidden_items`` has been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``seat`` has been added.
|
||||
|
||||
@@ -13,10 +13,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the waiting list entry
|
||||
created datetime Creation date of the waiting list entry
|
||||
name string Name of the user on the waiting list (or ``null``)
|
||||
name_parts object of strings Decomposition of name of the user (or ``null``)
|
||||
email string Email address of the user on the waiting list
|
||||
phone string Phone number of the user on the waiting list (or ``null``)
|
||||
voucher integer Internal ID of the voucher sent to this user. If
|
||||
this field is set, the user has been sent a voucher
|
||||
and is no longer waiting. If it is ``null``, the
|
||||
|
||||
@@ -34,7 +34,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
@@ -79,7 +79,7 @@ Ticket designs
|
||||
""""""""""""""
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: layout_text_variables, layout_image_variables
|
||||
:members: layout_text_variables
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
:members: override_layout
|
||||
|
||||
@@ -106,22 +106,14 @@ The provider class
|
||||
|
||||
.. automethod:: payment_control_render
|
||||
|
||||
.. automethod:: payment_control_render_short
|
||||
|
||||
.. automethod:: payment_refund_supported
|
||||
|
||||
.. automethod:: payment_partial_refund_supported
|
||||
|
||||
.. automethod:: payment_presale_render
|
||||
|
||||
.. automethod:: execute_refund
|
||||
|
||||
.. automethod:: refund_control_render
|
||||
|
||||
.. automethod:: new_refund_control_form_render
|
||||
|
||||
.. automethod:: new_refund_control_form_process
|
||||
|
||||
.. automethod:: api_payment_details
|
||||
|
||||
.. automethod:: matching_id
|
||||
|
||||
@@ -82,15 +82,11 @@ Orders
|
||||
^^^^^^
|
||||
|
||||
If a customer completes the checkout process, an **Order** will be created containing all the entered information.
|
||||
An order can be in one of currently six states that are listed in the diagram below:
|
||||
An order can be in one of currently four states that are listed in the diagram below:
|
||||
|
||||
.. image:: /images/order_states.png
|
||||
|
||||
The dotted lines represent status changes that usually do not happen as part of the regular process, but can be
|
||||
performed manually in the admin backend.
|
||||
|
||||
For historical reasons, there are only four valid values of the ``status`` field, and the two additional states are
|
||||
represented differently:
|
||||
There are additional "fake" states that are displayed like states but not represented as states in the system:
|
||||
|
||||
* An order is considered **canceled (with paid fee)** if it is in **paid** status but does not include any non-cancelled positions.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
@@ -1,34 +0,0 @@
|
||||
@startuml
|
||||
|
||||
participant User
|
||||
collections "OrderPayment\nOrderRefund" as P
|
||||
collections "Order\nOrderPosition" as O
|
||||
collections "Invoice\nInvoiceLine" as I
|
||||
|
||||
User -> O: Order placed (€100)
|
||||
rnote over O #6DD96D: Order A1B2C\nstatus = **n**\ntotal = €100
|
||||
O -> P: Payment created
|
||||
O -> I: Invoice created\n(can also happen later)
|
||||
rnote over I #6DD96D: Invoice 00001\n€100
|
||||
rnote over P #6DD96D: OrderPayment A1B2C-P-1\nstate = **created**
|
||||
P -> User: Payment details (web, email)
|
||||
User -> P: Payment performed
|
||||
rnote over P #EFF46B: OrderPayment A1B2C-P-1\nstate = **confirmed**
|
||||
P -> O: Order marked as paid
|
||||
rnote over O #EFF46B: Order A1B2C\nstatus = **p**\ntotal = €100
|
||||
User -> O: Data change (e.g. invoice address)
|
||||
O -> I: Invoice reissued
|
||||
rnote over I #6DD96D: Invoice 00002\n€-100
|
||||
rnote over I #6DD96D: Invoice 00003\n€100
|
||||
rnote over O #EFF46B: Order A1B2C\nstatus = **p**\ntotal = €100
|
||||
User -> O: Order canceled
|
||||
rnote over O #EFF46B: Order A1B2C\nstatus = **c**
|
||||
O -> I: Invoice canceled
|
||||
rnote over I #6DD96D: Invoice 00004\n€-100
|
||||
O -> P: Refund started
|
||||
rnote over P #6DD96D: OrderRefund\nA1B2C-R-1\nstate = **created**
|
||||
P -> User: Money sent
|
||||
rnote over P #EFF46B: OrderRefund\nA1B2C-R-1\nstate = **done**
|
||||
|
||||
@enduml
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 53 KiB |
@@ -1,39 +1,19 @@
|
||||
@startuml
|
||||
|
||||
state "Approval Pending" as AP
|
||||
state "Canceled (with paid fee)" as CP
|
||||
AP: status = "n"
|
||||
AP: require_approval = true
|
||||
Pending: status = "n"
|
||||
Pending: require_approval = false
|
||||
Pending: Tickets reserved: yes
|
||||
Expired: status = "e"
|
||||
Expired: Tickets reserved: no
|
||||
Paid: status = "p"
|
||||
Paid: count(positions | !canceled) > 0
|
||||
Paid: Tickets reserved: yes
|
||||
CP: status = "p"
|
||||
CP: count(positions | !canceled) = 0
|
||||
Canceled: status = "c"
|
||||
Canceled: Tickets reserved: no
|
||||
Pending: Order is expecting payment\nOrder reduces quotas
|
||||
Expired: Payment period is over\nOrder does not affect quotas
|
||||
Paid: Order was successful\nOrder reduces quotas
|
||||
Canceled: Order has been canceled\nOrder does not affect quotas
|
||||
|
||||
|
||||
[*] -> Pending: order placed\ntotal > 0
|
||||
[*] -> Paid: order placed\ntotal = 0
|
||||
[*] -> AP: order placed\napproval required
|
||||
Pending --> Paid: order paid
|
||||
Pending --> Expired: after payment\ndeadline
|
||||
Expired --> Paid: order paid\n(only if quota left)
|
||||
Expired -[dashed]-> Canceled
|
||||
Expired -[dashed]-> Pending: order extended
|
||||
Paid --> Canceled: order canceled
|
||||
Pending --> Canceled: order canceled
|
||||
Paid -[dashed]-> Pending: refund
|
||||
AP --> Pending: order approved
|
||||
AP --> Canceled: order denied
|
||||
Paid --> CP: order canceled\n(with cancellation fee)
|
||||
Canceled -[dashed]-> Pending: order reactivated
|
||||
Canceled -[dashed]-> Paid: order reactivated
|
||||
CP -[dashed]-> Canceled: fee canceled
|
||||
[*] --> Pending: customer\nplaces order
|
||||
Pending --> Paid: successful payment
|
||||
Pending --> Expired: automatically\nor manually\non admin action
|
||||
Expired --> Paid: if payment is received\nonly if quota left
|
||||
Expired --> Canceled
|
||||
Expired --> Pending: manually\non admin action
|
||||
Paid --> Canceled: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund
|
||||
Pending --> Canceled: on admin or\ncustomer action
|
||||
Paid -> Pending: manually on admin action
|
||||
[*] --> Paid: customer\nplaces free order
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -22,6 +22,10 @@ item_assignments list of objects Products this l
|
||||
└ item integer Item ID
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
This resource has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -24,6 +24,14 @@ item_assignments list of objects Products this l
|
||||
└ item integer Item ID
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
This resource has been added.
|
||||
|
||||
.. versionchanged:: 2.3
|
||||
|
||||
The ``item_assignments.sales_channel`` field has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -64,35 +64,20 @@ is valid in every text):
|
||||
Placeholder Description
|
||||
============================== ===============================================================================
|
||||
event The event name
|
||||
event_slug The event's short form
|
||||
code In case of the waiting list, the voucher code to redeem
|
||||
currency The currency used for the event (three-letter code)
|
||||
total The order's total value
|
||||
total_with_currency The order's total value with a localized currency sign
|
||||
refund_amount (For cancellation emails) The amount of money that will be refunded, including
|
||||
the currency
|
||||
currency The currency used for the event (three-letter code)
|
||||
payment_info Information text specific to the payment method (e.g. banking details)
|
||||
url An URL pointing to the download/status page of the order
|
||||
url_info_change An URL pointing to the page of the order that can be used to change ticket
|
||||
information
|
||||
url_products_change An URL pointing to the page of the order that can be used to change the products
|
||||
in the order
|
||||
url_cancel An URL pointing to the page of the order that can be used to cancel the order
|
||||
name, name_* Any name that can be used to address the recipient (e.g. name from invoice address,
|
||||
name from first ticket, …)
|
||||
invoice_name, invoice_name_* The name field of the invoice address
|
||||
invoice_name The name field of the invoice address
|
||||
invoice_company The company field of the invoice address
|
||||
attendee_name, attendee_name_* The name of the attendee represented by the ticket
|
||||
expire_date The order's expiration date
|
||||
comment When rejecting an order, this will contain the reason for the rejection
|
||||
date The same as ``expire_date``, but in a different e-mail (for backwards
|
||||
compatibility)
|
||||
orders A list of orders including links to their status pages, specific to the "resend
|
||||
link (requested by user)" e-mail
|
||||
code In case of the waiting list, the voucher code to redeem
|
||||
hours In case of the waiting list, the number of hours the voucher code is valid
|
||||
product In case of the waiting list, the product that has become available
|
||||
voucher_list When sending out vouchers in bulk, this will be replaced with the list of
|
||||
vouchers
|
||||
============================== ===============================================================================
|
||||
|
||||
The different e-mails are explained in the following:
|
||||
|
||||
@@ -88,15 +88,6 @@ website. If you confident to have a good reason for not using SSL, you can overr
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" skip-ssl-check></pretix-widget>
|
||||
|
||||
Always open a new tab
|
||||
---------------------
|
||||
|
||||
If you want the checkout process to always open a new tab regardless of screen size, you can pass the ``disable-iframe``
|
||||
attribute::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-iframe></pretix-widget>
|
||||
|
||||
|
||||
Pre-selecting a voucher
|
||||
-----------------------
|
||||
|
||||
@@ -206,10 +197,7 @@ should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,ite
|
||||
where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
|
||||
items, if the items have variations. If you omit the ``items`` attribute, the general start page will be presented.
|
||||
|
||||
In case you are using an event-series, you will need to specify the subevent for which the item(s) should be put in the
|
||||
cart. This can be done by specifying the ``subevent``-attribute.
|
||||
|
||||
Just as the widget, the button supports the optional attributes ``voucher``, ``disable-iframe``, and ``skip-ssl-check``.
|
||||
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
|
||||
|
||||
You can style the button using the ``pretix-button`` CSS class.
|
||||
|
||||
@@ -316,92 +304,8 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
|
||||
made through this widget will be counted towards this campaign.
|
||||
|
||||
* If you use the tracking plugin, you can enable cross-domain tracking. To do so, you need to initialize the
|
||||
pretix-widget manually. Use the html code to embed the widget and add one the following code snippets. Make sure to
|
||||
replace all occurrences of <MEASUREMENT_ID> with your Google Analytics MEASUREMENT_ID (UA-XXXXXXX-X or G-XXXXXXXX)
|
||||
|
||||
Please also make sure to add the embedding website to your `Referral exclusions
|
||||
<https://support.google.com/analytics/answer/2795830>`_ in your Google Analytics settings.
|
||||
|
||||
If you use Google Analytics 4 (GA4 – G-XXXXXXXX)::
|
||||
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
|
||||
<script type="text/javascript">
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<MEASUREMENT_ID>');
|
||||
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.build_widgets = false;
|
||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||
if (!window['google_tag_manager']) {
|
||||
window.PretixWidget.buildWidgets();
|
||||
return;
|
||||
}
|
||||
|
||||
var clientId;
|
||||
var sessionId;
|
||||
var loadingTimeout;
|
||||
function build() {
|
||||
// use loadingTimeout to make sure build() is only called once
|
||||
if (!loadingTimeout) return;
|
||||
window.clearTimeout(loadingTimeout);
|
||||
loadingTimeout = null;
|
||||
if (clientId) window.PretixWidget.widget_data["tracking-ga-id"] = clientId;
|
||||
if (sessionId) window.PretixWidget.widget_data["tracking-ga-sessid"] = sessionId;
|
||||
window.PretixWidget.buildWidgets();
|
||||
};
|
||||
// make sure to build pretix-widgets if gtag fails to load either client_id or session_id
|
||||
loadingTimeout = window.setTimeout(build, 2000);
|
||||
|
||||
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
||||
clientId = id;
|
||||
if (sessionId !== undefined) build();
|
||||
});
|
||||
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
|
||||
sessionId = id;
|
||||
if (clientId !== undefined) build();
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
If you use Universal Analytics with ``gtag.js`` (UA-XXXXXXX-X)::
|
||||
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
|
||||
<script type="text/javascript">
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<MEASUREMENT_ID>');
|
||||
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.build_widgets = false;
|
||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||
if (!window['google_tag_manager']) {
|
||||
window.PretixWidget.buildWidgets();
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure to build pretix-widgets if gtag fails to load client_id
|
||||
var loadingTimeout = window.setTimeout(function() {
|
||||
loadingTimeout = null;
|
||||
window.PretixWidget.buildWidgets();
|
||||
}, 1000);
|
||||
|
||||
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
||||
if (loadingTimeout) {
|
||||
window.clearTimeout(loadingTimeout);
|
||||
window.PretixWidget.widget_data["tracking-ga-id"] = id;
|
||||
window.PretixWidget.buildWidgets();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
If you use ```analytics.js` (Universal Analytics)::
|
||||
* If you use the tracking plugin, you can pass a Google Analytics User ID to enable cross-domain tracking. This will
|
||||
require you to dynamically load the widget, like this::
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
@@ -409,33 +313,32 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '<MEASUREMENT_ID>', 'auto');
|
||||
ga('create', 'UA-XXXXXX-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.build_widgets = false;
|
||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||
if (!window['ga'] || !ga.create) {
|
||||
// Tracking is probably blocked
|
||||
window.PretixWidget.buildWidgets()
|
||||
return;
|
||||
}
|
||||
|
||||
var loadingTimeout = window.setTimeout(function() {
|
||||
loadingTimeout = null;
|
||||
window.PretixWidget.buildWidgets();
|
||||
}, 1000);
|
||||
ga(function(tracker) {
|
||||
if (loadingTimeout) {
|
||||
window.clearTimeout(loadingTimeout);
|
||||
if(window.ga && ga.create) {
|
||||
ga(function(tracker) {
|
||||
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
|
||||
window.PretixWidget.buildWidgets();
|
||||
}
|
||||
});
|
||||
window.PretixWidget.buildWidgets()
|
||||
});
|
||||
} else { // Tracking is probably blocked
|
||||
window.PretixWidget.buildWidgets()
|
||||
}
|
||||
});
|
||||
};
|
||||
</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
|
||||
|
||||
Data passing options have been added in pretix 2.3. If you use a self-hosted version of pretix, they only work
|
||||
fully if you configured a redis server.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ localecompile:
|
||||
./manage.py compilemessages
|
||||
|
||||
localegen:
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
|
||||
staticfiles: jsi18n
|
||||
./manage.py collectstatic --noinput
|
||||
@@ -23,8 +23,3 @@ test:
|
||||
|
||||
coverage:
|
||||
coverage run -m py.test
|
||||
|
||||
npminstall:
|
||||
mkdir -p pretix/static.dist/node_prefix
|
||||
npm install --prefix=pretix/static.dist/node_prefix pretix/static/npm_dir/
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.17.0.dev0"
|
||||
__version__ = "3.15.0.dev0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class FullAccessSecurityProfile:
|
||||
@@ -41,9 +41,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
)
|
||||
|
||||
|
||||
@@ -69,9 +67,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
)
|
||||
|
||||
|
||||
@@ -99,9 +95,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:order-detail'),
|
||||
('DELETE', 'api-v1:orderposition-detail'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('POST', 'api-v1:order-mark_canceled'),
|
||||
('POST', 'api-v1:orderpayment-list'),
|
||||
('POST', 'api-v1:orderrefund-list'),
|
||||
('POST', 'api-v1:orderrefund-done'),
|
||||
('POST', 'api-v1:cartposition-list'),
|
||||
@@ -119,7 +113,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'plugins:pretix_seating:event.event.subevent'),
|
||||
('GET', 'plugins:pretix_seating:event.plan'),
|
||||
('GET', 'plugins:pretix_seating:selection.simple'),
|
||||
('POST', 'api-v1:upload'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -89,38 +89,10 @@ class EventCRUDPermission(EventPermission):
|
||||
class ProfilePermission(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
|
||||
if not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
assert_session_valid(request)
|
||||
except SessionInvalid:
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
|
||||
if isinstance(request.auth, OAuthAccessToken):
|
||||
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AnyAuthenticatedClientPermission(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
|
||||
return False
|
||||
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
assert_session_valid(request)
|
||||
except SessionInvalid:
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files import File
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy
|
||||
@@ -101,15 +100,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options')
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
answ = cp.answers.create(**answ_data, answer='')
|
||||
answ.file.save(an.name, an, save=False)
|
||||
answ.answer = 'file://' + answ.file.name
|
||||
answ.save()
|
||||
else:
|
||||
answ = cp.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
answ = cp.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
return cp
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from pytz import common_timezones
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
from pretix.base.models import Event, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
from pretix.base.settings import validate_event_settings
|
||||
from pretix.base.settings import DEFAULTS, validate_event_settings
|
||||
from pretix.base.signals import api_event_settings_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetaDataField(Field):
|
||||
|
||||
@@ -174,12 +170,9 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
}
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key, v in value['meta_data'].items():
|
||||
for key in value['meta_data'].keys():
|
||||
if key not in self.meta_properties:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||
if self.meta_properties[key].allowed_values:
|
||||
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
@@ -226,14 +219,6 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
def ignored_meta_properties(self):
|
||||
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
||||
else self.context['request'].user)
|
||||
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
|
||||
return []
|
||||
return [k for k, p in self.meta_properties.items() if p.protected]
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
@@ -249,11 +234,10 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
if key not in self.ignored_meta_properties:
|
||||
event.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
event.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
@@ -291,21 +275,19 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
|
||||
for key, value in meta_data.items():
|
||||
if key not in self.ignored_meta_properties:
|
||||
prop = self.meta_properties.get(key)
|
||||
if prop in current:
|
||||
current[prop].value = value
|
||||
current[prop].save()
|
||||
else:
|
||||
event.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
prop = self.meta_properties.get(key)
|
||||
if prop in current:
|
||||
current[prop].value = value
|
||||
current[prop].save()
|
||||
else:
|
||||
event.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
for prop, current_object in current.items():
|
||||
if prop.name not in self.ignored_meta_properties:
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
@@ -409,8 +391,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
model = SubEvent
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
|
||||
'meta_data', 'seat_category_mapping', 'last_modified')
|
||||
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
|
||||
'seat_category_mapping', 'last_modified')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -458,22 +440,11 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
}
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key, v in value['meta_data'].items():
|
||||
for key in value['meta_data'].keys():
|
||||
if key not in self.meta_properties:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||
if self.meta_properties[key].allowed_values:
|
||||
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
def ignored_meta_properties(self):
|
||||
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
||||
else self.context['request'].user)
|
||||
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
|
||||
return []
|
||||
return [k for k, p in self.meta_properties.items() if p.protected]
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
@@ -490,11 +461,10 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
if key not in self.ignored_meta_properties:
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
# Seats
|
||||
if subevent.seating_plan:
|
||||
@@ -540,21 +510,19 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in subevent.meta_values.select_related('property')}
|
||||
for key, value in meta_data.items():
|
||||
if key not in self.ignored_meta_properties:
|
||||
prop = self.meta_properties.get(key)
|
||||
if prop in current:
|
||||
current[prop].value = value
|
||||
current[prop].save()
|
||||
else:
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
prop = self.meta_properties.get(key)
|
||||
if prop in current:
|
||||
current[prop].value = value
|
||||
current[prop].save()
|
||||
else:
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
for prop, current_object in current.items():
|
||||
if prop.name not in self.ignored_meta_properties:
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
# Seats
|
||||
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
|
||||
@@ -590,13 +558,12 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||
|
||||
|
||||
class EventSettingsSerializer(SettingsSerializer):
|
||||
class EventSettingsSerializer(serializers.Serializer):
|
||||
default_fields = [
|
||||
'imprint_url',
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'voucher_explanation_text',
|
||||
'checkout_success_text',
|
||||
'banner_text',
|
||||
'banner_text_bottom',
|
||||
'show_dates_on_frontpage',
|
||||
@@ -609,16 +576,10 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'locale',
|
||||
'region',
|
||||
'last_order_modification_date',
|
||||
'allow_modifications_after_checkin',
|
||||
'show_quota_left',
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_hours',
|
||||
'waiting_list_auto',
|
||||
'waiting_list_names_asked',
|
||||
'waiting_list_names_required',
|
||||
'waiting_list_phones_asked',
|
||||
'waiting_list_phones_required',
|
||||
'waiting_list_phones_explanation_text',
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
@@ -629,7 +590,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'frontpage_subevent_ordering',
|
||||
'event_list_type',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
@@ -663,7 +623,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'mail_from',
|
||||
'mail_from_name',
|
||||
'mail_attach_ical',
|
||||
'mail_attach_tickets',
|
||||
'invoice_address_asked',
|
||||
'invoice_address_required',
|
||||
'invoice_address_vatid',
|
||||
@@ -695,7 +654,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_additional_text',
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'invoice_logo_image',
|
||||
'cancel_allow_user',
|
||||
'cancel_allow_user_until',
|
||||
'cancel_allow_user_paid',
|
||||
@@ -705,7 +663,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||
'cancel_allow_user_paid_adjust_fees_step',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'change_allow_user_variation',
|
||||
@@ -717,21 +674,45 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'logo_image',
|
||||
'logo_image_large',
|
||||
'logo_show_title',
|
||||
'og_image',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
self.fields[fname] = f
|
||||
|
||||
for recv, resp in api_event_settings_fields.send(sender=self.event):
|
||||
for fname, field in resp.items():
|
||||
field.required = False
|
||||
self.fields[fname] = field
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
settings_dict = self.instance.freeze()
|
||||
@@ -739,14 +720,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
validate_event_settings(self.event, settings_dict)
|
||||
return data
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
nonce = get_random_string(length=8)
|
||||
fname = '%s/%s/%s.%s.%s' % (
|
||||
self.event.organizer.slug, self.event.slug, name.split('/')[-1], nonce, name.split('.')[-1]
|
||||
)
|
||||
# TODO: make sure pub is always correct
|
||||
return 'pub/' + fname
|
||||
|
||||
|
||||
class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
default_fields = [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
@@ -28,50 +27,3 @@ class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(representation_data)
|
||||
|
||||
|
||||
class UploadedFileField(serializers.Field):
|
||||
default_error_messages = {
|
||||
'required': 'No file was submitted.',
|
||||
'not_found': 'The submitted file ID was not found.',
|
||||
'invalid_type': 'The submitted file has a file type that is not allowed in this field.',
|
||||
'size': 'The submitted file is too large to be used in this field.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.allowed_types = kwargs.pop('allowed_types', None)
|
||||
self.max_size = kwargs.pop('max_size', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
from pretix.base.models import CachedFile
|
||||
|
||||
request = self.context.get('request', None)
|
||||
try:
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}',
|
||||
file__isnull=False,
|
||||
pk=data[len("file:"):],
|
||||
)
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
self.fail('not_found')
|
||||
except CachedFile.DoesNotExist:
|
||||
self.fail('not_found')
|
||||
|
||||
if self.allowed_types and cf.type not in self.allowed_types:
|
||||
self.fail('invalid_type')
|
||||
if self.max_size and cf.file.size > self.max_size:
|
||||
self.fail('size')
|
||||
|
||||
return cf.file
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
url = value.url
|
||||
except AttributeError:
|
||||
return None
|
||||
request = self.context['request']
|
||||
return request.build_absolute_uri(url)
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
@@ -114,9 +113,6 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=10 * 1024 * 1024)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -127,7 +123,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
|
||||
read_only_fields = ('has_variations',)
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -3,7 +3,6 @@ from collections import Counter, defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
import pycountry
|
||||
from django.core.files import File
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy
|
||||
@@ -18,14 +17,13 @@ from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer, Seat,
|
||||
SubEvent, TaxRule, Voucher,
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
|
||||
)
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.services.pricing import get_price
|
||||
@@ -45,14 +43,6 @@ class CompatibleCountryField(serializers.Field):
|
||||
return instance.country_old
|
||||
|
||||
|
||||
class CountryField(serializers.Field):
|
||||
def to_internal_value(self, data):
|
||||
return {self.field_name: Country(data)}
|
||||
|
||||
def to_representation(self, src):
|
||||
return str(src) if src else None
|
||||
|
||||
|
||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
country = CompatibleCountryField(source='*')
|
||||
name = serializers.CharField(required=False)
|
||||
@@ -104,9 +94,12 @@ class AnswerQuestionIdentifierField(serializers.Field):
|
||||
|
||||
class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
if isinstance(instance, WrappedModel) or instance.pk:
|
||||
return [o.identifier for o in instance.options.all()]
|
||||
return []
|
||||
return [o.identifier for o in instance.options.all()]
|
||||
|
||||
|
||||
class AnswerQuestionOptionsField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
return [o.pk for o in instance.options.all()]
|
||||
|
||||
|
||||
class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
@@ -119,102 +112,12 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
if r['answer'].startswith('file://') and instance.orderposition:
|
||||
r['answer'] = reverse('api-v1:orderposition-answer', kwargs={
|
||||
'organizer': instance.orderposition.order.event.organizer.slug,
|
||||
'event': instance.orderposition.order.event.slug,
|
||||
'pk': instance.orderposition.pk,
|
||||
'question': instance.question_id,
|
||||
}, request=self.context['request'])
|
||||
return r
|
||||
options = AnswerQuestionOptionsField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = QuestionAnswer
|
||||
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
|
||||
|
||||
def validate_question(self, q):
|
||||
if q.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified question does not belong to this event.'
|
||||
)
|
||||
return q
|
||||
|
||||
def _handle_file_upload(self, data):
|
||||
try:
|
||||
ao = self.context["request"].user or self.context["request"].auth
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(ao))}-{ao.pk}',
|
||||
file__isnull=False,
|
||||
pk=data['answer'][len("file:"):],
|
||||
)
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
except CachedFile.DoesNotExist:
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
|
||||
allowed_types = (
|
||||
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
|
||||
)
|
||||
if cf.type not in allowed_types:
|
||||
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
|
||||
if cf.file.size > 10 * 1024 * 1024:
|
||||
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
|
||||
|
||||
data['options'] = []
|
||||
data['answer'] = cf.file
|
||||
return data
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('question').type == Question.TYPE_FILE:
|
||||
return self._handle_file_upload(data)
|
||||
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
if not data.get('options'):
|
||||
raise ValidationError(
|
||||
'You need to specify options if the question is of a choice type.'
|
||||
)
|
||||
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
|
||||
raise ValidationError(
|
||||
'You can specify at most one option for this question.'
|
||||
)
|
||||
for o in data.get('options'):
|
||||
if o.question_id != data.get('question').pk:
|
||||
raise ValidationError(
|
||||
'The specified option does not belong to this question.'
|
||||
)
|
||||
|
||||
data['answer'] = ", ".join([str(o) for o in data.get('options')])
|
||||
|
||||
else:
|
||||
if data.get('options'):
|
||||
raise ValidationError(
|
||||
'You should not specify options if the question is not of a choice type.'
|
||||
)
|
||||
|
||||
if data.get('question').type == Question.TYPE_BOOLEAN:
|
||||
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
|
||||
data['answer'] = 'True'
|
||||
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
|
||||
data['answer'] = 'False'
|
||||
else:
|
||||
raise ValidationError(
|
||||
'Please specify "true" or "false" for boolean questions.'
|
||||
)
|
||||
elif data.get('question').type == Question.TYPE_NUMBER:
|
||||
serializers.DecimalField(
|
||||
max_digits=50,
|
||||
decimal_places=25
|
||||
).to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATE:
|
||||
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_TIME:
|
||||
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATETIME:
|
||||
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
|
||||
return data
|
||||
|
||||
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
@@ -284,9 +187,6 @@ class PdfDataSerializer(serializers.Field):
|
||||
if 'vars' not in self.context:
|
||||
self.context['vars'] = get_variables(self.context['request'].event)
|
||||
|
||||
if 'vars_images' not in self.context:
|
||||
self.context['vars_images'] = get_images(self.context['request'].event)
|
||||
|
||||
for k, f in self.context['vars'].items():
|
||||
res[k] = f['evaluate'](instance, instance.order, ev)
|
||||
|
||||
@@ -301,39 +201,17 @@ class PdfDataSerializer(serializers.Field):
|
||||
for k, v in instance.item._cached_meta_data.items():
|
||||
res['itemmeta:' + k] = v
|
||||
|
||||
res['images'] = {}
|
||||
|
||||
for k, f in self.context['vars_images'].items():
|
||||
if 'etag' in f:
|
||||
has_image = etag = f['etag'](instance, instance.order, ev)
|
||||
else:
|
||||
has_image = f['etag'](instance, instance.order, ev)
|
||||
etag = None
|
||||
if has_image:
|
||||
url = reverse('api-v1:orderposition-pdf_image', kwargs={
|
||||
'organizer': instance.order.event.organizer.slug,
|
||||
'event': instance.order.event.slug,
|
||||
'pk': instance.pk,
|
||||
'key': k,
|
||||
}, request=self.context['request'])
|
||||
if etag:
|
||||
url += f'#etag={etag}'
|
||||
res['images'][k] = url
|
||||
else:
|
||||
res['images'][k] = None
|
||||
|
||||
return res
|
||||
return res
|
||||
|
||||
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
checkins = CheckinSerializer(many=True, read_only=True)
|
||||
checkins = CheckinSerializer(many=True)
|
||||
answers = AnswerSerializer(many=True)
|
||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||
downloads = PositionDownloadsField(source='*')
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
@@ -341,99 +219,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
||||
'seat', 'canceled'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||
self.fields.pop('pdf_data')
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
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
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# 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.
|
||||
update_fields = [
|
||||
'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
||||
'state', 'attendee_email',
|
||||
]
|
||||
answers_data = validated_data.pop('answers', None)
|
||||
|
||||
name = validated_data.pop('attendee_name', '')
|
||||
if name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
if attr in update_fields:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save(update_fields=update_fields)
|
||||
|
||||
if answers_data is not None:
|
||||
qs_seen = set()
|
||||
answercache = {
|
||||
a.question_id: a for a in instance.answers.all()
|
||||
}
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
if answ_data['question'].pk in qs_seen:
|
||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||
if answ_data['question'].pk in answercache:
|
||||
a = answercache[answ_data['question'].pk]
|
||||
if isinstance(answ_data['answer'], File):
|
||||
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
else:
|
||||
for attr, value in answ_data.items():
|
||||
setattr(a, attr, value)
|
||||
a.save()
|
||||
else:
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
a = instance.answers.create(**answ_data, answer='')
|
||||
a.file.save(an.name, an, save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
a.save()
|
||||
else:
|
||||
a = instance.answers.create(**answ_data)
|
||||
a.options.set(options)
|
||||
qs_seen.add(a.question_id)
|
||||
for qid, a in answercache.items():
|
||||
if qid not in qs_seen:
|
||||
a.delete()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class RequireAttentionField(serializers.Field):
|
||||
def to_representation(self, instance: OrderPosition):
|
||||
@@ -545,7 +336,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderRefund
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'comment', 'provider')
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
||||
|
||||
|
||||
class OrderURLField(serializers.URLField):
|
||||
@@ -634,17 +425,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
return instance
|
||||
|
||||
|
||||
class AnswerQuestionOptionsField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
return [o.pk for o in instance.options.all()]
|
||||
|
||||
|
||||
class SimulatedAnswerSerializer(AnswerSerializer):
|
||||
options = AnswerQuestionOptionsField(read_only=True, source='*')
|
||||
|
||||
|
||||
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
|
||||
answers = SimulatedAnswerSerializer(many=True)
|
||||
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
|
||||
|
||||
|
||||
@@ -671,8 +452,62 @@ class PriceCalcSerializer(serializers.Serializer):
|
||||
del self.fields['subevent']
|
||||
|
||||
|
||||
class AnswerCreateSerializer(AnswerSerializer):
|
||||
pass
|
||||
class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = QuestionAnswer
|
||||
fields = ('question', 'answer', 'options')
|
||||
|
||||
def validate_question(self, q):
|
||||
if q.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified question does not belong to this event.'
|
||||
)
|
||||
return q
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('question').type == Question.TYPE_FILE:
|
||||
raise ValidationError(
|
||||
'File uploads are currently not supported via the API.'
|
||||
)
|
||||
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
if not data.get('options'):
|
||||
raise ValidationError(
|
||||
'You need to specify options if the question is of a choice type.'
|
||||
)
|
||||
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
|
||||
raise ValidationError(
|
||||
'You can specify at most one option for this question.'
|
||||
)
|
||||
data['answer'] = ", ".join([str(o) for o in data.get('options')])
|
||||
|
||||
else:
|
||||
if data.get('options'):
|
||||
raise ValidationError(
|
||||
'You should not specify options if the question is not of a choice type.'
|
||||
)
|
||||
|
||||
if data.get('question').type == Question.TYPE_BOOLEAN:
|
||||
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
|
||||
data['answer'] = 'True'
|
||||
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
|
||||
data['answer'] = 'False'
|
||||
else:
|
||||
raise ValidationError(
|
||||
'Please specify "true" or "false" for boolean questions.'
|
||||
)
|
||||
elif data.get('question').type == Question.TYPE_NUMBER:
|
||||
serializers.DecimalField(
|
||||
max_digits=50,
|
||||
decimal_places=25
|
||||
).to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATE:
|
||||
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_TIME:
|
||||
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATETIME:
|
||||
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
|
||||
return data
|
||||
|
||||
|
||||
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||
@@ -1209,16 +1044,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
pos.save()
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
answ = pos.answers.create(**answ_data, answer='')
|
||||
answ.file.save(an.name, an, save=False)
|
||||
answ.answer = 'file://' + answ.file.name
|
||||
answ.save()
|
||||
else:
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
pos_map[pos.positionid] = pos
|
||||
|
||||
if not simulate:
|
||||
@@ -1330,24 +1157,17 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
refers = serializers.SlugRelatedField(slug_field='full_invoice_no', read_only=True)
|
||||
refers = serializers.SlugRelatedField(slug_field='invoice_no', read_only=True)
|
||||
lines = InlineInvoiceLineSerializer(many=True)
|
||||
invoice_to_country = CountryField()
|
||||
invoice_from_country = CountryField()
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
||||
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
||||
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
|
||||
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
|
||||
'custom_field', 'date', 'refers', 'locale',
|
||||
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
|
||||
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
||||
'internal_reference')
|
||||
@@ -1374,7 +1194,7 @@ class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderRefund
|
||||
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info', 'comment')
|
||||
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info')
|
||||
|
||||
def create(self, validated_data):
|
||||
pid = validated_data.pop('payment', None)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
@@ -18,11 +16,9 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.settings import validate_organizer_settings
|
||||
from pretix.base.settings import DEFAULTS, validate_organizer_settings
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
@@ -211,10 +207,8 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
class OrganizerSettingsSerializer(serializers.Serializer):
|
||||
default_fields = [
|
||||
'contact_mail',
|
||||
'imprint_url',
|
||||
'organizer_info_text',
|
||||
'event_list_type',
|
||||
'event_list_availability',
|
||||
@@ -231,13 +225,40 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
'theme_color_danger',
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'organizer_logo_image'
|
||||
'primary_font'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
self.fields[fname] = f
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -245,11 +266,3 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
settings_dict.update(data)
|
||||
validate_organizer_settings(self.organizer, settings_dict)
|
||||
return data
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
nonce = get_random_string(length=8)
|
||||
fname = '%s/%s.%s.%s' % (
|
||||
self.organizer.slug, name.split('/')[-1], nonce, name.split('.')[-1]
|
||||
)
|
||||
# TODO: make sure pub is always correct
|
||||
return 'pub/' + fname
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import logging
|
||||
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models.fields.files import FieldFile
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.base.settings import DEFAULTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsSerializer(serializers.Serializer):
|
||||
default_fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
f.parent = self
|
||||
self.fields[fname] = f
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if isinstance(value, FieldFile):
|
||||
# Delete old file
|
||||
fname = instance.get(attr, as_type=File)
|
||||
if fname:
|
||||
try:
|
||||
default_storage.delete(fname.name)
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
|
||||
# Create new file
|
||||
newname = default_storage.save(self.get_new_filename(value.name), value)
|
||||
instance.set(attr, File(file=value, name=newname))
|
||||
self.changed_data.append(attr)
|
||||
elif isinstance(self.fields[attr], UploadedFileField):
|
||||
if value is None:
|
||||
fname = instance.get(attr, as_type=File)
|
||||
if fname:
|
||||
try:
|
||||
default_storage.delete(fname.name)
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
instance.delete(attr)
|
||||
else:
|
||||
# file is unchanged
|
||||
continue
|
||||
elif value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
raise NotImplementedError()
|
||||
@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ('id', 'created', 'name', 'name_parts', 'email', 'phone', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
|
||||
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
|
||||
read_only_fields = ('id', 'created', 'voucher')
|
||||
|
||||
def validate(self, data):
|
||||
@@ -32,11 +32,4 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
||||
if availability[0] == 100:
|
||||
raise ValidationError("This product is currently available.")
|
||||
|
||||
if data.get('name') and data.get('name_parts'):
|
||||
raise ValidationError(
|
||||
{'name': ['Do not specify name if you specified name_parts.']}
|
||||
)
|
||||
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
||||
data['name_parts']['_scheme'] = event.settings.name_scheme
|
||||
|
||||
return data
|
||||
|
||||
@@ -8,7 +8,9 @@ from pretix.api.models import ApiCall, WebHookCall
|
||||
from pretix.base.signals import periodic_task
|
||||
from pretix.helpers.periodic import minimum_interval
|
||||
|
||||
register_webhook_events = Signal()
|
||||
register_webhook_events = Signal(
|
||||
providing_args=[]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to get all known webhook events. Receivers should return an
|
||||
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import importlib
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf.urls import include, re_path
|
||||
from django.conf.urls import include, url
|
||||
from rest_framework import routers
|
||||
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, upload,
|
||||
user, version, voucher, waitinglist, webhooks,
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, user,
|
||||
version, voucher, waitinglist, webhooks,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -72,32 +72,29 @@ for app in apps.get_app_configs():
|
||||
importlib.import_module(app.name + '.urls')
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^', include(router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
||||
name="organizer.settings"),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||
name="event.settings"),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/',
|
||||
include(item_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||
include(question_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
||||
include(checkinlist_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/orders/(?P<order>[^/]+)/',
|
||||
include(order_router.urls)),
|
||||
re_path(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"),
|
||||
re_path(r"^oauth/token$", oauth.TokenView.as_view(), name="token"),
|
||||
re_path(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"),
|
||||
re_path(r"^device/initialize$", device.InitializeView.as_view(), name="device.initialize"),
|
||||
re_path(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
||||
re_path(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||
re_path(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
re_path(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||
re_path(r"^upload$", upload.UploadView.as_view(), name="upload"),
|
||||
re_path(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||
re_path(r"^version$", version.VersionView.as_view(), name="version"),
|
||||
url(r'^', include(router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/', include(orga_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
||||
name="organizer.settings"),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||
name="event.settings"),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||
include(question_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
|
||||
include(checkinlist_router.urls)),
|
||||
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/orders/(?P<order>[^/]+)/', include(order_router.urls)),
|
||||
url(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"),
|
||||
url(r"^oauth/token$", oauth.TokenView.as_view(), name="token"),
|
||||
url(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"),
|
||||
url(r"^device/initialize$", device.InitializeView.as_view(), name="device.initialize"),
|
||||
url(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
||||
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
url(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||
url(r"^version$", version.VersionView.as_view(), name="version"),
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@ from pretix.api.views import RichOrderingFilter
|
||||
from pretix.api.views.order import OrderPositionFilter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, CheckinList, Event, Order, OrderPosition, Question,
|
||||
Checkin, CheckinList, Event, Order, OrderPosition,
|
||||
)
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, perform_checkin,
|
||||
@@ -302,10 +302,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
for q in op.item.questions.filter(ask_during_checkin=True):
|
||||
if str(q.pk) in aws:
|
||||
try:
|
||||
if q.type == Question.TYPE_FILE:
|
||||
given_answers[q] = self._handle_file_upload(aws[str(q.pk)])
|
||||
else:
|
||||
given_answers[q] = q.clean_answer(aws[str(q.pk)])
|
||||
given_answers[q] = q.clean_answer(aws[str(q.pk)])
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
@@ -355,25 +352,3 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=201)
|
||||
|
||||
def _handle_file_upload(self, data):
|
||||
try:
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}',
|
||||
file__isnull=False,
|
||||
pk=data[len("file:"):],
|
||||
)
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
except CachedFile.DoesNotExist:
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
|
||||
allowed_types = (
|
||||
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
|
||||
)
|
||||
if cf.type not in allowed_types:
|
||||
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
|
||||
if cf.file.size > 10 * 1024 * 1024:
|
||||
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
|
||||
|
||||
return cf.file
|
||||
|
||||
@@ -365,13 +365,9 @@ class EventSettingsView(views.APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if isinstance(request.auth, Device):
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
elif 'can_change_event_settings' in request.eventpermset:
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
if 'explain' in request.GET:
|
||||
@@ -386,7 +382,7 @@ class EventSettingsView(views.APIView):
|
||||
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
||||
event=request.event, context={'request': request})
|
||||
event=request.event)
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
s.save()
|
||||
@@ -396,9 +392,6 @@ class EventSettingsView(views.APIView):
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_css.apply_async(args=(request.event.pk,))
|
||||
s = EventSettingsSerializer(
|
||||
instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
regenerate_css.apply_async(args=(request.organizer.pk,))
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
return Response(s.data)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import datetime
|
||||
import mimetypes
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
import django_filters
|
||||
@@ -14,7 +12,6 @@ from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from PIL import Image
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import (
|
||||
@@ -38,9 +35,8 @@ from pretix.base.models import (
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
|
||||
TaxRule, TeamAPIToken, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
|
||||
from pretix.base.models.orders import RevokedTicketSecret
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.pdf import get_images
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import (
|
||||
@@ -767,7 +763,7 @@ with scopes_disabled():
|
||||
}
|
||||
|
||||
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
@@ -787,11 +783,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
},
|
||||
}
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
qs = OrderPosition.all
|
||||
@@ -917,62 +908,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
'tax_rule': tr.pk if tr else None,
|
||||
})
|
||||
|
||||
@action(detail=True, url_name='answer', url_path=r'answer/(?P<question>\d+)')
|
||||
def answer(self, request, **kwargs):
|
||||
pos = self.get_object()
|
||||
answer = get_object_or_404(
|
||||
QuestionAnswer,
|
||||
orderposition=self.get_object(),
|
||||
question_id=kwargs.get('question')
|
||||
)
|
||||
if not answer.file:
|
||||
raise NotFound()
|
||||
|
||||
ftype, ignored = mimetypes.guess_type(answer.file.name)
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
|
||||
def pdf_image(self, request, key, **kwargs):
|
||||
pos = self.get_object()
|
||||
|
||||
image_vars = get_images(request.event)
|
||||
if key not in image_vars:
|
||||
raise NotFound('Unknown key')
|
||||
|
||||
image_file = image_vars[key]['evaluate'](pos, pos.order, pos.subevent or self.request.event)
|
||||
if image_file is None:
|
||||
raise NotFound('No image available')
|
||||
|
||||
if getattr(image_file, 'name', ''):
|
||||
ftype, ignored = mimetypes.guess_type(image_file.name)
|
||||
extension = os.path.basename(image_file.name).split('.')[-1]
|
||||
else:
|
||||
img = Image.open(image_file)
|
||||
ftype = Image.MIME[img.format]
|
||||
extensions = {
|
||||
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
|
||||
}
|
||||
extension = extensions.get(img.format, 'bin')
|
||||
if hasattr(image_file, 'seek'):
|
||||
image_file.seek(0)
|
||||
|
||||
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
key,
|
||||
extension,
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
def download(self, request, output, **kwargs):
|
||||
provider = self._get_output_provider(output)
|
||||
@@ -1016,44 +951,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.get('partial', False)
|
||||
if not partial:
|
||||
return Response(
|
||||
{"detail": "Method \"PUT\" not allowed."},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
with transaction.atomic():
|
||||
old_data = self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data
|
||||
serializer.save()
|
||||
new_data = serializer.data
|
||||
|
||||
if old_data != new_data:
|
||||
log_data = self.request.data
|
||||
if 'answers' in log_data:
|
||||
for a in new_data['answers']:
|
||||
log_data[f'question_{a["question"]}'] = a["answer"]
|
||||
log_data.pop('answers', None)
|
||||
serializer.instance.order.log_action(
|
||||
'pretix.event.order.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'data': [
|
||||
dict(
|
||||
position=serializer.instance.pk,
|
||||
**log_data
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
||||
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
||||
|
||||
|
||||
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPaymentSerializer
|
||||
|
||||
@@ -425,9 +425,7 @@ class OrganizerSettingsView(views.APIView):
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
|
||||
if 'explain' in request.GET:
|
||||
return Response({
|
||||
fname: {
|
||||
@@ -441,9 +439,7 @@ class OrganizerSettingsView(views.APIView):
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = OrganizerSettingsSerializer(
|
||||
instance=request.organizer.settings, data=request.data, partial=True,
|
||||
organizer=request.organizer, context={
|
||||
'request': request
|
||||
}
|
||||
organizer=request.organizer
|
||||
)
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
@@ -455,7 +451,5 @@ class OrganizerSettingsView(views.APIView):
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
|
||||
return Response(s.data)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from django.utils.timezone import now
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
|
||||
from pretix.api.auth.token import TeamTokenAuthentication
|
||||
from pretix.base.models import CachedFile
|
||||
|
||||
ALLOWED_TYPES = {
|
||||
'image/gif': {'.gif'},
|
||||
'image/jpeg': {'.jpg', '.jpeg'},
|
||||
'image/png': {'.png'},
|
||||
'application/pdf': {'.pdf'},
|
||||
}
|
||||
|
||||
|
||||
class UploadView(APIView):
|
||||
authentication_classes = (
|
||||
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
|
||||
)
|
||||
parser_classes = [FileUploadParser]
|
||||
permission_classes = [AnyAuthenticatedClientPermission]
|
||||
|
||||
def post(self, request):
|
||||
if 'file' not in request.data:
|
||||
raise ValidationError('No file has been submitted.')
|
||||
file_obj = request.data['file']
|
||||
content_type = file_obj.content_type.split(";")[0] # ignore e.g. "; charset=…"
|
||||
if content_type not in ALLOWED_TYPES:
|
||||
raise ValidationError('Content type "{type}" is not allowed'.format(type=content_type))
|
||||
if not any(file_obj.name.endswith(ext) for ext in ALLOWED_TYPES[content_type]):
|
||||
raise ValidationError('File name "{name}" has an invalid extension for type "{type}"'.format(
|
||||
name=file_obj.name,
|
||||
type=content_type
|
||||
))
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
date=now(),
|
||||
web_download=False,
|
||||
filename=file_obj.name,
|
||||
type=content_type,
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}'
|
||||
)
|
||||
cf.file.save(file_obj.name, file_obj)
|
||||
cf.save()
|
||||
return Response({
|
||||
'id': f'file:{cf.pk}'
|
||||
}, status=201)
|
||||
@@ -6,7 +6,6 @@ from rest_framework.views import APIView
|
||||
|
||||
from pretix import __version__
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
|
||||
from pretix.api.auth.token import TeamTokenAuthentication
|
||||
|
||||
|
||||
@@ -49,7 +48,6 @@ class VersionView(APIView):
|
||||
authentication_classes = (
|
||||
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
|
||||
)
|
||||
permission_classes = [AnyAuthenticatedClientPermission]
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
|
||||
@@ -2,12 +2,10 @@ import inspect
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.timezone import now
|
||||
@@ -130,21 +128,9 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
|
||||
if order:
|
||||
htmlctx['order'] = order
|
||||
positions = list(order.positions.select_related(
|
||||
'item', 'variation', 'subevent', 'addon_to'
|
||||
).annotate(
|
||||
has_addons=Count('addons')
|
||||
))
|
||||
htmlctx['cart'] = [(k, list(v)) for k, v in groupby(
|
||||
positions, key=lambda op: (
|
||||
op.item, op.variation, op.subevent, op.attendee_name,
|
||||
(op.pk if op.addon_to_id else None), (op.pk if op.has_addons else None)
|
||||
)
|
||||
)]
|
||||
|
||||
if position:
|
||||
htmlctx['position'] = position
|
||||
htmlctx['ev'] = position.subevent or self.event
|
||||
|
||||
tpl = get_template(self.template_name)
|
||||
body_html = inline_css(tpl.render(htmlctx))
|
||||
@@ -251,8 +237,6 @@ def get_email_context(**kwargs):
|
||||
from pretix.base.models import InvoiceAddress
|
||||
|
||||
event = kwargs['event']
|
||||
if 'position' in kwargs:
|
||||
kwargs.setdefault("position_or_address", kwargs['position'])
|
||||
if 'order' in kwargs:
|
||||
try:
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
@@ -443,30 +427,28 @@ def base_placeholders(sender, **kwargs):
|
||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
order.full_code,
|
||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash(),
|
||||
'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.open', kwargs={
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order['code'],
|
||||
'secret': order['secret'],
|
||||
'hash': order['hash'],
|
||||
'secret': order['secret']
|
||||
}),
|
||||
)
|
||||
for order in [
|
||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
|
||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
|
||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
|
||||
]
|
||||
),
|
||||
),
|
||||
@@ -484,8 +466,7 @@ def base_placeholders(sender, **kwargs):
|
||||
'68CYU2H6ZTP3WLK5'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
|
||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import io
|
||||
import re
|
||||
import tempfile
|
||||
from collections import OrderedDict, namedtuple
|
||||
from decimal import Decimal
|
||||
@@ -11,21 +10,11 @@ from django.db.models import QuerySet
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES
|
||||
from openpyxl.cell.cell import KNOWN_TYPES
|
||||
|
||||
from pretix.base.models import Event
|
||||
|
||||
|
||||
def excel_safe(val):
|
||||
if not isinstance(val, KNOWN_TYPES):
|
||||
val = str(val)
|
||||
|
||||
if isinstance(val, str):
|
||||
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
|
||||
|
||||
return val
|
||||
|
||||
|
||||
class BaseExporter:
|
||||
"""
|
||||
This is the base class for all data exporters
|
||||
@@ -192,7 +181,7 @@ class ListExporter(BaseExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
excel_safe(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||
str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||
for val in line
|
||||
])
|
||||
if total:
|
||||
@@ -253,10 +242,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
pass
|
||||
|
||||
def iterate_sheet(self, form_data, sheet):
|
||||
if hasattr(self, 'iterate_' + sheet):
|
||||
yield from getattr(self, 'iterate_' + sheet)(form_data)
|
||||
else:
|
||||
raise NotImplementedError() # noqa
|
||||
raise NotImplementedError() # noqa
|
||||
|
||||
def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
|
||||
total = 0
|
||||
@@ -302,9 +288,6 @@ class MultiSheetListExporter(ListExporter):
|
||||
n_sheets = len(self.sheets)
|
||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||
ws = wb.create_sheet(str(l))
|
||||
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
||||
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
||||
|
||||
total = 0
|
||||
counter = 0
|
||||
for i, line in enumerate(self.iterate_sheet(form_data, sheet=s)):
|
||||
@@ -312,7 +295,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
excel_safe(val)
|
||||
str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||
for val in line
|
||||
])
|
||||
if total:
|
||||
|
||||
@@ -18,7 +18,6 @@ from ...control.forms.filter import get_all_payment_providers
|
||||
from ...helpers import GroupConcat
|
||||
from ...helpers.iter import chunked_iterable
|
||||
from ..exporter import BaseExporter, MultiSheetListExporter
|
||||
from ..services.export import ExportError
|
||||
from ..services.invoices import invoice_pdf_task
|
||||
from ..signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
@@ -67,7 +66,7 @@ class InvoiceExporterMixin:
|
||||
)
|
||||
|
||||
def invoices_queryset(self, form_data: dict):
|
||||
qs = Invoice.objects.filter(event__in=self.events).select_related('order')
|
||||
qs = Invoice.objects.filter(event__in=self.events)
|
||||
|
||||
if form_data.get('payment_provider'):
|
||||
qs = qs.annotate(
|
||||
@@ -112,16 +111,14 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
|
||||
if not i.file:
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
if not i.file:
|
||||
raise ExportError('Could not generate PDF for invoice {nr}'.format(nr=i.full_invoice_no))
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
|
||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||
i.file.close()
|
||||
except FileNotFoundError:
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
|
||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||
i.file.close()
|
||||
counter += 1
|
||||
if total and counter % max(10, total // 100) == 0:
|
||||
|
||||
@@ -59,12 +59,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
initial=False,
|
||||
required=False
|
||||
)),
|
||||
('group_multiple_choice',
|
||||
forms.BooleanField(
|
||||
label=_('Show multiple choice answers grouped in one column'),
|
||||
initial=False,
|
||||
required=False
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -455,14 +449,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for q in questions:
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
options[q.pk] = []
|
||||
if form_data['group_multiple_choice']:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
headers.append(str(q.question))
|
||||
else:
|
||||
for o in q.options.all():
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
for o in q.options.all():
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
else:
|
||||
headers.append(str(q.question))
|
||||
headers += [
|
||||
@@ -562,11 +551,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
acache[a.question_id] = str(a)
|
||||
for q in questions:
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
if form_data['group_multiple_choice']:
|
||||
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
|
||||
else:
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
else:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
|
||||
@@ -652,7 +638,7 @@ class PaymentListExporter(ListExporter):
|
||||
|
||||
headers = [
|
||||
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
|
||||
_('Status code'), _('Amount'), _('Payment method'), _('Comment')
|
||||
_('Status code'), _('Amount'), _('Payment method')
|
||||
]
|
||||
yield headers
|
||||
|
||||
@@ -674,8 +660,7 @@ class PaymentListExporter(ListExporter):
|
||||
obj.get_state_display(),
|
||||
obj.state,
|
||||
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||
provider_names.get(obj.provider, obj.provider),
|
||||
obj.comment if isinstance(obj, OrderRefund) else "",
|
||||
provider_names.get(obj.provider, obj.provider)
|
||||
]
|
||||
yield row
|
||||
|
||||
@@ -691,18 +676,13 @@ class QuotaListExporter(ListExporter):
|
||||
verbose_name = gettext_lazy('Quota availabilities')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
has_subevents = self.event.has_subevents
|
||||
headers = [
|
||||
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
|
||||
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
|
||||
]
|
||||
if has_subevents:
|
||||
headers.append(pgettext('subevent', 'Date'))
|
||||
headers.append(_('Start date'))
|
||||
headers.append(_('End date'))
|
||||
yield headers
|
||||
|
||||
quotas = list(self.event.quotas.select_related('subevent'))
|
||||
quotas = list(self.event.quotas.all())
|
||||
qa = QuotaAvailability(full_results=True)
|
||||
qa.queue(*quotas)
|
||||
qa.compute()
|
||||
@@ -720,18 +700,6 @@ class QuotaListExporter(ListExporter):
|
||||
qa.count_exited_orders[quota],
|
||||
_('Infinite') if avail[1] is None else avail[1]
|
||||
]
|
||||
if has_subevents:
|
||||
if quota.subevent:
|
||||
row.append(quota.subevent.name)
|
||||
row.append(quota.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
if quota.subevent.date_to:
|
||||
row.append(quota.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
else:
|
||||
row.append('')
|
||||
else:
|
||||
row.append('')
|
||||
row.append('')
|
||||
row.append('')
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
|
||||
@@ -82,9 +82,7 @@ class WaitingListExporter(ListExporter):
|
||||
|
||||
headers = [
|
||||
_('Date'),
|
||||
_('Name'),
|
||||
_('Email'),
|
||||
_('Phone number'),
|
||||
_('Product name'),
|
||||
_('Variation'),
|
||||
_('Event slug'),
|
||||
@@ -119,9 +117,7 @@ class WaitingListExporter(ListExporter):
|
||||
|
||||
row = [
|
||||
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
|
||||
entry.name,
|
||||
entry.email,
|
||||
entry.phone,
|
||||
str(entry.item) if entry.item else "",
|
||||
str(entry.variation) if entry.variation else "",
|
||||
entry.event.slug,
|
||||
|
||||
@@ -9,14 +9,12 @@ import pycountry
|
||||
import pytz
|
||||
import vat_moss.errors
|
||||
import vat_moss.id
|
||||
from babel import Locale
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Select
|
||||
from django.utils import translation
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -26,7 +24,9 @@ from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from phonenumber_field.widgets import (
|
||||
PhoneNumberPrefixWidget, PhonePrefixSelect,
|
||||
)
|
||||
from phonenumbers import NumberParseException, national_significant_number
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
|
||||
@@ -205,39 +205,10 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
return value
|
||||
|
||||
|
||||
class WrappedPhonePrefixSelect(Select):
|
||||
initial = None
|
||||
|
||||
def __init__(self, initial=None):
|
||||
choices = [("", "---------")]
|
||||
language = get_babel_locale() # changed from default implementation that used the django locale
|
||||
locale = Locale(translation.to_locale(language))
|
||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
prefix = "+%d" % prefix
|
||||
if initial and initial in values:
|
||||
self.initial = prefix
|
||||
for country_code in values:
|
||||
country_name = locale.territories.get(country_code)
|
||||
if country_name:
|
||||
choices.append((prefix, "{} {}".format(country_name, prefix)))
|
||||
super().__init__(choices=sorted(choices, key=lambda item: item[1]))
|
||||
|
||||
def render(self, name, value, *args, **kwargs):
|
||||
return super().render(name, value or self.initial, *args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
if value and self.choices[1][0] != value:
|
||||
matching_choices = len([1 for p, c in self.choices if p == value])
|
||||
if matching_choices > 1:
|
||||
# Some countries share a phone prefix, for example +1 is used all over the Americas.
|
||||
# This causes a UX problem: If the default value or the existing data is +12125552368,
|
||||
# the widget will just show the first <option> entry with value="+1" as selected,
|
||||
# which alphabetically is America Samoa, although most numbers statistically are from
|
||||
# the US. As a workaround, we detect this case and add an aditional choice value with
|
||||
# just <option value="+1">+1</option> without an explicit country.
|
||||
self.choices.insert(1, (value, value))
|
||||
context = super().get_context(name, value, attrs)
|
||||
return context
|
||||
class WrappedPhonePrefixSelect(PhonePrefixSelect):
|
||||
def __init__(self, *args, **kwargs):
|
||||
with language(get_babel_locale()):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
@@ -444,7 +415,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
|
||||
cc = None
|
||||
state = None
|
||||
if fprefix + 'country' in self.data:
|
||||
cc = str(self.data[fprefix + 'country'])
|
||||
elif country:
|
||||
@@ -453,7 +423,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
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])
|
||||
state = (cartpos.state if cartpos else orderpos.state)
|
||||
elif fprefix + 'state' in self.data:
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'state']
|
||||
@@ -462,7 +431,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
initial=state,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
@@ -693,9 +661,8 @@ class BaseQuestionsForm(forms.Form):
|
||||
if not self.all_optional:
|
||||
for q in question_cache.values():
|
||||
answer = d.get('question_%d' % q.pk)
|
||||
field = self['question_%d' % q.pk]
|
||||
if question_is_required(q) and not answer and answer != 0 and not field.errors:
|
||||
raise ValidationError({'question_%d' % q.pk: [_('This field is required.')]})
|
||||
if question_is_required(q) and not answer and answer != 0:
|
||||
raise ValidationError({'question_%d' % q.pk: [_('This field is required')]})
|
||||
|
||||
return d
|
||||
|
||||
@@ -851,13 +818,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
) 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 data.get('vat_id') and is_eu_country(data.get('country')) and data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
|
||||
raise ValidationError(_('Your VAT ID does not match the selected country.'))
|
||||
|
||||
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||
pass
|
||||
elif self.validate_vat_id and data.get('is_business') and is_eu_country(data.get('country')) and data.get('vat_id'):
|
||||
if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
|
||||
raise ValidationError(_('Your VAT ID does not match the selected country.'))
|
||||
try:
|
||||
result = vat_moss.id.validate(data.get('vat_id'))
|
||||
if result:
|
||||
|
||||
@@ -16,15 +16,12 @@ class DatePickerWidget(forms.DateInput):
|
||||
date_attrs = dict(attrs)
|
||||
date_attrs.setdefault('class', 'form-control')
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
date_attrs['autocomplete'] = 'off'
|
||||
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
|
||||
|
||||
def placeholder():
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
return now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
|
||||
date_attrs['placeholder'] = lazy(placeholder, str)
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
date_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
|
||||
forms.DateInput.__init__(self, date_attrs, date_format)
|
||||
|
||||
@@ -37,15 +34,12 @@ class TimePickerWidget(forms.TimeInput):
|
||||
time_attrs = dict(attrs)
|
||||
time_attrs.setdefault('class', 'form-control')
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
time_attrs['autocomplete'] = 'off'
|
||||
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
|
||||
|
||||
def placeholder():
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
return now().replace(
|
||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
time_attrs['placeholder'] = lazy(placeholder, str)
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
time_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
forms.TimeInput.__init__(self, time_attrs, time_format)
|
||||
|
||||
@@ -111,8 +105,8 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
time_attrs.setdefault('autocomplete', 'off')
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
date_attrs['autocomplete'] = 'off'
|
||||
time_attrs['autocomplete'] = 'off'
|
||||
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
|
||||
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
|
||||
if min_date:
|
||||
date_attrs['data-min'] = (
|
||||
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
|
||||
|
||||
@@ -12,10 +12,12 @@ from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import (
|
||||
get_language, gettext, gettext_lazy, pgettext,
|
||||
)
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
|
||||
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
@@ -29,7 +31,6 @@ from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Invoice, Order
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -220,6 +221,26 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||
|
||||
|
||||
class ThumbnailingImageReader(ImageReader):
|
||||
def resize(self, width, height, dpi):
|
||||
if width is None:
|
||||
width = height * self._image.size[0] / self._image.size[1]
|
||||
if height is None:
|
||||
height = width * self._image.size[1] / self._image.size[0]
|
||||
self._image.thumbnail(
|
||||
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||
resample=BICUBIC
|
||||
)
|
||||
self._data = None
|
||||
return width, height
|
||||
|
||||
def _jpeg_fh(self):
|
||||
# Bypass a reportlab-internal optimization that falls back to the original
|
||||
# file handle if the file is a JPEG, and therefore does not respect the
|
||||
# (smaller) size of the modified image.
|
||||
return None
|
||||
|
||||
|
||||
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
identifier = 'classic'
|
||||
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
|
||||
@@ -255,10 +276,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_from_top = 17 * mm
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
p = Paragraph(
|
||||
bleach.clean(self.invoice.full_invoice_from, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
style=self.stylesheet['InvoiceFrom']
|
||||
)
|
||||
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
|
||||
'InvoiceFrom'])
|
||||
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
||||
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
|
||||
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
|
||||
@@ -363,7 +382,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _draw_event(self, canvas):
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
txt = bleach.clean(txt, tags=[]).strip()
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
|
||||
@@ -444,18 +462,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story = []
|
||||
if self.invoice.custom_field:
|
||||
story.append(Paragraph(
|
||||
'{}: {}'.format(
|
||||
bleach.clean(self.invoice.event.settings.invoice_address_custom_field, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
),
|
||||
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.internal_reference:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
||||
reference=bleach.clean(self.invoice.internal_reference, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
),
|
||||
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
@@ -474,10 +487,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
))
|
||||
|
||||
if self.invoice.introductory_text:
|
||||
story.append(Paragraph(
|
||||
self.invoice.introductory_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
return story
|
||||
@@ -577,16 +587,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(Spacer(1, 15 * mm))
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(Paragraph(
|
||||
self.invoice.payment_provider_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
|
||||
|
||||
if self.invoice.additional_text:
|
||||
story.append(Paragraph(
|
||||
self.invoice.additional_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
|
||||
story.append(Spacer(1, 15 * mm))
|
||||
|
||||
tstyledata = [
|
||||
@@ -718,10 +722,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
def _draw_invoice_from(self, canvas):
|
||||
if not self.invoice.invoice_from:
|
||||
return
|
||||
c = [
|
||||
bleach.clean(l, tags=[]).strip().replace('\n', '<br />\n')
|
||||
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||
]
|
||||
c = self.invoice.address_invoice_from.strip().split('\n')
|
||||
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||
|
||||
@@ -16,8 +16,6 @@ class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
||||
'(dotted path, comma separation)')
|
||||
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
|
||||
'(dotted path, comma separation)')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbosity = int(options['verbosity'])
|
||||
@@ -30,12 +28,9 @@ class Command(BaseCommand):
|
||||
if options.get('tasks'):
|
||||
if name not in options.get('tasks').split(','):
|
||||
continue
|
||||
if options.get('exclude'):
|
||||
if name in options.get('exclude').split(','):
|
||||
continue
|
||||
|
||||
if verbosity > 1:
|
||||
self.stdout.write(f'INFO Running {name}…')
|
||||
self.stdout.write(f'Running {name}…')
|
||||
t0 = time.time()
|
||||
try:
|
||||
r = receiver(signal=periodic_task, sender=self)
|
||||
@@ -45,13 +40,13 @@ class Command(BaseCommand):
|
||||
if settings.SENTRY_ENABLED:
|
||||
from sentry_sdk import capture_exception
|
||||
capture_exception(err)
|
||||
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
|
||||
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
|
||||
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
|
||||
traceback.print_exc()
|
||||
else:
|
||||
if options.get('verbosity') > 1:
|
||||
if r is SKIPPED:
|
||||
self.stdout.write(self.style.SUCCESS(f'INFO Skipped {name}'))
|
||||
self.stdout.write(self.style.SUCCESS(f'Skipped {name}'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f'INFO Completed {name} in {round(time.time() - t0, 3)}s'))
|
||||
self.stdout.write(self.style.SUCCESS(f'Completed {name} in {round(time.time() - t0, 3)}s'))
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from django.apps import apps
|
||||
@@ -8,7 +6,6 @@ from django.conf import settings
|
||||
from django.db import connection
|
||||
|
||||
from pretix.base.models import Event, Invoice, Order, OrderPosition, Organizer
|
||||
from pretix.celery_app import app
|
||||
|
||||
if settings.HAS_REDIS:
|
||||
import django_redis
|
||||
@@ -251,19 +248,6 @@ def metric_values():
|
||||
else:
|
||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
||||
|
||||
if settings.HAS_CELERY:
|
||||
client = app.broker_connection().channel().client
|
||||
for q in settings.CELERY_TASK_QUEUES:
|
||||
llen = client.llen(q.name)
|
||||
lfirst = client.lindex(q.name, -1)
|
||||
metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = llen
|
||||
if lfirst:
|
||||
ldata = json.loads(lfirst)
|
||||
dt = time.time() - ldata.get('created', 0)
|
||||
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = dt
|
||||
else:
|
||||
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = 0
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.1 on 2018-10-17 00:24
|
||||
|
||||
|
||||
import jsonfallback.fields
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import migrations
|
||||
from django_mysql.checks import mysql_connections
|
||||
@@ -77,19 +77,19 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='attendee_name_parts',
|
||||
field=models.JSONField(null=False, default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='attendee_name_parts',
|
||||
field=models.JSONField(null=False, default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(null=False, default=dict),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(set_attendee_name_parts, migrations.RunPython.noop)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.1 on 2018-11-21 12:24
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.db.models.manager
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1 on 2019-01-12 15:12
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.5 on 2019-01-29 13:37
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.5 on 2019-02-01 15:27
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import migrations, models
|
||||
@@ -189,7 +190,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='attendee_name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
@@ -209,7 +210,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='invoiceaddress',
|
||||
name='name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
@@ -224,7 +225,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='attendee_name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1 on 2019-02-08 14:32
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.5 on 2019-02-19 12:45
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.5 on 2019-02-19 09:49
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-12 09:42
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-16 10:14
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.1.5 on 2019-04-02 07:22
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Generated by Django 2.2 on 2019-04-23 08:39
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# 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
|
||||
@@ -16,7 +17,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='attendee_name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
@@ -36,7 +37,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='invoiceaddress',
|
||||
name='name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
@@ -51,7 +52,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='attendee_name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_countries.fields
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.helpers.countries
|
||||
@@ -42,7 +43,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='rules',
|
||||
field=models.JSONField(default=dict),
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='checkin',
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.0.11 on 2021-01-15 09:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0174_merge_20201222_1031'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='orderrefund',
|
||||
name='comment',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 3.0.11 on 2021-02-05 15:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0175_orderrefund_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='eventmetaproperty',
|
||||
name='allowed_values',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventmetaproperty',
|
||||
name='protected',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventmetaproperty',
|
||||
name='required',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 3.0.10 on 2021-03-01 15:10
|
||||
|
||||
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0176_auto_20210205_1512'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='name_cached',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='name_parts',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='phone',
|
||||
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None),
|
||||
),
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# Generated by Django 3.0.12 on 2021-03-08 13:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0177_auto_20210301_1510'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='attendee_name',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='event_date_to',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='item',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='variation',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation'),
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,7 @@ from django.db.models import Exists, F, Max, OuterRef, Q, Subquery
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from jsonfallback.fields import FallbackJSONField
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
@@ -46,7 +47,7 @@ class CheckinList(LoggedModel):
|
||||
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
|
||||
'are not checked again before entry and should be considered validated directly upon purchase.')
|
||||
)
|
||||
rules = models.JSONField(default=dict, blank=True)
|
||||
rules = FallbackJSONField(default=dict, blank=True)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
|
||||
@@ -10,18 +10,15 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import (
|
||||
MaxValueValidator, MinLengthValidator, MinValueValidator, RegexValidator,
|
||||
)
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
@@ -356,11 +353,8 @@ class Event(EventMixin, LoggedModel):
|
||||
"remembered, but you can also choose to use a random value. "
|
||||
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
||||
validators=[
|
||||
MinLengthValidator(
|
||||
limit_value=2,
|
||||
),
|
||||
RegexValidator(
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$",
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
|
||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||
),
|
||||
EventSlugBanlistValidator()
|
||||
@@ -399,18 +393,10 @@ class Event(EventMixin, LoggedModel):
|
||||
geo_lat = models.FloatField(
|
||||
verbose_name=_("Latitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-90),
|
||||
MaxValueValidator(90),
|
||||
]
|
||||
)
|
||||
geo_lon = models.FloatField(
|
||||
verbose_name=_("Longitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-180),
|
||||
MaxValueValidator(180),
|
||||
]
|
||||
)
|
||||
plugins = models.TextField(
|
||||
null=False, blank=True,
|
||||
@@ -697,9 +683,9 @@ class Event(EventMixin, LoggedModel):
|
||||
for k, v in rules.items():
|
||||
if k == 'lookup':
|
||||
if v[0] == 'product':
|
||||
v[1] = str(item_map.get(int(v[1]), 0).pk) if int(v[1]) in item_map else "0"
|
||||
v[1] = str(item_map.get(int(v[1]), 0).pk)
|
||||
elif v[0] == 'variation':
|
||||
v[1] = str(variation_map.get(int(v[1]), 0).pk) if int(v[1]) in variation_map else "0"
|
||||
v[1] = str(variation_map.get(int(v[1]), 0).pk)
|
||||
else:
|
||||
_walk_rules(v)
|
||||
elif isinstance(rules, list):
|
||||
@@ -862,7 +848,7 @@ class Event(EventMixin, LoggedModel):
|
||||
Returns the currently configured ticket secret generator.
|
||||
"""
|
||||
tsgs = self.ticket_secret_generators
|
||||
return tsgs.get(self.settings.ticket_secret_generator, tsgs.get('random'))
|
||||
return tsgs[self.settings.ticket_secret_generator]
|
||||
|
||||
def get_data_shredders(self) -> dict:
|
||||
"""
|
||||
@@ -954,18 +940,6 @@ class Event(EventMixin, LoggedModel):
|
||||
if not self.quotas.exists():
|
||||
issues.append(_('You need to configure at least one quota to sell anything.'))
|
||||
|
||||
for mp in self.organizer.meta_properties.all():
|
||||
if mp.required and not self.meta_data.get(mp.name):
|
||||
issues.append(
|
||||
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
|
||||
property=mp.name,
|
||||
a_attr='href="%s#id_prop-%d-value"' % (
|
||||
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
||||
mp.pk
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
responses = event_live_issues.send(self)
|
||||
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
if response:
|
||||
@@ -1147,18 +1121,10 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
geo_lat = models.FloatField(
|
||||
verbose_name=_("Latitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-90),
|
||||
MaxValueValidator(90),
|
||||
]
|
||||
)
|
||||
geo_lon = models.FloatField(
|
||||
verbose_name=_("Longitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-180),
|
||||
MaxValueValidator(180),
|
||||
]
|
||||
null=True, blank=True
|
||||
)
|
||||
frontpage_text = I18nTextField(
|
||||
null=True, blank=True,
|
||||
@@ -1376,26 +1342,7 @@ class EventMetaProperty(LoggedModel):
|
||||
],
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
default = models.TextField(blank=True, verbose_name=_("Default value"))
|
||||
protected = models.BooleanField(default=False,
|
||||
verbose_name=_("Can only be changed by organizer-level administrators"))
|
||||
required = models.BooleanField(
|
||||
default=False, verbose_name=_("Required for events"),
|
||||
help_text=_("If checked, an event can only be taken live if the property is set. In event series, its always "
|
||||
"optional to set a value for individual dates")
|
||||
)
|
||||
allowed_values = models.TextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Valid values"),
|
||||
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
|
||||
)
|
||||
|
||||
def full_clean(self, exclude=None, validate_unique=True):
|
||||
super().full_clean(exclude, validate_unique)
|
||||
if self.default and self.required:
|
||||
raise ValidationError(_("A property can either be required or have a default value, not both."))
|
||||
if self.default and self.allowed_values and self.default not in self.allowed_values.splitlines():
|
||||
raise ValidationError(_("You cannot set a default value that is not a valid value."))
|
||||
default = models.TextField(blank=True)
|
||||
|
||||
|
||||
class EventMetaValue(LoggedModel):
|
||||
|
||||
@@ -273,14 +273,6 @@ class InvoiceLine(models.Model):
|
||||
:type subevent: SubEvent
|
||||
:param event_date_from: Event date of the (sub)event at the time the invoice was created
|
||||
:type event_date_from: datetime
|
||||
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
|
||||
:type event_date_to: datetime
|
||||
:param item: The item this line refers to
|
||||
:type item: Item
|
||||
:param variation: The variation this line refers to
|
||||
:type variation: ItemVariation
|
||||
:param attendee_name: The attendee name at the time the invoice was created
|
||||
:type attendee_name: str
|
||||
"""
|
||||
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
|
||||
position = models.PositiveIntegerField(default=0)
|
||||
@@ -291,10 +283,6 @@ class InvoiceLine(models.Model):
|
||||
tax_name = models.CharField(max_length=190)
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event_date_from = models.DateTimeField(null=True)
|
||||
event_date_to = models.DateTimeField(null=True)
|
||||
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
|
||||
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
|
||||
attendee_name = models.TextField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def net_value(self):
|
||||
|
||||
@@ -1026,7 +1026,7 @@ class Question(LoggedModel):
|
||||
(TYPE_PHONENUMBER, _("Phone number")),
|
||||
)
|
||||
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
|
||||
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_PHONENUMBER]
|
||||
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE, TYPE_PHONENUMBER]
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -1069,7 +1069,6 @@ class Question(LoggedModel):
|
||||
)
|
||||
ask_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
hidden = models.BooleanField(
|
||||
|
||||
@@ -2,6 +2,7 @@ import copy
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import string
|
||||
from collections import Counter
|
||||
from datetime import datetime, time, timedelta
|
||||
@@ -29,6 +30,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from jsonfallback.fields import FallbackJSONField
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumbers import NumberParseException
|
||||
@@ -613,26 +615,21 @@ class Order(LockModel, LoggedModel):
|
||||
return proposals
|
||||
|
||||
@staticmethod
|
||||
def normalize_code(code, is_fallback=False):
|
||||
d = {
|
||||
def normalize_code(code):
|
||||
tr = str.maketrans({
|
||||
'2': 'Z',
|
||||
'4': 'A',
|
||||
'5': 'S',
|
||||
'6': 'G',
|
||||
}
|
||||
if is_fallback:
|
||||
d['8'] = 'B'
|
||||
# 8 has been removed from the character set only in 2021, which means there are a lot of order codes
|
||||
# with an 8 in it around. We only want to replace this when this is used in a fallback.
|
||||
tr = str.maketrans(d)
|
||||
})
|
||||
return code.upper().translate(tr)
|
||||
|
||||
def assign_code(self):
|
||||
# This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
|
||||
# and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
|
||||
# handwriting (2/Z, 4/A, 5/S, 6/G, 8/B). This allows for better detection e.g. in incoming wire transfers that
|
||||
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
|
||||
# might include OCR'd handwritten text
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ379')
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
iteration = 0
|
||||
length = settings.ENTROPY['order_code']
|
||||
while True:
|
||||
@@ -664,8 +661,6 @@ class Order(LockModel, LoggedModel):
|
||||
related to the order. This checks order status and modification deadlines. It also
|
||||
returns ``False`` if there are no questions that can be answered.
|
||||
"""
|
||||
from .checkin import Checkin
|
||||
|
||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
|
||||
@@ -681,21 +676,10 @@ class Order(LockModel, LoggedModel):
|
||||
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
return False
|
||||
|
||||
positions = list(
|
||||
self.positions.all().annotate(
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||
).select_related('item').prefetch_related('item__questions')
|
||||
)
|
||||
if not self.event.settings.allow_modifications_after_checkin:
|
||||
for cp in positions:
|
||||
if cp.has_checkin:
|
||||
return False
|
||||
|
||||
if self.event.settings.get('invoice_address_asked', as_type=bool):
|
||||
return True
|
||||
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||
for cp in positions:
|
||||
for cp in self.positions.all().prefetch_related('item__questions'):
|
||||
if (cp.item.admission and ask_names) or cp.item.questions.all():
|
||||
return True
|
||||
|
||||
@@ -1131,7 +1115,7 @@ class AbstractPosition(models.Model):
|
||||
blank=True, null=True,
|
||||
help_text=_("Empty, if this product is not an admission ticket")
|
||||
)
|
||||
attendee_name_parts = models.JSONField(
|
||||
attendee_name_parts = FallbackJSONField(
|
||||
blank=True, default=dict
|
||||
)
|
||||
attendee_email = models.EmailField(
|
||||
@@ -1235,9 +1219,6 @@ class AbstractPosition(models.Model):
|
||||
else self.variation.quotas.filter(subevent=self.subevent))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
if 'attendee_name_parts' in update_fields:
|
||||
update_fields.append('attendee_name_cached')
|
||||
self.attendee_name_cached = self.attendee_name
|
||||
if self.attendee_name_parts is None:
|
||||
self.attendee_name_parts = {}
|
||||
@@ -1503,7 +1484,7 @@ class OrderPayment(models.Model):
|
||||
OrderRefund.REFUND_STATE_CREATED)
|
||||
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||
if payment_sum - refund_sum < self.order.total:
|
||||
logger.info('Confirmed payment {} but payment sum is {} and refund sum is {}.'.format(
|
||||
logger.info('Confirmed payment {} but payment sum is {} and refund sum is.'.format(
|
||||
self.full_id, payment_sum, refund_sum
|
||||
))
|
||||
return
|
||||
@@ -1727,11 +1708,6 @@ class OrderRefund(models.Model):
|
||||
max_length=255,
|
||||
verbose_name=_("Payment provider")
|
||||
)
|
||||
comment = models.TextField(
|
||||
verbose_name=_("Refund reason"),
|
||||
help_text=_('May be shown to the end user or used e.g. as part of a payment reference.'),
|
||||
null=True, blank=True
|
||||
)
|
||||
info = models.TextField(
|
||||
verbose_name=_("Payment information"),
|
||||
null=True, blank=True
|
||||
@@ -1770,7 +1746,7 @@ class OrderRefund(models.Model):
|
||||
Marks the refund as complete. This does not modify the state of the order.
|
||||
|
||||
:param user: The user who performed the change
|
||||
:param auth: The API auth token that performed the change
|
||||
:param user: The API auth token that performed the change
|
||||
"""
|
||||
self.state = self.REFUND_STATE_DONE
|
||||
self.execution_date = self.execution_date or now()
|
||||
@@ -2251,7 +2227,7 @@ class InvoiceAddress(models.Model):
|
||||
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
||||
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||
name_parts = models.JSONField(default=dict)
|
||||
name_parts = FallbackJSONField(default=dict)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=False)
|
||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
||||
@@ -2331,6 +2307,7 @@ def cachedticket_name(instance, filename: str) -> str:
|
||||
no=instance.order_position.positionid,
|
||||
code=instance.order_position.order.code,
|
||||
secret=secret,
|
||||
ext=os.path.splitext(filename)[1]
|
||||
)
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user