forked from CGM_Public/pretix_original
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0026d8a0c | ||
|
|
1cee082821 | ||
|
|
6c1a3a4c68 | ||
|
|
156e8413f8 | ||
|
|
46ccce439a | ||
|
|
675de12a5d | ||
|
|
5992892035 | ||
|
|
1c81792cd7 | ||
|
|
73e7d407cd | ||
|
|
fa78583cd3 | ||
|
|
bcba7b70ca | ||
|
|
141c6d04b2 | ||
|
|
9c0da900a2 | ||
|
|
580479b266 | ||
|
|
4adaa2059d | ||
|
|
a900f39121 | ||
|
|
b625d987a9 | ||
|
|
71e7d527d1 | ||
|
|
6fd0880e79 | ||
|
|
8ca253c860 | ||
|
|
63c2852668 | ||
|
|
5b36fa198d | ||
|
|
ef8b6f60b8 | ||
|
|
6ca07662b6 | ||
|
|
45a499ebba | ||
|
|
1bfa4c6fda | ||
|
|
8a169d0496 | ||
|
|
40dbae76ca | ||
|
|
4203087eff | ||
|
|
88bf31bd7a | ||
|
|
3423923d84 | ||
|
|
beb33e21ee | ||
|
|
461ab8ba0a | ||
|
|
7562f333cf | ||
|
|
32f1c32936 | ||
|
|
eb0123e350 | ||
|
|
37ba885c55 | ||
|
|
8330448a94 | ||
|
|
8582bf8158 | ||
|
|
e872180ed1 | ||
|
|
cc88e70db6 | ||
|
|
c335dd35b3 | ||
|
|
cea8efc4a3 | ||
|
|
c6c0f92891 | ||
|
|
d5950821e2 | ||
|
|
78f2581bb8 | ||
|
|
c9f89dc920 | ||
|
|
fb7d38ede0 | ||
|
|
8be2f9ad6b | ||
|
|
c033efbfa2 | ||
|
|
d990f0e927 | ||
|
|
e011b7810d | ||
|
|
0d0bbe1ce5 | ||
|
|
488273d5f2 | ||
|
|
9fdaf040dc | ||
|
|
d109dde1e1 | ||
|
|
d713398e88 | ||
|
|
0898d13e4c | ||
|
|
04098ce002 | ||
|
|
f2a18325b6 | ||
|
|
4db0530c09 | ||
|
|
938d84b251 | ||
|
|
c65b2aa4f8 | ||
|
|
2583e6166a | ||
|
|
825fd1820b | ||
|
|
c8d039b196 | ||
|
|
72b6ff0389 | ||
|
|
ef4db07e8b | ||
|
|
ef1e5759eb | ||
|
|
9f1079dcc4 | ||
|
|
518c1fbbf2 | ||
|
|
b9c9a03cdd | ||
|
|
5060bac7e0 | ||
|
|
c4be508e26 | ||
|
|
c75f741d4f | ||
|
|
d6ef563f83 | ||
|
|
3f75a935a3 | ||
|
|
246e7c9443 | ||
|
|
3dd685bf7a | ||
|
|
1480bd0690 | ||
|
|
01af8568ca | ||
|
|
74461dde50 | ||
|
|
f0fd4272dc | ||
|
|
a0f60c71b9 | ||
|
|
6b2ab44b26 | ||
|
|
9472d81e55 | ||
|
|
b630174f72 | ||
|
|
25c35b0f73 | ||
|
|
c0792f4171 | ||
|
|
5d490728df | ||
|
|
21fbf095cf | ||
|
|
7b8ad1ebbe | ||
|
|
81f37d9ce5 | ||
|
|
40c4872459 | ||
|
|
f0574755a2 | ||
|
|
4cfedebf3b | ||
|
|
45376dd757 | ||
|
|
0999f41b0c | ||
|
|
565f77d13b | ||
|
|
5ae7a350b0 | ||
|
|
af7d9942f6 | ||
|
|
36efb25b98 | ||
|
|
7a496da945 | ||
|
|
03f1016cc7 | ||
|
|
4d4d2d5fe7 | ||
|
|
98f48e78a8 | ||
|
|
512c9f5301 | ||
|
|
d16c59e86c | ||
|
|
177b0505fd | ||
|
|
01f7a70347 | ||
|
|
3b4c99d450 | ||
|
|
3bb23bb77e | ||
|
|
89da0847ca | ||
|
|
07bed72b5e | ||
|
|
c103288eec | ||
|
|
03648b77b1 | ||
|
|
818d75ddd7 | ||
|
|
20f608caae | ||
|
|
7b3a6d47fc | ||
|
|
d586406c79 | ||
|
|
e214c8cb95 | ||
|
|
81f2b9db30 | ||
|
|
04a6ed20b9 |
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
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 --no-use-pep517 -Ur doc/requirements.txt
|
||||
run: pip3 install -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 --no-use-pep517 -Ur src/requirements.txt
|
||||
run: pip3 install -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 --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
run: pip3 install -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 --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
run: pip3 install -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 --no-use-pep517 -Ur src/requirements/dev.txt
|
||||
run: pip3 install -r src/requirements.txt -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 --no-use-pep517 -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
|
||||
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
file=/tmp/supervisor.sock
|
||||
|
||||
[supervisord]
|
||||
logfile=/tmp/supervisord.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=10
|
||||
logfile=/dev/stdout
|
||||
logfile_maxbytes=0
|
||||
loglevel=info
|
||||
pidfile=/tmp/supervisord.pid
|
||||
nodaemon=false
|
||||
|
||||
@@ -3,5 +3,7 @@ command=/usr/sbin/nginx
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
@@ -4,3 +4,7 @@ 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,3 +5,7 @@ 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
|
||||
|
||||
@@ -8,4 +8,5 @@ This part of the documentation contains how-to guides on some special use cases
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
order_lifecycle
|
||||
custom_checkout
|
||||
|
||||
56
doc/api/guides/order_lifecycle.rst
Normal file
56
doc/api/guides/order_lifecycle.rst
Normal file
@@ -0,0 +1,56 @@
|
||||
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,10 +42,6 @@ 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,14 +25,6 @@ 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,22 +36,6 @@ 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``,
|
||||
@@ -68,10 +52,6 @@ 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.
|
||||
@@ -380,29 +360,6 @@ 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,31 +52,6 @@ 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.
|
||||
|
||||
@@ -46,24 +46,6 @@ 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.
|
||||
|
||||
@@ -28,10 +28,6 @@ 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,10 +30,6 @@ designated_price money (string) Designated pric
|
||||
taxation. This is not added to the price.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -26,14 +26,6 @@ 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
|
||||
---------
|
||||
|
||||
|
||||
@@ -118,44 +118,6 @@ 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,60 +94,6 @@ 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.
|
||||
@@ -220,7 +166,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
|
||||
├ answer string Text representation of the answer (URL if answer is a file)
|
||||
├ 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
|
||||
@@ -233,30 +179,6 @@ 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
|
||||
@@ -274,6 +196,10 @@ 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
|
||||
@@ -302,14 +228,6 @@ 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
|
||||
@@ -330,17 +248,9 @@ execution_date datetime Date and time o
|
||||
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.
|
||||
@@ -1446,21 +1356,6 @@ 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.
|
||||
@@ -1800,10 +1695,6 @@ 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.
|
||||
@@ -2083,10 +1974,6 @@ 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.
|
||||
|
||||
@@ -19,10 +19,6 @@ 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,28 +75,6 @@ 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,14 +30,6 @@ 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,10 +20,6 @@ layout object JSON representa
|
||||
still evolves. The version in use can be found `here`_.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
|
||||
This endpoint has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ 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``)
|
||||
@@ -54,25 +55,6 @@ 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,14 +24,6 @@ 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,14 +46,6 @@ 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.
|
||||
|
||||
@@ -34,7 +34,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, 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, 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
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
@@ -79,7 +79,7 @@ Ticket designs
|
||||
""""""""""""""
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: layout_text_variables
|
||||
:members: layout_text_variables, layout_image_variables
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
:members: override_layout
|
||||
|
||||
@@ -82,11 +82,15 @@ 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 four states that are listed in the diagram below:
|
||||
An order can be in one of currently six states that are listed in the diagram below:
|
||||
|
||||
.. image:: /images/order_states.png
|
||||
|
||||
There are additional "fake" states that are displayed like states but not represented as states in the system:
|
||||
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:
|
||||
|
||||
* An order is considered **canceled (with paid fee)** if it is in **paid** status but does not include any non-cancelled positions.
|
||||
|
||||
|
||||
BIN
doc/images/order_objects.png
Normal file
BIN
doc/images/order_objects.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
34
doc/images/order_objects.puml
Normal file
34
doc/images/order_objects.puml
Normal file
@@ -0,0 +1,34 @@
|
||||
@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: 53 KiB After Width: | Height: | Size: 93 KiB |
@@ -1,19 +1,39 @@
|
||||
@startuml
|
||||
|
||||
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
|
||||
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: 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
|
||||
|
||||
[*] -> 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
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -22,10 +22,6 @@ item_assignments list of objects Products this l
|
||||
└ item integer Item ID
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.16
|
||||
|
||||
This resource has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -24,14 +24,6 @@ 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
|
||||
---------
|
||||
|
||||
@@ -88,6 +88,15 @@ 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
|
||||
-----------------------
|
||||
|
||||
@@ -197,7 +206,10 @@ 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.
|
||||
|
||||
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
|
||||
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``.
|
||||
|
||||
You can style the button using the ``pretix-button`` CSS class.
|
||||
|
||||
@@ -425,11 +437,6 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
</script>
|
||||
|
||||
|
||||
.. 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
|
||||
|
||||
Dynamically opening the widget has been added in pretix 3.6.
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.15.0"
|
||||
__version__ = "3.16.0"
|
||||
|
||||
@@ -41,6 +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'),
|
||||
)
|
||||
@@ -68,6 +69,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'),
|
||||
)
|
||||
@@ -97,7 +99,9 @@ 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'),
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 Event, TaxRule
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
@@ -174,9 +174,12 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
}
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
for key, v in value['meta_data'].items():
|
||||
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
|
||||
@@ -223,6 +226,14 @@ 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)
|
||||
@@ -238,10 +249,11 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
event.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
if key not in self.ignored_meta_properties:
|
||||
event.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
@@ -279,19 +291,21 @@ 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():
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
for prop, current_object in current.items():
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
if prop.name not in self.ignored_meta_properties:
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
@@ -395,8 +409,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',
|
||||
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
|
||||
'seat_category_mapping', 'last_modified')
|
||||
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
|
||||
'meta_data', 'seat_category_mapping', 'last_modified')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -444,11 +458,22 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
}
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
for key, v in value['meta_data'].items():
|
||||
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 {}
|
||||
@@ -465,10 +490,11 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
if key not in self.ignored_meta_properties:
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
# Seats
|
||||
if subevent.seating_plan:
|
||||
@@ -514,19 +540,21 @@ 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():
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
for prop, current_object in current.items():
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
if prop.name not in self.ignored_meta_properties:
|
||||
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):
|
||||
@@ -580,6 +608,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'locale',
|
||||
'region',
|
||||
'last_order_modification_date',
|
||||
'allow_modifications_after_checkin',
|
||||
'show_quota_left',
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_hours',
|
||||
|
||||
@@ -25,7 +25,7 @@ from pretix.base.models import (
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
|
||||
)
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.pdf import get_images, 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
|
||||
@@ -112,6 +112,17 @@ 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
|
||||
|
||||
class Meta:
|
||||
model = QuestionAnswer
|
||||
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
|
||||
@@ -265,6 +276,9 @@ 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)
|
||||
|
||||
@@ -279,7 +293,28 @@ class PdfDataSerializer(serializers.Field):
|
||||
for k, v in instance.item._cached_meta_data.items():
|
||||
res['itemmeta:' + k] = v
|
||||
|
||||
return res
|
||||
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
|
||||
|
||||
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -213,6 +213,8 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
|
||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
default_fields = [
|
||||
'contact_mail',
|
||||
'imprint_url',
|
||||
'organizer_info_text',
|
||||
'event_list_type',
|
||||
'event_list_availability',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import datetime
|
||||
import mimetypes
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
import django_filters
|
||||
@@ -12,6 +14,7 @@ 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 (
|
||||
@@ -35,8 +38,9 @@ from pretix.base.models import (
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
|
||||
TaxRule, TeamAPIToken, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import RevokedTicketSecret
|
||||
from pretix.base.models.orders import QuestionAnswer, 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 (
|
||||
@@ -913,6 +917,62 @@ 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)
|
||||
|
||||
@@ -468,7 +468,8 @@ def base_placeholders(sender, **kwargs):
|
||||
'68CYU2H6ZTP3WLK5'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
|
||||
# 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),
|
||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import io
|
||||
import re
|
||||
import tempfile
|
||||
from collections import OrderedDict, namedtuple
|
||||
from decimal import Decimal
|
||||
@@ -10,11 +11,21 @@ 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 KNOWN_TYPES
|
||||
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, 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
|
||||
@@ -181,7 +192,7 @@ class ListExporter(BaseExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||
excel_safe(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||
for val in line
|
||||
])
|
||||
if total:
|
||||
@@ -242,7 +253,10 @@ class MultiSheetListExporter(ListExporter):
|
||||
pass
|
||||
|
||||
def iterate_sheet(self, form_data, sheet):
|
||||
raise NotImplementedError() # noqa
|
||||
if hasattr(self, 'iterate_' + sheet):
|
||||
yield from getattr(self, 'iterate_' + sheet)(form_data)
|
||||
else:
|
||||
raise NotImplementedError() # noqa
|
||||
|
||||
def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
|
||||
total = 0
|
||||
@@ -288,6 +302,9 @@ 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)):
|
||||
@@ -295,7 +312,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
str(val) if not isinstance(val, KNOWN_TYPES) else val
|
||||
excel_safe(val)
|
||||
for val in line
|
||||
])
|
||||
if total:
|
||||
|
||||
@@ -66,7 +66,7 @@ class InvoiceExporterMixin:
|
||||
)
|
||||
|
||||
def invoices_queryset(self, form_data: dict):
|
||||
qs = Invoice.objects.filter(event__in=self.events)
|
||||
qs = Invoice.objects.filter(event__in=self.events).select_related('order')
|
||||
|
||||
if form_data.get('payment_provider'):
|
||||
qs = qs.annotate(
|
||||
@@ -112,13 +112,13 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), 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.file.read())
|
||||
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
|
||||
i.file.close()
|
||||
counter += 1
|
||||
if total and counter % max(10, total // 100) == 0:
|
||||
|
||||
@@ -444,6 +444,7 @@ 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:
|
||||
@@ -452,6 +453,7 @@ 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']
|
||||
@@ -460,6 +462,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=pgettext_lazy('address', 'State'),
|
||||
required=False,
|
||||
choices=c,
|
||||
initial=state,
|
||||
widget=forms.Select(attrs={
|
||||
'autocomplete': 'address-level1',
|
||||
}),
|
||||
@@ -848,11 +851,13 @@ 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,7 +16,7 @@ class DatePickerWidget(forms.DateInput):
|
||||
date_attrs = dict(attrs)
|
||||
date_attrs.setdefault('class', 'form-control')
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
|
||||
date_attrs['autocomplete'] = 'off'
|
||||
|
||||
def placeholder():
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
@@ -37,7 +37,7 @@ class TimePickerWidget(forms.TimeInput):
|
||||
time_attrs = dict(attrs)
|
||||
time_attrs.setdefault('class', 'form-control')
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
|
||||
time_attrs['autocomplete'] = 'off'
|
||||
|
||||
def placeholder():
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
@@ -111,8 +111,8 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
time_attrs.setdefault('autocomplete', 'off')
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
|
||||
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
|
||||
date_attrs['autocomplete'] = 'off'
|
||||
time_attrs['autocomplete'] = 'off'
|
||||
if min_date:
|
||||
date_attrs['data-min'] = (
|
||||
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
|
||||
|
||||
@@ -12,12 +12,10 @@ 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
|
||||
@@ -31,6 +29,7 @@ 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__)
|
||||
|
||||
@@ -221,26 +220,6 @@ 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)')
|
||||
|
||||
@@ -30,7 +30,7 @@ class Command(BaseCommand):
|
||||
continue
|
||||
|
||||
if verbosity > 1:
|
||||
self.stdout.write(f'Running {name}…')
|
||||
self.stdout.write(f'INFO Running {name}…')
|
||||
t0 = time.time()
|
||||
try:
|
||||
r = receiver(signal=periodic_task, sender=self)
|
||||
@@ -40,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'FAIL: {str(err)}\n'))
|
||||
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
|
||||
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
|
||||
traceback.print_exc()
|
||||
else:
|
||||
if options.get('verbosity') > 1:
|
||||
if r is SKIPPED:
|
||||
self.stdout.write(self.style.SUCCESS(f'Skipped {name}'))
|
||||
self.stdout.write(self.style.SUCCESS(f'INFO Skipped {name}'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f'Completed {name} in {round(time.time() - t0, 3)}s'))
|
||||
self.stdout.write(self.style.SUCCESS(f'INFO Completed {name} in {round(time.time() - t0, 3)}s'))
|
||||
|
||||
28
src/pretix/base/migrations/0176_auto_20210205_1512.py
Normal file
28
src/pretix/base/migrations/0176_auto_20210205_1512.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -16,11 +16,12 @@ from django.core.validators import (
|
||||
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_lazy as _
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
@@ -861,7 +862,7 @@ class Event(EventMixin, LoggedModel):
|
||||
Returns the currently configured ticket secret generator.
|
||||
"""
|
||||
tsgs = self.ticket_secret_generators
|
||||
return tsgs[self.settings.ticket_secret_generator]
|
||||
return tsgs.get(self.settings.ticket_secret_generator, tsgs.get('random'))
|
||||
|
||||
def get_data_shredders(self) -> dict:
|
||||
"""
|
||||
@@ -953,6 +954,18 @@ 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:
|
||||
@@ -1363,7 +1376,26 @@ class EventMetaProperty(LoggedModel):
|
||||
],
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
default = models.TextField(blank=True)
|
||||
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."))
|
||||
|
||||
|
||||
class EventMetaValue(LoggedModel):
|
||||
|
||||
@@ -2,7 +2,6 @@ import copy
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import string
|
||||
from collections import Counter
|
||||
from datetime import datetime, time, timedelta
|
||||
@@ -666,6 +665,8 @@ 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,10 +682,21 @@ 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 self.positions.all().prefetch_related('item__questions'):
|
||||
for cp in positions:
|
||||
if (cp.item.admission and ask_names) or cp.item.questions.all():
|
||||
return True
|
||||
|
||||
@@ -1492,7 +1504,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
|
||||
@@ -2320,7 +2332,6 @@ 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]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import date, datetime, time
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
@@ -88,6 +89,15 @@ class Organizer(LoggedModel):
|
||||
|
||||
return ObjectRelatedCache(self)
|
||||
|
||||
@cached_property
|
||||
def all_logentries_link(self):
|
||||
return reverse(
|
||||
'control:organizer.log',
|
||||
kwargs={
|
||||
'organizer': self.slug,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def has_gift_cards(self):
|
||||
return self.cache.get_or_set(
|
||||
|
||||
@@ -718,7 +718,7 @@ class BasePaymentProvider:
|
||||
return a very short version of the payment method. Usually, this should return e.g.
|
||||
a transaction ID or account identifier, but no information on status, dates, etc.
|
||||
|
||||
The default implementation falls back to payment_presa_elrender.
|
||||
The default implementation falls back to ``payment_presale_render``.
|
||||
|
||||
:param order: The order object
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
@@ -35,9 +36,9 @@ from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoice import ThumbnailingImageReader
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.models import Order, OrderPosition, Question
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import layout_text_variables
|
||||
from pretix.base.signals import layout_image_variables, layout_text_variables
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.phone_format import phone_format
|
||||
from pretix.presale.style import get_fonts
|
||||
@@ -154,6 +155,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"editor_sample": _("Sample event name"),
|
||||
"evaluate": lambda op, order, ev: str(ev.name)
|
||||
}),
|
||||
("event_series_name", {
|
||||
"label": _("Event series"),
|
||||
"editor_sample": _("Sample event name"),
|
||||
"evaluate": lambda op, order, ev: str(order.event.name)
|
||||
}),
|
||||
("event_date", {
|
||||
"label": _("Event date"),
|
||||
"editor_sample": _("May 31st, 2017"),
|
||||
@@ -339,6 +345,47 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"evaluate": lambda op, order, ev: str(op.seat.seat_number if op.seat else "")
|
||||
}),
|
||||
))
|
||||
DEFAULT_IMAGES = OrderedDict([])
|
||||
|
||||
|
||||
@receiver(layout_image_variables, dispatch_uid="pretix_base_layout_image_variables_questions")
|
||||
def images_from_questions(sender, *args, **kwargs):
|
||||
def get_answer(op, order, event, question_id, etag):
|
||||
a = None
|
||||
if op.addon_to:
|
||||
if 'answers' in getattr(op.addon_to, '_prefetched_objects_cache', {}):
|
||||
try:
|
||||
a = [a for a in op.addon_to.answers.all() if a.question_id == question_id][0]
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
a = op.addon_to.answers.filter(question_id=question_id).first()
|
||||
|
||||
if 'answers' in getattr(op, '_prefetched_objects_cache', {}):
|
||||
try:
|
||||
a = [a for a in op.answers.all() if a.question_id == question_id][0]
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
a = op.answers.filter(question_id=question_id).first()
|
||||
|
||||
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
|
||||
return None
|
||||
else:
|
||||
if etag:
|
||||
return hashlib.sha1(a.file.name.encode()).hexdigest()
|
||||
return a.file
|
||||
|
||||
d = {}
|
||||
for q in sender.questions.all():
|
||||
if q.type != Question.TYPE_FILE:
|
||||
continue
|
||||
d['question_{}'.format(q.identifier)] = {
|
||||
'label': _('Question: {question}').format(question=q.question),
|
||||
'evaluate': partial(get_answer, question_id=q.pk, etag=False),
|
||||
'etag': partial(get_answer, question_id=q.pk, etag=True),
|
||||
}
|
||||
return d
|
||||
|
||||
|
||||
@receiver(layout_text_variables, dispatch_uid="pretix_base_layout_text_variables_questions")
|
||||
@@ -369,6 +416,8 @@ def variables_from_questions(sender, *args, **kwargs):
|
||||
|
||||
d = {}
|
||||
for q in sender.questions.all():
|
||||
if q.type == Question.TYPE_FILE:
|
||||
continue
|
||||
d['question_{}'.format(q.pk)] = {
|
||||
'label': _('Question: {question}').format(question=q.question),
|
||||
'editor_sample': _('<Answer: {question}>').format(question=q.question),
|
||||
@@ -387,6 +436,15 @@ def _get_ia_name_part(key, op, order, ev):
|
||||
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
|
||||
|
||||
|
||||
def get_images(event):
|
||||
v = copy.copy(DEFAULT_IMAGES)
|
||||
|
||||
for recv, res in layout_image_variables.send(sender=event):
|
||||
v.update(res)
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def get_variables(event):
|
||||
v = copy.copy(DEFAULT_VARIABLES)
|
||||
|
||||
@@ -427,6 +485,7 @@ class Renderer:
|
||||
self.layout = layout
|
||||
self.background_file = background_file
|
||||
self.variables = get_variables(event)
|
||||
self.images = get_images(event)
|
||||
self.event = event
|
||||
if self.background_file:
|
||||
self.bg_bytes = self.background_file.read()
|
||||
@@ -514,6 +573,47 @@ class Renderer:
|
||||
return '(error)'
|
||||
return ''
|
||||
|
||||
def _draw_imagearea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||
ev = self._get_ev(op, order)
|
||||
if not o['content'] or o['content'] not in self.images:
|
||||
image_file = None
|
||||
else:
|
||||
try:
|
||||
image_file = self.images[o['content']]['evaluate'](op, order, ev)
|
||||
except:
|
||||
logger.exception('Failed to process variable.')
|
||||
image_file = None
|
||||
|
||||
if image_file:
|
||||
ir = ThumbnailingImageReader(image_file)
|
||||
try:
|
||||
ir.resize(float(o['width']) * mm, float(o['height']) * mm, 300)
|
||||
except:
|
||||
logger.exception("Can not resize image")
|
||||
pass
|
||||
canvas.drawImage(
|
||||
image=ir,
|
||||
x=float(o['left']) * mm,
|
||||
y=float(o['bottom']) * mm,
|
||||
width=float(o['width']) * mm,
|
||||
height=float(o['height']) * mm,
|
||||
preserveAspectRatio=True,
|
||||
anchor='c', # centered in frame
|
||||
mask='auto'
|
||||
)
|
||||
else:
|
||||
canvas.saveState()
|
||||
canvas.setFillColorRGB(.8, .8, .8, alpha=1)
|
||||
canvas.rect(
|
||||
x=float(o['left']) * mm,
|
||||
y=float(o['bottom']) * mm,
|
||||
width=float(o['width']) * mm,
|
||||
height=float(o['height']) * mm,
|
||||
stroke=0,
|
||||
fill=1,
|
||||
)
|
||||
canvas.restoreState()
|
||||
|
||||
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||
font = o['fontfamily']
|
||||
if o['bold']:
|
||||
@@ -572,6 +672,8 @@ class Renderer:
|
||||
for o in self.layout:
|
||||
if o['type'] == "barcodearea":
|
||||
self._draw_barcodearea(canvas, op, o)
|
||||
elif o['type'] == "imagearea":
|
||||
self._draw_imagearea(canvas, op, order, o)
|
||||
elif o['type'] == "textarea":
|
||||
self._draw_textarea(canvas, op, order, o)
|
||||
elif o['type'] == "poweredby":
|
||||
|
||||
@@ -4,7 +4,7 @@ import struct
|
||||
|
||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.serialization.base import (
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
)
|
||||
|
||||
@@ -102,9 +102,11 @@ class RequiredQuestionsError(Exception):
|
||||
|
||||
|
||||
def _save_answers(op, answers, given_answers):
|
||||
written = False
|
||||
for q, a in given_answers.items():
|
||||
if not a:
|
||||
if q in answers:
|
||||
written = True
|
||||
answers[q].delete()
|
||||
else:
|
||||
continue
|
||||
@@ -113,6 +115,7 @@ def _save_answers(op, answers, given_answers):
|
||||
qa = answers[q]
|
||||
qa.answer = str(a.answer)
|
||||
qa.save()
|
||||
written = True
|
||||
qa.options.clear()
|
||||
else:
|
||||
qa = op.answers.create(question=q, answer=str(a.answer))
|
||||
@@ -122,6 +125,7 @@ def _save_answers(op, answers, given_answers):
|
||||
qa = answers[q]
|
||||
qa.answer = ", ".join([str(o) for o in a])
|
||||
qa.save()
|
||||
written = True
|
||||
qa.options.clear()
|
||||
else:
|
||||
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
|
||||
@@ -134,6 +138,7 @@ def _save_answers(op, answers, given_answers):
|
||||
qa.file.save(a.name, a, save=False)
|
||||
qa.answer = 'file://' + qa.file.name
|
||||
qa.save()
|
||||
written = True
|
||||
else:
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
@@ -141,9 +146,14 @@ def _save_answers(op, answers, given_answers):
|
||||
qa.save()
|
||||
else:
|
||||
op.answers.create(question=q, answer=str(a))
|
||||
written = True
|
||||
|
||||
if written:
|
||||
prefetched_objects_cache = getattr(op, '_prefetched_objects_cache', {})
|
||||
if 'answers' in prefetched_objects_cache:
|
||||
del prefetched_objects_cache['answers']
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY):
|
||||
@@ -163,18 +173,16 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
"""
|
||||
dt = datetime or now()
|
||||
|
||||
# Lock order positions
|
||||
op = OrderPosition.all.select_for_update().get(pk=op.pk)
|
||||
checkin_questions = list(
|
||||
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
|
||||
)
|
||||
|
||||
if op.canceled or op.order.status not in (Order.STATUS_PAID, Order.STATUS_PENDING):
|
||||
raise CheckInError(
|
||||
_('This order position has been canceled.'),
|
||||
'canceled' if canceled_supported else 'unpaid'
|
||||
)
|
||||
|
||||
# Do this outside of transaction so it is saved even if the checkin fails for some other reason
|
||||
checkin_questions = list(
|
||||
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
|
||||
)
|
||||
require_answers = []
|
||||
if checkin_questions:
|
||||
answers = {a.question: a for a in op.answers.all()}
|
||||
@@ -184,81 +192,85 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
|
||||
_save_answers(op, answers, given_answers)
|
||||
|
||||
if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]:
|
||||
raise CheckInError(
|
||||
_('This order position has an invalid product for this check-in list.'),
|
||||
'product'
|
||||
)
|
||||
elif clist.subevent_id and op.subevent_id != clist.subevent_id:
|
||||
raise CheckInError(
|
||||
_('This order position has an invalid date for this check-in list.'),
|
||||
'product'
|
||||
)
|
||||
elif op.order.status != Order.STATUS_PAID and not force and not (
|
||||
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
|
||||
):
|
||||
raise CheckInError(
|
||||
_('This order is not marked as paid.'),
|
||||
'unpaid'
|
||||
)
|
||||
elif require_answers and not force and questions_supported:
|
||||
raise RequiredQuestionsError(
|
||||
_('You need to answer questions to complete this check-in.'),
|
||||
'incomplete',
|
||||
require_answers
|
||||
)
|
||||
with transaction.atomic():
|
||||
# Lock order positions
|
||||
op = OrderPosition.all.select_for_update().get(pk=op.pk)
|
||||
|
||||
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
|
||||
rule_data = LazyRuleVars(op, clist, dt)
|
||||
logic = get_logic_environment(op.subevent or clist.event)
|
||||
if not logic.apply(clist.rules, rule_data):
|
||||
if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]:
|
||||
raise CheckInError(
|
||||
_('This entry is not permitted due to custom rules.'),
|
||||
'rules'
|
||||
_('This order position has an invalid product for this check-in list.'),
|
||||
'product'
|
||||
)
|
||||
elif clist.subevent_id and op.subevent_id != clist.subevent_id:
|
||||
raise CheckInError(
|
||||
_('This order position has an invalid date for this check-in list.'),
|
||||
'product'
|
||||
)
|
||||
elif op.order.status != Order.STATUS_PAID and not force and not (
|
||||
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
|
||||
):
|
||||
raise CheckInError(
|
||||
_('This order is not marked as paid.'),
|
||||
'unpaid'
|
||||
)
|
||||
elif require_answers and not force and questions_supported:
|
||||
raise RequiredQuestionsError(
|
||||
_('You need to answer questions to complete this check-in.'),
|
||||
'incomplete',
|
||||
require_answers
|
||||
)
|
||||
|
||||
device = None
|
||||
if isinstance(auth, Device):
|
||||
device = auth
|
||||
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
|
||||
rule_data = LazyRuleVars(op, clist, dt)
|
||||
logic = get_logic_environment(op.subevent or clist.event)
|
||||
if not logic.apply(clist.rules, rule_data):
|
||||
raise CheckInError(
|
||||
_('This entry is not permitted due to custom rules.'),
|
||||
'rules'
|
||||
)
|
||||
|
||||
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
|
||||
entry_allowed = (
|
||||
type == Checkin.TYPE_EXIT or
|
||||
clist.allow_multiple_entries or
|
||||
last_ci is None or
|
||||
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
|
||||
)
|
||||
device = None
|
||||
if isinstance(auth, Device):
|
||||
device = auth
|
||||
|
||||
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
|
||||
return
|
||||
|
||||
if entry_allowed or force:
|
||||
ci = Checkin.objects.create(
|
||||
position=op,
|
||||
type=type,
|
||||
list=clist,
|
||||
datetime=dt,
|
||||
device=device,
|
||||
gate=device.gate if device else None,
|
||||
nonce=nonce,
|
||||
forced=force and not entry_allowed,
|
||||
)
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': True,
|
||||
'forced': force or op.order.status != Order.STATUS_PAID,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': clist.pk
|
||||
}, user=user, auth=auth)
|
||||
checkin_created.send(op.order.event, checkin=ci)
|
||||
else:
|
||||
raise CheckInError(
|
||||
_('This ticket has already been redeemed.'),
|
||||
'already_redeemed',
|
||||
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
|
||||
entry_allowed = (
|
||||
type == Checkin.TYPE_EXIT or
|
||||
clist.allow_multiple_entries or
|
||||
last_ci is None or
|
||||
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
|
||||
)
|
||||
|
||||
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
|
||||
return
|
||||
|
||||
if entry_allowed or force:
|
||||
ci = Checkin.objects.create(
|
||||
position=op,
|
||||
type=type,
|
||||
list=clist,
|
||||
datetime=dt,
|
||||
device=device,
|
||||
gate=device.gate if device else None,
|
||||
nonce=nonce,
|
||||
forced=force and not entry_allowed,
|
||||
)
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': True,
|
||||
'forced': force or op.order.status != Order.STATUS_PAID,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': clist.pk
|
||||
}, user=user, auth=auth)
|
||||
checkin_created.send(op.order.event, checkin=ci)
|
||||
else:
|
||||
raise CheckInError(
|
||||
_('This ticket has already been redeemed.'),
|
||||
'already_redeemed',
|
||||
)
|
||||
|
||||
|
||||
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
|
||||
def order_placed(sender, **kwargs):
|
||||
|
||||
@@ -1082,6 +1082,15 @@ DEFAULTS = {
|
||||
help_text=_('If your event series has more than 50 dates in the future, only the month or week calendar can be used.')
|
||||
),
|
||||
},
|
||||
'allow_modifications_after_checkin': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow customers to modify their information after they checked in."),
|
||||
)
|
||||
},
|
||||
'last_order_modification_date': {
|
||||
'default': None,
|
||||
'type': RelativeDateWrapper,
|
||||
@@ -2367,6 +2376,22 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
'_scheme': 'full_transcription',
|
||||
},
|
||||
}),
|
||||
('salutation_given_family', {
|
||||
'fields': (
|
||||
('salutation', pgettext_lazy('person_name', 'Salutation'), 1),
|
||||
('given_name', _('Given name'), 2),
|
||||
('family_name', _('Family name'), 2),
|
||||
),
|
||||
'concatenation': lambda d: ' '.join(
|
||||
str(p) for p in (d.get(key, '') for key in ["given_name", "family_name"]) if p
|
||||
),
|
||||
'sample': {
|
||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
'_scheme': 'salutation_given_family',
|
||||
},
|
||||
}),
|
||||
('salutation_title_given_family', {
|
||||
'fields': (
|
||||
('salutation', pgettext_lazy('person_name', 'Salutation'), 1),
|
||||
|
||||
@@ -613,6 +613,7 @@ If the email is associated with a specific user, e.g. a notification email, the
|
||||
well, otherwise it will be ``None``.
|
||||
"""
|
||||
|
||||
|
||||
layout_text_variables = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to collect variables that can be used to display text in ticket-related PDF layouts.
|
||||
@@ -627,11 +628,35 @@ dictionaries as values that contain keys like in the following example::
|
||||
}
|
||||
}
|
||||
|
||||
The evaluate member will be called with the order position, order and event as arguments. The event might
|
||||
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
|
||||
also be a subevent, if applicable.
|
||||
"""
|
||||
|
||||
|
||||
layout_image_variables = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to collect variables that can be used to display dynamic images in ticket-related PDF layouts.
|
||||
Receivers are expected to return a dictionary with globally unique identifiers as keys and more
|
||||
dictionaries as values that contain keys like in the following example::
|
||||
|
||||
return {
|
||||
"profile": {
|
||||
"label": _("Profile picture"),
|
||||
"evaluate": lambda orderposition, order, event: ContentFile(b"some-image-data"),
|
||||
"etag": lambda orderposition, order, event: hash(b"some-image-data")
|
||||
}
|
||||
}
|
||||
|
||||
The ``evaluate`` member will be called with the order position, order and event as arguments. The event might
|
||||
also be a subevent, if applicable. The return value of ``evaluate`` should be an instance of Django's ``File``
|
||||
class and point to a valid JPEG or PNG file. If no image is available, ``evaluate`` should return ``None``.
|
||||
|
||||
The ``etag`` member will be called with the same arguments as ``evaluate`` but should return a ``str`` value
|
||||
uniquely identifying the version of the file. This can be a hash of the file, but can also be something else.
|
||||
If no image is available, ``etag`` should return ``None``. In some cases, this can speed up the implementation.
|
||||
"""
|
||||
|
||||
|
||||
timeline_events = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to collect events for the time line shown on event dashboards. You are passed
|
||||
|
||||
@@ -101,7 +101,8 @@ def truelink_callback(attrs, new=False):
|
||||
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
||||
"""
|
||||
text = re.sub(r'[^a-zA-Z0-9.\-/_]', '', attrs.get('_text')) # clean up link text
|
||||
href_url = urllib.parse.urlparse(attrs[None, 'href'])
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
href_url = urllib.parse.urlparse(url)
|
||||
if URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
|
||||
# link text looks like a url
|
||||
if text.startswith('//'):
|
||||
|
||||
@@ -28,6 +28,13 @@ class NextTimeField(forms.TimeField):
|
||||
return result
|
||||
|
||||
|
||||
class NextTimeInput(forms.TimeInput):
|
||||
def format_value(self, value):
|
||||
if isinstance(value, datetime):
|
||||
value = value.astimezone(get_current_timezone()).time()
|
||||
return super().format_value(value)
|
||||
|
||||
|
||||
class CheckinListForm(forms.ModelForm):
|
||||
def __init__(self, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
@@ -89,7 +96,7 @@ class CheckinListForm(forms.ModelForm):
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
|
||||
'exit_all_at': forms.TimeInput(attrs={'class': 'timepickerfield'}),
|
||||
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_products': SafeModelMultipleChoiceField,
|
||||
|
||||
@@ -265,15 +265,32 @@ class EventMetaValueForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.property = kwargs.pop('property')
|
||||
self.disabled = kwargs.pop('disabled')
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.property.allowed_values:
|
||||
self.fields['value'] = forms.ChoiceField(
|
||||
label=self.property.name,
|
||||
choices=[
|
||||
('', _('Default ({value})').format(value=self.property.default) if self.property.default else ''),
|
||||
] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()],
|
||||
)
|
||||
else:
|
||||
self.fields['value'].label = self.property.name
|
||||
self.fields['value'].widget.attrs['placeholder'] = self.property.default
|
||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||
reverse('control:events.meta.typeahead') + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
'organizer': self.property.organizer.slug,
|
||||
})
|
||||
)
|
||||
self.fields['value'].required = False
|
||||
self.fields['value'].widget.attrs['placeholder'] = self.property.default
|
||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||
reverse('control:events.meta.typeahead') + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
'organizer': self.property.organizer.slug,
|
||||
})
|
||||
)
|
||||
if self.disabled:
|
||||
self.fields['value'].widget.attrs['readonly'] = 'readonly'
|
||||
|
||||
def clean_slug(self):
|
||||
if self.disabled:
|
||||
return self.instance.value if self.instance else None
|
||||
return self.cleaned_data['slug']
|
||||
|
||||
class Meta:
|
||||
model = EventMetaValue
|
||||
@@ -457,6 +474,7 @@ class EventSettingsForm(SettingsForm):
|
||||
'banner_text_bottom',
|
||||
'order_email_asked_twice',
|
||||
'last_order_modification_date',
|
||||
'allow_modifications_after_checkin',
|
||||
'checkout_show_copy_answers_button',
|
||||
'primary_color',
|
||||
'theme_color_success',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, time
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
@@ -766,10 +766,15 @@ class SubEventFilterForm(FilterForm):
|
||||
),
|
||||
required=False
|
||||
)
|
||||
date = forms.DateField(
|
||||
label=_('Date'),
|
||||
date_from = forms.DateField(
|
||||
label=_('Date from'),
|
||||
required=False,
|
||||
widget=DatePickerWidget
|
||||
widget=DatePickerWidget,
|
||||
)
|
||||
date_until = forms.DateField(
|
||||
label=_('Date until'),
|
||||
required=False,
|
||||
widget=DatePickerWidget,
|
||||
)
|
||||
weekday = forms.ChoiceField(
|
||||
label=_('Weekday'),
|
||||
@@ -796,7 +801,8 @@ class SubEventFilterForm(FilterForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['date'].widget = DatePickerWidget()
|
||||
self.fields['date_from'].widget = DatePickerWidget()
|
||||
self.fields['date_until'].widget = DatePickerWidget()
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
@@ -838,19 +844,21 @@ class SubEventFilterForm(FilterForm):
|
||||
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
|
||||
)
|
||||
|
||||
if fdata.get('date'):
|
||||
date_start = make_aware(datetime.combine(
|
||||
fdata.get('date'),
|
||||
if fdata.get('date_until'):
|
||||
date_end = make_aware(datetime.combine(
|
||||
fdata.get('date_until') + timedelta(days=1),
|
||||
time(hour=0, minute=0, second=0, microsecond=0)
|
||||
), get_current_timezone())
|
||||
date_end = make_aware(datetime.combine(
|
||||
fdata.get('date'),
|
||||
time(hour=23, minute=59, second=59, microsecond=999999)
|
||||
), get_current_timezone())
|
||||
qs = qs.filter(
|
||||
Q(date_to__isnull=True, date_from__gte=date_start, date_from__lte=date_end) |
|
||||
Q(date_to__isnull=False, date_from__lte=date_end, date_to__gte=date_start)
|
||||
Q(date_to__isnull=True, date_from__lt=date_end) |
|
||||
Q(date_to__isnull=False, date_to__lt=date_end)
|
||||
)
|
||||
if fdata.get('date_from'):
|
||||
date_start = make_aware(datetime.combine(
|
||||
fdata.get('date_from'),
|
||||
time(hour=0, minute=0, second=0, microsecond=0)
|
||||
), get_current_timezone())
|
||||
qs = qs.filter(date_from__gte=date_start)
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
|
||||
@@ -104,7 +104,7 @@ class ConfirmPaymentForm(forms.Form):
|
||||
class CancelForm(ConfirmPaymentForm):
|
||||
send_email = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Notify user by e-mail'),
|
||||
label=_('Notify customer by email'),
|
||||
initial=True
|
||||
)
|
||||
cancellation_fee = forms.DecimalField(
|
||||
@@ -139,6 +139,11 @@ class CancelForm(ConfirmPaymentForm):
|
||||
|
||||
|
||||
class MarkPaidForm(ConfirmPaymentForm):
|
||||
send_email = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Notify customer by email'),
|
||||
initial=True
|
||||
)
|
||||
amount = forms.DecimalField(
|
||||
required=True,
|
||||
max_digits=10, decimal_places=2,
|
||||
|
||||
@@ -13,7 +13,9 @@ from pretix.api.models import WebHook
|
||||
from pretix.api.webhooks import get_all_webhook_events
|
||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import Device, Gate, GiftCard, Organizer, Team
|
||||
from pretix.base.models import (
|
||||
Device, EventMetaProperty, Gate, GiftCard, Organizer, Team,
|
||||
)
|
||||
from pretix.control.forms import ExtFileField, SplitDateTimeField
|
||||
from pretix.control.forms.event import SafeEventMultipleChoiceField
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
@@ -125,7 +127,8 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
|
||||
class EventMetaPropertyForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ['name', 'default']
|
||||
model = EventMetaProperty
|
||||
fields = ['name', 'default', 'required', 'protected', 'allowed_values']
|
||||
widgets = {
|
||||
'default': forms.TextInput()
|
||||
}
|
||||
@@ -214,6 +217,8 @@ class DeviceForm(forms.ModelForm):
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
auto_fields = [
|
||||
'contact_mail',
|
||||
'imprint_url',
|
||||
'organizer_info_text',
|
||||
'event_list_type',
|
||||
'event_list_availability',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from bootstrap3.renderers import FieldRenderer
|
||||
from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
|
||||
from bootstrap3.text import text_value
|
||||
from django.forms import CheckboxInput
|
||||
from django.forms.utils import flatatt
|
||||
@@ -58,3 +58,40 @@ class ControlFieldRenderer(FieldRenderer):
|
||||
optional=not required and not isinstance(self.widget, CheckboxInput)
|
||||
) + html
|
||||
return html
|
||||
|
||||
|
||||
class BulkEditMixin:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['layout'] = self.layout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def wrap_field(self, html):
|
||||
field_class = self.get_field_class()
|
||||
name = '{}{}'.format(self.field.form.prefix, self.field.name)
|
||||
checked = self.field.form.data and name in self.field.form.data.getlist('_bulk')
|
||||
html = (
|
||||
'<div class="{klass} bulk-edit-field-group">'
|
||||
'<label class="field-toggle">'
|
||||
'<input type="checkbox" name="_bulk" value="{name}" {checked}> {label}'
|
||||
'</label>'
|
||||
'<div class="field-content">'
|
||||
'{html}'
|
||||
'</div>'
|
||||
'</div>'
|
||||
).format(
|
||||
klass=field_class or '',
|
||||
name=name,
|
||||
label=pgettext('form_bulk', 'change'),
|
||||
checked='checked' if checked else '',
|
||||
html=html
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
class BulkEditFieldRenderer(BulkEditMixin, FieldRenderer):
|
||||
layout = 'horizontal'
|
||||
|
||||
|
||||
class InlineBulkEditFieldRenderer(BulkEditMixin, InlineFieldRenderer):
|
||||
layout = 'inline'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django import forms
|
||||
from django.forms import formset_factory
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.urls import reverse
|
||||
from django.utils.dates import MONTHS, WEEKDAYS
|
||||
from django.utils.functional import cached_property
|
||||
@@ -11,6 +12,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from i18nfield.forms import I18nInlineFormSet
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.forms.widgets import DatePickerWidget, TimePickerWidget
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import SubEventItem
|
||||
from pretix.base.reldate import RelativeDateTimeField
|
||||
@@ -88,6 +90,142 @@ class SubEventBulkForm(SubEventForm):
|
||||
del self.fields['date_admission']
|
||||
|
||||
|
||||
class NullBooleanSelect(forms.NullBooleanSelect):
|
||||
def __init__(self, attrs=None):
|
||||
choices = (
|
||||
('unknown', _('Keep the current values')),
|
||||
('true', _('Yes')),
|
||||
('false', _('No')),
|
||||
)
|
||||
super(forms.NullBooleanSelect, self).__init__(attrs, choices)
|
||||
|
||||
|
||||
class SubEventBulkEditForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.mixed_values = kwargs.pop('mixed_values')
|
||||
self.queryset = kwargs.pop('queryset')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
|
||||
for k in ('name', 'location', 'frontpage_text'):
|
||||
# i18n fields
|
||||
if k in self.mixed_values:
|
||||
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
|
||||
else:
|
||||
self.fields[k].widget.attrs['placeholder'] = ''
|
||||
self.fields[k].one_required = False
|
||||
|
||||
for k in ('geo_lat', 'geo_lon'):
|
||||
# scalar fields
|
||||
if k in self.mixed_values:
|
||||
self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
|
||||
else:
|
||||
self.fields[k].widget.attrs['placeholder'] = ''
|
||||
self.fields[k].widget.is_required = False
|
||||
self.fields[k].required = False
|
||||
|
||||
for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end'):
|
||||
self.fields[k + '_day'] = forms.DateField(
|
||||
label=self._meta.model._meta.get_field(k).verbose_name,
|
||||
help_text=self._meta.model._meta.get_field(k).help_text,
|
||||
widget=DatePickerWidget(),
|
||||
required=False,
|
||||
)
|
||||
self.fields[k + '_time'] = forms.TimeField(
|
||||
label=self._meta.model._meta.get_field(k).verbose_name,
|
||||
help_text=self._meta.model._meta.get_field(k).help_text,
|
||||
widget=TimePickerWidget(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'name',
|
||||
'location',
|
||||
'frontpage_text',
|
||||
'geo_lat',
|
||||
'geo_lon',
|
||||
'is_public',
|
||||
'active',
|
||||
]
|
||||
field_classes = {
|
||||
}
|
||||
widgets = {
|
||||
}
|
||||
|
||||
def save(self, commit=True):
|
||||
objs = list(self.queryset)
|
||||
fields = set()
|
||||
|
||||
check_map = {
|
||||
'geo_lat': '__geo',
|
||||
'geo_lon': '__geo',
|
||||
}
|
||||
for k in self.fields:
|
||||
cb_val = self.prefix + check_map.get(k, k)
|
||||
if cb_val not in self.data.getlist('_bulk'):
|
||||
continue
|
||||
|
||||
if k.endswith('_day'):
|
||||
for obj in objs:
|
||||
oldval = getattr(obj, k.replace('_day', ''))
|
||||
cval = self.cleaned_data[k]
|
||||
if cval is None:
|
||||
newval = None
|
||||
if not self._meta.model._meta.get_field(k.replace('_day', '')).null:
|
||||
continue
|
||||
elif oldval:
|
||||
oldval = oldval.astimezone(self.event.timezone)
|
||||
newval = oldval.replace(
|
||||
year=cval.year,
|
||||
month=cval.month,
|
||||
day=cval.day,
|
||||
)
|
||||
else:
|
||||
# If there is no previous date/time set, we'll just set to midnight
|
||||
# If the user also selected a time, this will be overridden anyways
|
||||
newval = datetime(
|
||||
year=cval.year,
|
||||
month=cval.month,
|
||||
day=cval.day,
|
||||
tzinfo=self.event.timezone
|
||||
)
|
||||
setattr(obj, k.replace('_day', ''), newval)
|
||||
fields.add(k.replace('_day', ''))
|
||||
elif k.endswith('_time'):
|
||||
for obj in objs:
|
||||
# If there is no previous date/time set and only a time is changed not the
|
||||
# date, we instead use the date of the event
|
||||
oldval = getattr(obj, k.replace('_time', '')) or obj.date_from
|
||||
cval = self.cleaned_data[k]
|
||||
if cval is None:
|
||||
continue
|
||||
oldval = oldval.astimezone(self.event.timezone)
|
||||
newval = oldval.replace(
|
||||
hour=cval.hour,
|
||||
minute=cval.minute,
|
||||
second=cval.second,
|
||||
)
|
||||
setattr(obj, k.replace('_time', ''), newval)
|
||||
fields.add(k.replace('_time', ''))
|
||||
else:
|
||||
fields.add(k)
|
||||
for obj in objs:
|
||||
setattr(obj, k, self.cleaned_data[k])
|
||||
|
||||
if fields:
|
||||
SubEvent.objects.bulk_update(objs, fields, 200)
|
||||
|
||||
def full_clean(self):
|
||||
if len(self.data) == 0:
|
||||
# form wasn't submitted
|
||||
self._errors = ErrorDict()
|
||||
return
|
||||
super().full_clean()
|
||||
|
||||
|
||||
class SubEventItemOrVariationFormMixin:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.item = kwargs.pop('item')
|
||||
@@ -162,15 +300,32 @@ class SubEventMetaValueForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.property = kwargs.pop('property')
|
||||
self.default = kwargs.pop('default', None)
|
||||
self.disabled = kwargs.pop('disabled', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.property.allowed_values:
|
||||
self.fields['value'] = forms.ChoiceField(
|
||||
label=self.property.name,
|
||||
choices=[
|
||||
('', _('Default ({value})').format(value=self.default or self.property.default) if self.default or self.property.default else ''),
|
||||
] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()],
|
||||
)
|
||||
else:
|
||||
self.fields['value'].label = self.property.name
|
||||
self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
|
||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||
reverse('control:events.meta.typeahead') + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
'organizer': self.property.organizer.slug,
|
||||
})
|
||||
)
|
||||
self.fields['value'].required = False
|
||||
self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
|
||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||
reverse('control:events.meta.typeahead') + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
'organizer': self.property.organizer.slug,
|
||||
})
|
||||
)
|
||||
if self.disabled:
|
||||
self.fields['value'].widget.attrs['readonly'] = 'readonly'
|
||||
|
||||
def clean_slug(self):
|
||||
if self.disabled:
|
||||
return self.instance.value if self.instance else None
|
||||
return self.cleaned_data['slug']
|
||||
|
||||
class Meta:
|
||||
model = SubEventMetaValue
|
||||
|
||||
@@ -273,8 +273,15 @@ def _display_checkin(event, logentry):
|
||||
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
plains = {
|
||||
'pretix.object.cloned': _('This object has been created by cloning.'),
|
||||
'pretix.organizer.changed': _('The organizer has been changed.'),
|
||||
'pretix.organizer.settings': _('The organizer settings have been changed.'),
|
||||
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
|
||||
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||
'pretix.webhook.created': _('The webhook has been created.'),
|
||||
'pretix.webhook.changed': _('The webhook has been changed.'),
|
||||
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
|
||||
'pretix.event.canceled': _('The event has been canceled.'),
|
||||
'pretix.event.deleted': _('An event has been deleted.'),
|
||||
'pretix.event.order.modified': _('The order details have been changed.'),
|
||||
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
|
||||
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
|
||||
|
||||
@@ -427,6 +427,13 @@ def get_organizer_navigation(request):
|
||||
}),
|
||||
'active': url.url_name == 'organizer.edit',
|
||||
},
|
||||
{
|
||||
'label': _('Event metadata'),
|
||||
'url': reverse('control:organizer.properties', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': url.url_name.startswith('organizer.propert'),
|
||||
},
|
||||
]
|
||||
})
|
||||
if 'can_change_teams' in request.orgapermset:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
|
||||
<div class="geodata-section">
|
||||
{% bootstrap_field form.location layout="control" %}
|
||||
{% include "pretixcontrol/event/fragment_geodata_autoupdate.html" %}
|
||||
<div class="form-group geodata-group"
|
||||
data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}"
|
||||
data-attrib="{{ global_settings.leaflet_tiles_attribution }}"
|
||||
data-icon="{% static "leaflet/images/marker-icon.png" %}"
|
||||
data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Geo coordinates" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lat layout="inline" %}
|
||||
{% if global_settings.opencagedata_apikey %}
|
||||
<p class="attrib">
|
||||
<a href="https://openstreetmap.org/" target="_blank">
|
||||
{% trans "Geocoding data © OpenStreetMap" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lon layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
{% load i18n %}
|
||||
|
||||
<span class="geodata-autoupdate">
|
||||
<span class="optional" data-notification="error" title="{% trans "Failed to retrieve geo coordinates" %}"><i class="fa fa-warning"></i></span>
|
||||
<span class="optional" data-notification="loading" title="{% trans "Retrieving geo coordinates …" %}"><i class="fa fa-cog fa-spin"></i></span>
|
||||
<span class="optional" data-notification="updated"><i class="fa fa-check"></i> {% trans "Geo coordinates updated" %}</span>
|
||||
<span class="optional" data-notification="confirm">New geo coordinates. <button type="button" data-action="update" class="btn btn-link text-nowrap">{% trans "Update map?" %}</button></span>
|
||||
</span>
|
||||
@@ -24,34 +24,7 @@
|
||||
{% endif %}
|
||||
{% bootstrap_field form.date_from layout="control" %}
|
||||
{% bootstrap_field form.date_to layout="control" %}
|
||||
<div class="geodata-section">
|
||||
{% bootstrap_field form.location layout="control" %}
|
||||
<div class="form-group geodata-group"
|
||||
data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}"
|
||||
data-attrib="{{ global_settings.leaflet_tiles_attribution }}"
|
||||
data-icon="{% static "leaflet/images/marker-icon.png" %}"
|
||||
data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Geo coordinates" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lat layout="inline" %}
|
||||
{% if global_settings.opencagedata_apikey %}
|
||||
<p class="attrib">
|
||||
<a href="https://openstreetmap.org/" target="_blank">
|
||||
{% trans "Geocoding data © OpenStreetMap" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lon layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||
{% bootstrap_field form.date_admission layout="control" %}
|
||||
{% bootstrap_field form.currency layout="control" %}
|
||||
{% bootstrap_field sform.contact_mail layout="control" %}
|
||||
@@ -70,7 +43,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -243,6 +216,7 @@
|
||||
{% bootstrap_field form.presale_end layout="control" %}
|
||||
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
||||
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
||||
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Display" %}</legend>
|
||||
|
||||
@@ -39,30 +39,7 @@
|
||||
{% if form.date_to %}
|
||||
{% bootstrap_field form.date_to layout="control" %}
|
||||
{% endif %}
|
||||
<div class="geodata-section">
|
||||
{% bootstrap_field form.location layout="control" %}
|
||||
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Geo coordinates" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lat layout="inline" %}
|
||||
{% if global_settings.opencagedata_apikey %}
|
||||
<p class="attrib">
|
||||
<a href="https://openstreetmap.org/" target="_blank">
|
||||
{% trans "Geocoding data © OpenStreetMap" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lon layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||
{% bootstrap_field form.currency layout="control" %}
|
||||
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.amount layout='horizontal' %}
|
||||
{% bootstrap_field form.payment_date layout='horizontal' %}
|
||||
{% bootstrap_field form.send_email layout='horizontal' %}
|
||||
{% if form.force %}
|
||||
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
||||
{% endif %}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</h1>
|
||||
<form method="post" href="">
|
||||
<form method="post" href=""
|
||||
>
|
||||
{% csrf_token %}
|
||||
<fieldset class="form-inline form-refund-choose">
|
||||
<legend>{% trans "How should the refund be sent?" %}</legend>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% for e in exporters %}
|
||||
<details class="panel panel-default" {% if "identifier" in request.GET %}open{% endif %}>
|
||||
<details class="panel panel-default" {% if "identifier" in request.GET or "exporter" in request.POST %}open{% endif %}>
|
||||
<summary class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{ e.verbose_name }}
|
||||
|
||||
@@ -22,111 +22,68 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors sform %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.slug layout="control" %}
|
||||
{% if form.domain %}
|
||||
{% bootstrap_field form.domain layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.organizer_info_text layout="control" %}
|
||||
{% bootstrap_field sform.event_team_provisioning layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Organizer page" %}</legend>
|
||||
{% bootstrap_field sform.organizer_logo_image layout="control" %}
|
||||
{% bootstrap_field sform.organizer_logo_image_large layout="control" %}
|
||||
{% bootstrap_field sform.organizer_homepage_text layout="control" %}
|
||||
{% bootstrap_field sform.event_list_type layout="control" %}
|
||||
{% bootstrap_field sform.event_list_availability layout="control" %}
|
||||
{% bootstrap_field sform.organizer_link_back layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Localization" %}</legend>
|
||||
{% bootstrap_field sform.locales layout="control" %}
|
||||
{% bootstrap_field sform.region layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Shop design" %}</legend>
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
These settings will be used for the organizer page as well as for the default settings
|
||||
for all events in this account that do not have their own design settings.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_field sform.primary_color layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_success layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_background layout="control" %}
|
||||
{% bootstrap_field sform.theme_round_borders layout="control" %}
|
||||
{% bootstrap_field sform.primary_font layout="control" %}
|
||||
{% bootstrap_field sform.favicon layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Gift cards" %}</legend>
|
||||
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
|
||||
{% bootstrap_field sform.giftcard_length layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Event metadata" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can here define a set of metadata properties (i.e. variables) that you can later set for your
|
||||
events and re-use in places like ticket layouts. This is an useful timesaver if you create lots and
|
||||
lots of events.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div class="row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-5 col-lg-6">
|
||||
{% bootstrap_field form.default layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 col-lg-1 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-5 col-lg-6">
|
||||
{% bootstrap_field formset.empty_form.default layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 col-lg-1 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add property" %}</button>
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-lg-10">
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.slug layout="control" %}
|
||||
{% if form.domain %}
|
||||
{% bootstrap_field form.domain layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.imprint_url layout="control" %}
|
||||
{% bootstrap_field sform.contact_mail layout="control" %}
|
||||
{% bootstrap_field sform.organizer_info_text layout="control" %}
|
||||
{% bootstrap_field sform.event_team_provisioning layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Organizer page" %}</legend>
|
||||
{% bootstrap_field sform.organizer_logo_image layout="control" %}
|
||||
{% bootstrap_field sform.organizer_logo_image_large layout="control" %}
|
||||
{% bootstrap_field sform.organizer_homepage_text layout="control" %}
|
||||
{% bootstrap_field sform.event_list_type layout="control" %}
|
||||
{% bootstrap_field sform.event_list_availability layout="control" %}
|
||||
{% bootstrap_field sform.organizer_link_back layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Localization" %}</legend>
|
||||
{% bootstrap_field sform.locales layout="control" %}
|
||||
{% bootstrap_field sform.region layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Shop design" %}</legend>
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
These settings will be used for the organizer page as well as for the default settings
|
||||
for all events in this account that do not have their own design settings.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_field sform.primary_color layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_success layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_background layout="control" %}
|
||||
{% bootstrap_field sform.theme_round_borders layout="control" %}
|
||||
{% bootstrap_field sform.primary_font layout="control" %}
|
||||
{% bootstrap_field sform.favicon layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Gift cards" %}</legend>
|
||||
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
|
||||
{% bootstrap_field sform.giftcard_length layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Change history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=organizer %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% for e in exporters %}
|
||||
<details class="panel panel-default" {% if "identifier" in request.GET %}open{% endif %}>
|
||||
<details class="panel panel-default" {% if "identifier" in request.GET or "exporter" in request.POST %}open{% endif %}>
|
||||
<summary class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{ e.verbose_name }}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<p>{% blocktrans %}Are you sure you want to delete the gate?{% endblocktrans %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.teams" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
||||
<a href="{% url "control:organizer.gates" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Organizer logs" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Organizer logs" %}</h1>
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
||||
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
||||
<p>
|
||||
<select name="user" class="form-control">
|
||||
<option value="">{% trans "All actions" %}</option>
|
||||
{% for up in userlist %}
|
||||
{% if up.user__id %}
|
||||
<option value="{{ up.user__id }}"
|
||||
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||
{{ up.user__email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
<ul class="list-group">
|
||||
{% for log in logs %}
|
||||
<li class="list-group-item logentry">
|
||||
<div class="row">
|
||||
<div class="col-lg-2 col-sm-6 col-xs-12">
|
||||
<span class="fa fa-clock-o"></span>
|
||||
{{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if log.shredded %}
|
||||
<span class="fa fa-eraser fa-danger fa-fw"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Personal data was cleared from this log entry." %}">
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-2 col-sm-6 col-xs-12">
|
||||
{% if log.user %}
|
||||
{% if log.user.is_staff %}
|
||||
<span class="fa fa-id-card fa-danger fa-fw"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "This change was performed by a pretix administrator." %}">
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="fa fa-user fa-fw"></span>
|
||||
{% endif %}
|
||||
{{ log.user.get_full_name }}
|
||||
{% if log.oauth_application %}
|
||||
<br><span class="fa fa-plug fa-fw"></span>
|
||||
{{ log.oauth_application.name }}
|
||||
{% endif %}
|
||||
{% elif log.device %}
|
||||
<span class="fa fa-mobile fa-fw"></span>
|
||||
{{ log.device.name }}
|
||||
{% elif log.api_token %}
|
||||
<span class="fa fa-key fa-fw"></span>
|
||||
{{ log.api_token.name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-2 col-sm-12 col-xs-12">
|
||||
{% if log.display_object %}
|
||||
<span class="fa fa-flag"></span> {{ log.display_object|safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-6 col-sm-12 col-xs-12">
|
||||
{{ log.display }}
|
||||
{% if staff_session %}
|
||||
<a href="" class="btn btn-default btn-xs" data-expandlogs data-id="{{ log.pk }}">
|
||||
<span class="fa-eye fa fa-fw"></span>
|
||||
{% trans "Inspect" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
<div class="list-group-item">
|
||||
<em>{% trans "No results" %}</em>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Event metadata" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can here define a set of metadata properties (i.e. variables) that you can later set for your
|
||||
events and re-use in places like ticket layouts. This is an useful timesaver if you create lots and
|
||||
lots of events.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% url "control:organizer.property.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new property" %}
|
||||
</a>
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Property" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in properties %}
|
||||
<tr>
|
||||
<td><strong>
|
||||
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}">
|
||||
{{ p.name }}
|
||||
</a>
|
||||
</strong></td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:organizer.property.delete" organizer=request.organizer.slug property=p.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Delete property:" %} {{ gate.name }}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete the property?{% endblocktrans %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.properties" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
{% if gate %}
|
||||
<h1>{% trans "Property:" %} {{ property.name }}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Create a new property" %}</h1>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -228,6 +228,18 @@
|
||||
id="toolbox-position-y">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group rectsize">
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Width (mm)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.01"
|
||||
id="toolbox-width">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Height (mm)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.01"
|
||||
id="toolbox-height">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group squaresize poweredby">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Size (mm)" %}</label><br>
|
||||
@@ -332,6 +344,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group imagecontent">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Image content" %}</label><br>
|
||||
<select class="input-block-level form-control" id="toolbox-imagecontent">
|
||||
<option value="">{% trans "Empty" %}</option>
|
||||
{% for varname, var in images.items %}
|
||||
<option value="{{ varname }}">{{ var.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group text">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Text content" %}</label><br>
|
||||
@@ -382,6 +405,10 @@
|
||||
<span class="fa fa-image"></span>
|
||||
{% trans "pretix Logo" %}
|
||||
</button>
|
||||
<button class="btn btn-default btn-block" id="editor-add-image" disabled>
|
||||
<span class="fa fa-image"></span>
|
||||
{% trans "Dynamic image" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -384,30 +384,7 @@
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.active layout="control" %}
|
||||
<div class="geodata-section">
|
||||
{% bootstrap_field form.location layout="control" %}
|
||||
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Geo coordinates" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lat layout="inline" %}
|
||||
{% if global_settings.opencagedata_apikey %}
|
||||
<p class="attrib">
|
||||
<a href="https://openstreetmap.org/" target="_blank">
|
||||
{% trans "Geocoding data © OpenStreetMap" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lon layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||
{% bootstrap_field form.frontpage_text layout="control" %}
|
||||
{% bootstrap_field form.is_public layout="control" %}
|
||||
{% if meta_forms %}
|
||||
@@ -422,7 +399,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% load captureas %}
|
||||
{% load static %}
|
||||
{% load eventsignal %}
|
||||
{% block title %}{% trans "Change multiple dates" context "subevent" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Change multiple dates" context "subevent" %}
|
||||
<small>
|
||||
{% blocktrans trimmed with number=subevents.count %}
|
||||
{{ number }} selected
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</h1>
|
||||
<form action="" method="post" class="form-horizontal" id="subevent-bulk-create-form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% for f in itemvar_forms %}
|
||||
{% bootstrap_form_errors f %}
|
||||
{% endfor %}
|
||||
<div class="hidden">
|
||||
{% for se in subevents %}
|
||||
<input type="hidden" name="subevent" value="{{ se.pk }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="bulkedit" %}
|
||||
{% bootstrap_field form.active layout="bulkedit" %}
|
||||
<div class="geodata-section">
|
||||
{% bootstrap_field form.location layout="bulkedit" %}
|
||||
{% include "pretixcontrol/event/fragment_geodata_autoupdate.html" %}
|
||||
<div class="form-group geodata-group"
|
||||
data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}"
|
||||
data-attrib="{{ global_settings.leaflet_tiles_attribution }}"
|
||||
data-icon="{% static "leaflet/images/marker-icon.png" %}"
|
||||
data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Geo coordinates" %}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<div class="bulk-edit-field-group">
|
||||
<label class="field-toggle">
|
||||
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__geo" {% if form.prefix|add:"__geo" in bulk_selected %}checked{% endif %}>
|
||||
{% trans "change" context "form_bulk" %}
|
||||
</label>
|
||||
<div class="field-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% bootstrap_field form.geo_lat layout="inline" %}
|
||||
{% if global_settings.opencagedata_apikey %}
|
||||
<p class="attrib">
|
||||
<a href="https://openstreetmap.org/" target="_blank" tabindex="-1">
|
||||
{% trans "Geocoding data © OpenStreetMap" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% bootstrap_field form.geo_lon layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.frontpage_text layout="bulkedit" %}
|
||||
{% bootstrap_field form.is_public layout="bulkedit" %}
|
||||
{% if meta_forms %}
|
||||
<div class="form-group metadata-group">
|
||||
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
|
||||
<div class="col-md-9">
|
||||
{% for form in meta_forms %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.value.id_for_label }}">
|
||||
{{ form.property.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="bulkedit_inline" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Timeline" %}</legend>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="{{ form.date_from_day.id_for_label }}">
|
||||
{{ form.date_from_day.label }}
|
||||
</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.date_from_day layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.date_from_time layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="{{ form.date_to_day.id_for_label }}">
|
||||
{{ form.date_to_day.label }}
|
||||
</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.date_to_day layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.date_to_time layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="{{ form.date_admission_day.id_for_label }}">
|
||||
{{ form.date_admission_day.label }}
|
||||
</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.date_admission_day layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.date_admission_time layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="{{ form.presale_start_day.id_for_label }}">
|
||||
{{ form.presale_start_day.label }}
|
||||
</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.presale_start_day layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.presale_start_time layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="{{ form.presale_end_day.id_for_label }}">
|
||||
{{ form.presale_end_day.label }}
|
||||
</label>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field form.presale_end_day layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.presale_end_time layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Item prices" %}</legend>
|
||||
{% for f in itemvar_forms %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="bulkedit_inline" %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% bootstrap_field f.disabled layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Quotas" %}</legend>
|
||||
{% if sampled_quotas|default_if_none:"NONE" == "NONE" %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
You selected a set of dates that currently have different quota setups. You can therefore
|
||||
not change their quotas in bulk. If you want, you can set up a new set of quotas to
|
||||
<strong>replace</strong> the quota setup of all selected dates.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bulk-edit-field-group">
|
||||
<label class="field-toggle">
|
||||
<input type="checkbox" name="_bulk" value="__quotas" {% if "__quotas" in bulk_selected %}checked{% endif %}>
|
||||
{% trans "change" context "form_bulk" %}
|
||||
</label>
|
||||
<div class="field-content">
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.size layout="control" %}
|
||||
{% bootstrap_field form.itemvars layout="control" %}
|
||||
{% bootstrap_field form.release_after_exit layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.size layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p> </p>
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
{% if sampled_lists|default_if_none:"NONE" == "NONE" %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
You selected a set of dates that currently have different check-in list setups. You can
|
||||
therefore not change their check-in lists in bulk.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bulk-edit-field-group">
|
||||
<label class="field-toggle">
|
||||
<input type="checkbox" name="_bulk" value="__checkinlists" {% if "__checkinlists" in bulk_selected %}checked{% endif %}>
|
||||
{% trans "change" context "form_bulk" %}
|
||||
</label>
|
||||
<div class="field-content">
|
||||
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
||||
{{ cl_formset.management_form }}
|
||||
{% bootstrap_formset_errors cl_formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in cl_formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
{% if form.gates %}
|
||||
{% bootstrap_field form.gates layout="control" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ cl_formset.empty_form.id }}
|
||||
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||
{% if cl_formset.empty_form.gates %}
|
||||
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -25,30 +25,7 @@
|
||||
{% bootstrap_field form.active layout="control" %}
|
||||
{% bootstrap_field form.date_from layout="control" %}
|
||||
{% bootstrap_field form.date_to layout="control" %}
|
||||
<div class="geodata-section">
|
||||
{% bootstrap_field form.location layout="control" %}
|
||||
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Geo coordinates" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lat layout="inline" %}
|
||||
{% if global_settings.opencagedata_apikey %}
|
||||
<p class="attrib">
|
||||
<a href="https://openstreetmap.org/" target="_blank">
|
||||
{% trans "Geocoding data © OpenStreetMap" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.geo_lon layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||
{% bootstrap_field form.date_admission layout="control" %}
|
||||
{% bootstrap_field form.frontpage_text layout="control" %}
|
||||
{% bootstrap_field form.is_public layout="control" %}
|
||||
@@ -64,7 +41,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -22,14 +22,17 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<form class="row filter-form" action="" method="get">
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.query layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.status layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.date layout='inline' %}
|
||||
{% bootstrap_field filter_form.date_from layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.date_until layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.weekday layout='inline' %}
|
||||
@@ -43,23 +46,28 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p>
|
||||
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i>
|
||||
{% trans "Create a new date" context "subevent" %}</a>
|
||||
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i>
|
||||
{% trans "Create many new dates" context "subevent" %}</a>
|
||||
</p>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i>
|
||||
{% trans "Create a new date" context "subevent" %}</a>
|
||||
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i>
|
||||
{% trans "Create many new dates" context "subevent" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="hidden">
|
||||
{{ filter_form.as_p }}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<input type="checkbox" data-toggle-table/>
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
{% endif %}
|
||||
</th>
|
||||
<th>
|
||||
@@ -67,28 +75,40 @@
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Begin" %}
|
||||
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Paid tickets per quota" %}
|
||||
<a href="?{% url_replace request 'ordering' '-sum_tickets_paid' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'sum_tickets_paid' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-sum_tickets_paid' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'sum_tickets_paid' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Status" %}
|
||||
<a href="?{% url_replace request 'ordering' '-active' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'active' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-active' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'active' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% if "can_change_event_settings" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
|
||||
<tr class="table-select-all warning hidden">
|
||||
<td>
|
||||
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
|
||||
</td>
|
||||
<td colspan="5">
|
||||
<label for="__all">
|
||||
{% trans "Select all results on other pages as well" %}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in subevents %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<input type="checkbox" name="subevent" class="" value="{{ s.pk }}"/>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label"><input type="checkbox" name="subevent" class="batch-select-checkbox" value="{{ s.pk }}"/></label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@@ -147,15 +167,21 @@
|
||||
</table>
|
||||
</div>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
<button type="submit" class="btn btn-default btn-save" name="action" value="delete">
|
||||
{% trans "Delete selected" %}
|
||||
<div class="batch-select-actions">
|
||||
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
|
||||
<i class="fa fa-trash"></i>{% trans "Delete selected" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="disable"
|
||||
formaction="{% url "control:event.subevents.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-default btn-save" name="action" value="enable">
|
||||
{% trans "Enable selected" %}
|
||||
<i class="fa fa-check"></i>{% trans "Activate selected" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-default btn-save" name="action" value="disable">
|
||||
{% trans "Disable selected" %}
|
||||
<i class="fa fa-ban"></i>{% trans "Deactivate selected" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
|
||||
@@ -77,6 +77,13 @@ urlpatterns = [
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
|
||||
name='organizer.display'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/properties$', organizer.EventMetaPropertyListView.as_view(), name='organizer.properties'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/property/add$', organizer.EventMetaPropertyCreateView.as_view(),
|
||||
name='organizer.property.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/property/(?P<property>[^/]+)/edit$', organizer.EventMetaPropertyUpdateView.as_view(),
|
||||
name='organizer.property.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/property/(?P<property>[^/]+)/delete$', organizer.EventMetaPropertyDeleteView.as_view(),
|
||||
name='organizer.property.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/giftcards$', organizer.GiftCardListView.as_view(), name='organizer.giftcards'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/add$', organizer.GiftCardCreateView.as_view(), name='organizer.giftcard.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),
|
||||
@@ -115,6 +122,7 @@ urlpatterns = [
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(),
|
||||
name='organizer.team.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/slugrng', main.SlugRNG.as_view(), name='events.add.slugrng'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/logs', organizer.LogView.as_view(), name='organizer.log'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/export/$', organizer.ExportView.as_view(), name='organizer.export'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/export/do$', organizer.ExportDoView.as_view(), name='organizer.export.do'),
|
||||
url(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
|
||||
@@ -166,6 +174,7 @@ urlpatterns = [
|
||||
url(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'),
|
||||
url(r'^subevents/bulk_add$', subevents.SubEventBulkCreate.as_view(), name='event.subevents.bulk'),
|
||||
url(r'^subevents/bulk_action$', subevents.SubEventBulkAction.as_view(), name='event.subevents.bulkaction'),
|
||||
url(r'^subevents/bulk_edit$', subevents.SubEventBulkEdit.as_view(), name='event.subevents.bulkedit'),
|
||||
url(r'^items/$', item.ItemList.as_view(), name='event.items'),
|
||||
url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'),
|
||||
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
|
||||
|
||||
@@ -91,6 +91,10 @@ class MetaDataEditorMixin:
|
||||
return self.meta_form(
|
||||
prefix='prop-{}'.format(p.pk),
|
||||
property=p,
|
||||
disabled=(
|
||||
p.protected and
|
||||
not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings', request=self.request)
|
||||
),
|
||||
instance=val_instances.get(p.pk, self.meta_model(property=p, event=self.object)),
|
||||
data=(self.request.POST if self.request.method == "POST" else None)
|
||||
)
|
||||
@@ -171,6 +175,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
||||
for k in form.changed_data
|
||||
})
|
||||
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk})
|
||||
if change_css:
|
||||
regenerate_css.apply_async(args=(self.request.event.pk,))
|
||||
messages.success(self.request, _('Your changes have been saved. Please note that it can '
|
||||
|
||||
@@ -1105,6 +1105,7 @@ class OrderTransition(OrderView):
|
||||
|
||||
try:
|
||||
p.confirm(user=self.request.user, count_waitinglist=False, payment_date=payment_date,
|
||||
send_mail=self.mark_paid_form.cleaned_data['send_email'],
|
||||
force=self.mark_paid_form.cleaned_data.get('force', False))
|
||||
except Quota.QuotaExceededException as e:
|
||||
p.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
@@ -2079,11 +2080,17 @@ class ExportMixin:
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View):
|
||||
|
||||
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
|
||||
permission = 'can_view_orders'
|
||||
known_errortypes = ['ExportError']
|
||||
task = export
|
||||
template_name = 'pretixcontrol/orders/export.html'
|
||||
|
||||
def get_success_message(self, value):
|
||||
return None
|
||||
@@ -2103,6 +2110,11 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
if ex.identifier == self.request.POST.get("exporter"):
|
||||
return ex
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return TemplateView.get(self, request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not self.exporter:
|
||||
messages.error(self.request, _('The selected exporter was not found.'))
|
||||
@@ -2112,16 +2124,8 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
}))
|
||||
|
||||
if not self.exporter.form.is_valid():
|
||||
messages.error(
|
||||
self.request,
|
||||
str(_('There was a problem processing your input:')) + ' ' + ', '.join(
|
||||
', '.join(line) for line in self.exporter.form.errors.values()
|
||||
)
|
||||
)
|
||||
return redirect(reverse('control:event.orders.export', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}) + '?identifier=' + self.exporter.identifier)
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
cf = CachedFile(web_download=True, session_key=request.session.session_key)
|
||||
cf.date = now()
|
||||
@@ -2134,11 +2138,6 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, TemplateView):
|
||||
permission = 'can_view_orders'
|
||||
template_name = 'pretixcontrol/orders/export.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
|
||||
class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = OrderRefund
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.db.models import (
|
||||
Count, Max, Min, OuterRef, Prefetch, ProtectedError, Subquery, Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.forms import DecimalField, inlineformset_factory
|
||||
from django.forms import DecimalField
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
@@ -51,6 +51,7 @@ from pretix.control.forms.organizer import (
|
||||
GiftCardUpdateForm, OrganizerDeleteForm, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
from pretix.control.permissions import (
|
||||
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
|
||||
)
|
||||
@@ -269,12 +270,10 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['sform'] = self.sform
|
||||
context['formset'] = self.formset
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
self.save_formset(self.object)
|
||||
self.sform.save()
|
||||
change_css = False
|
||||
if self.sform.has_changed():
|
||||
@@ -321,38 +320,11 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid() and self.sform.is_valid() and self.formset.is_valid():
|
||||
if form.is_valid() and self.sform.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
formsetclass = inlineformset_factory(
|
||||
Organizer, EventMetaProperty,
|
||||
form=EventMetaPropertyForm, can_order=False, can_delete=True, extra=0
|
||||
)
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
instance=self.object, queryset=self.object.meta_properties.all())
|
||||
|
||||
def save_formset(self, obj):
|
||||
for form in self.formset.initial_forms:
|
||||
if form in self.formset.deleted_forms:
|
||||
if not form.instance.pk:
|
||||
continue
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
elif form.has_changed():
|
||||
form.save()
|
||||
|
||||
for form in self.formset.extra_forms:
|
||||
if not form.has_changed():
|
||||
continue
|
||||
if self.formset._should_delete_form(form):
|
||||
continue
|
||||
form.instance.organizer = obj
|
||||
form.save()
|
||||
|
||||
|
||||
class OrganizerCreate(CreateView):
|
||||
model = Organizer
|
||||
@@ -395,7 +367,7 @@ class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, L
|
||||
memcount=Count('members', distinct=True),
|
||||
eventcount=Count('limit_events', distinct=True),
|
||||
invcount=Count('invites', distinct=True)
|
||||
).all()
|
||||
).all().order_by('name')
|
||||
|
||||
|
||||
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
@@ -1176,8 +1148,9 @@ class ExportMixin:
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
id = self.request.GET.get("identifier") or self.request.POST.get("exporter")
|
||||
for ex in sorted([response(events) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
|
||||
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"):
|
||||
if id and ex.identifier != id:
|
||||
continue
|
||||
|
||||
# Use form parse cycle to generate useful defaults
|
||||
@@ -1209,10 +1182,16 @@ class ExportMixin:
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, View):
|
||||
|
||||
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
|
||||
known_errortypes = ['ExportError']
|
||||
task = multiexport
|
||||
template_name = 'pretixcontrol/organizers/export.html'
|
||||
|
||||
def get_success_message(self, value):
|
||||
return None
|
||||
@@ -1231,6 +1210,11 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
|
||||
if ex.identifier == self.request.POST.get("exporter"):
|
||||
return ex
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return TemplateView.get(self, request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not self.exporter:
|
||||
messages.error(self.request, _('The selected exporter was not found.'))
|
||||
@@ -1260,11 +1244,6 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
|
||||
class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/organizers/export.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
|
||||
class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = Gate
|
||||
@@ -1365,3 +1344,115 @@ class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected gate has been deleted.'))
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
class EventMetaPropertyListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = EventMetaProperty
|
||||
template_name = 'pretixcontrol/organizers/properties.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'properties'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.meta_properties.all()
|
||||
|
||||
|
||||
class EventMetaPropertyCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = EventMetaProperty
|
||||
template_name = 'pretixcontrol/organizers/property_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
form_class = EventMetaPropertyForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.properties', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The property has been created.'))
|
||||
form.instance.organizer = self.request.organizer
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.property.created', user=self.request.user, data={
|
||||
k: getattr(self.object, k) for k in form.changed_data
|
||||
})
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = EventMetaProperty
|
||||
template_name = 'pretixcontrol/organizers/property_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'property'
|
||||
form_class = EventMetaPropertyForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.properties', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
self.object.log_action('pretix.property.changed', user=self.request.user, data={
|
||||
k: getattr(self.object, k)
|
||||
for k in form.changed_data
|
||||
})
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
||||
model = EventMetaProperty
|
||||
template_name = 'pretixcontrol/organizers/property_delete.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'property'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.properties', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
success_url = self.get_success_url()
|
||||
self.object = self.get_object()
|
||||
self.object.log_action('pretix.property.deleted', user=self.request.user)
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected property has been deleted.'))
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
class LogView(OrganizerPermissionRequiredMixin, ListView):
|
||||
template_name = 'pretixcontrol/organizers/logs.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
model = LogEntry
|
||||
context_object_name = 'logs'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.all_logentries().select_related(
|
||||
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
||||
).order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
||||
if self.request.GET.get('user'):
|
||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
return ctx
|
||||
|
||||
@@ -22,7 +22,7 @@ from reportlab.lib.units import mm
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
|
||||
from pretix.base.pdf import get_variables
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
@@ -211,11 +211,15 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
def get_variables(self):
|
||||
return get_variables(self.request.event)
|
||||
|
||||
def get_images(self):
|
||||
return get_images(self.request.event)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['fonts'] = get_fonts()
|
||||
ctx['pdf'] = self.get_current_background()
|
||||
ctx['variables'] = self.get_variables()
|
||||
ctx['images'] = self.get_images()
|
||||
ctx['layout'] = json.dumps(self.get_current_layout())
|
||||
ctx['title'] = self.title
|
||||
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
|
||||
from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.db import connections, transaction
|
||||
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import (
|
||||
Count, F, IntegerField, OuterRef, Prefetch, Subquery, Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, TruncDate, TruncTime
|
||||
from django.forms import inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import get_format
|
||||
@@ -16,7 +19,9 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
from django.views.generic import (
|
||||
CreateView, DeleteView, FormView, ListView, UpdateView,
|
||||
)
|
||||
|
||||
from pretix.base.models import CartPosition, LogEntry
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
@@ -25,29 +30,33 @@ from pretix.base.models.items import (
|
||||
ItemVariation, Quota, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.control.forms.checkin import SimpleCheckinListForm
|
||||
from pretix.control.forms.filter import SubEventFilterForm
|
||||
from pretix.control.forms.item import QuotaForm
|
||||
from pretix.control.forms.subevents import (
|
||||
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkForm,
|
||||
SubEventForm, SubEventItemForm, SubEventItemVariationForm,
|
||||
SubEventMetaValueForm, TimeFormSet,
|
||||
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkEditForm,
|
||||
SubEventBulkForm, SubEventForm, SubEventItemForm,
|
||||
SubEventItemVariationForm, SubEventMetaValueForm, TimeFormSet,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import subevent_forms
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.control.views.event import MetaDataEditorMixin
|
||||
from pretix.helpers import GroupConcat
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
|
||||
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = SubEvent
|
||||
context_object_name = 'subevents'
|
||||
template_name = 'pretixcontrol/subevents/index.html'
|
||||
permission = 'can_change_settings'
|
||||
class SubEventQueryMixin:
|
||||
|
||||
def get_queryset(self):
|
||||
@cached_property
|
||||
def request_data(self):
|
||||
if self.request.method == "POST":
|
||||
return self.request.POST
|
||||
return self.request.GET
|
||||
|
||||
def get_queryset(self, list=False):
|
||||
sum_tickets_paid = Quota.objects.filter(
|
||||
subevent=OuterRef('pk')
|
||||
).order_by().values('subevent').annotate(
|
||||
@@ -55,18 +64,39 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
).values(
|
||||
's'
|
||||
)
|
||||
|
||||
qs = self.request.event.subevents.annotate(
|
||||
sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField())
|
||||
).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
|
||||
to_attr='first_quotas')
|
||||
)
|
||||
qs = self.request.event.subevents
|
||||
if list:
|
||||
qs = qs.annotate(
|
||||
sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField())
|
||||
).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
|
||||
to_attr='first_quotas')
|
||||
)
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
if 'subevent' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('subevent')
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return SubEventFilterForm(data=self.request_data, prefix='filter')
|
||||
|
||||
|
||||
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryMixin, ListView):
|
||||
model = SubEvent
|
||||
context_object_name = 'subevents'
|
||||
template_name = 'pretixcontrol/subevents/index.html'
|
||||
permission = 'can_change_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset(True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
@@ -94,10 +124,6 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
)
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return SubEventFilterForm(data=self.request.GET)
|
||||
|
||||
|
||||
class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = SubEvent
|
||||
@@ -166,6 +192,10 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
return self.meta_form(
|
||||
prefix='prop-{}'.format(p.pk),
|
||||
property=p,
|
||||
disabled=(
|
||||
p.protected and
|
||||
not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings', request=self.request)
|
||||
),
|
||||
default=self._default_meta.get(p.name, ''),
|
||||
instance=val_instances.get(p.pk, self.meta_model(property=p, subevent=self.object)),
|
||||
data=(self.request.POST if self.request.method == "POST" else None)
|
||||
@@ -428,6 +458,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
|
||||
for f in self.plugin_forms:
|
||||
f.subevent = self.object
|
||||
f.save()
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk})
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
@@ -529,19 +560,13 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
|
||||
return formlist
|
||||
|
||||
|
||||
class SubEventBulkAction(EventPermissionRequiredMixin, View):
|
||||
class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_settings'
|
||||
|
||||
@cached_property
|
||||
def objects(self):
|
||||
return self.request.event.subevents.filter(
|
||||
id__in=self.request.POST.getlist('subevent')
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get('action') == 'disable':
|
||||
for obj in self.objects:
|
||||
for obj in self.get_queryset():
|
||||
obj.log_action(
|
||||
'pretix.subevent.changed', user=self.request.user, data={
|
||||
'active': False
|
||||
@@ -551,7 +576,7 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
|
||||
obj.save(update_fields=['active'])
|
||||
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.'))
|
||||
elif request.POST.get('action') == 'enable':
|
||||
for obj in self.objects:
|
||||
for obj in self.get_queryset():
|
||||
obj.log_action(
|
||||
'pretix.subevent.changed', user=self.request.user, data={
|
||||
'active': True
|
||||
@@ -562,11 +587,11 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
|
||||
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.'))
|
||||
elif request.POST.get('action') == 'delete':
|
||||
return render(request, 'pretixcontrol/subevents/delete_bulk.html', {
|
||||
'allowed': self.objects.filter(orderposition__isnull=True),
|
||||
'forbidden': self.objects.filter(orderposition__isnull=False),
|
||||
'allowed': self.get_queryset().filter(orderposition__isnull=True),
|
||||
'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(),
|
||||
})
|
||||
elif request.POST.get('action') == 'delete_confirm':
|
||||
for obj in self.objects:
|
||||
for obj in self.get_queryset():
|
||||
if obj.allow_delete():
|
||||
CartPosition.objects.filter(addon_to__subevent=obj).delete()
|
||||
obj.cartposition_set.all().delete()
|
||||
@@ -893,3 +918,537 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormView):
|
||||
permission = 'can_change_settings'
|
||||
form_class = SubEventBulkEditForm
|
||||
template_name = 'pretixcontrol/subevents/bulk_edit.html'
|
||||
context_object_name = 'subevent'
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related(None).order_by()
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.subevents', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse(status=405)
|
||||
|
||||
@cached_property
|
||||
def cached_num(self):
|
||||
return self.get_queryset().count()
|
||||
|
||||
@cached_property
|
||||
def itemvar_forms(self):
|
||||
matches = defaultdict(list)
|
||||
for sei in SubEventItem.objects.filter(
|
||||
subevent__in=self.get_queryset()
|
||||
).order_by().values('item', 'price', 'disabled').annotate(c=Count('*')):
|
||||
matches['item', sei['item']].append(sei)
|
||||
for sei in SubEventItemVariation.objects.filter(
|
||||
subevent__in=self.get_queryset()
|
||||
).order_by().values('variation', 'price', 'disabled').annotate(c=Count('*')):
|
||||
matches['variation', sei['variation']].append(sei)
|
||||
total = self.cached_num
|
||||
|
||||
formlist = []
|
||||
for i in self.request.event.items.filter(active=True).prefetch_related('variations'):
|
||||
if i.has_variations:
|
||||
for v in i.variations.all():
|
||||
m = matches['variation', v.pk]
|
||||
if m and len(m) == 1 and m[0]['c'] == total:
|
||||
inst = SubEventItemVariation(variation=v, disabled=m[0]['disabled'], price=m[0]['price'])
|
||||
else:
|
||||
inst = SubEventItemVariation(variation=v)
|
||||
formlist.append(SubEventItemVariationForm(
|
||||
prefix='itemvar-{}'.format(v.pk),
|
||||
item=i, variation=v,
|
||||
instance=inst,
|
||||
data=(self.request.POST if self.is_submitted else None)
|
||||
))
|
||||
else:
|
||||
m = matches['item', i.pk]
|
||||
if m and len(m) == 1 and m[0]['c'] == total:
|
||||
inst = SubEventItem(item=i, disabled=m[0]['disabled'], price=m[0]['price'])
|
||||
else:
|
||||
inst = SubEventItem(item=i)
|
||||
formlist.append(SubEventItemForm(
|
||||
prefix='item-{}'.format(i.pk),
|
||||
item=i,
|
||||
instance=inst,
|
||||
data=(self.request.POST if self.is_submitted else None)
|
||||
))
|
||||
return formlist
|
||||
|
||||
@cached_property
|
||||
def meta_forms(self):
|
||||
matches = defaultdict(list)
|
||||
for smv in SubEventMetaValue.objects.filter(
|
||||
subevent__in=self.get_queryset()
|
||||
).order_by().values('property', 'value').annotate(c=Count('*')):
|
||||
matches[smv['property']].append(smv)
|
||||
total = self.cached_num
|
||||
|
||||
formlist = []
|
||||
|
||||
if not hasattr(self, '_default_meta'):
|
||||
self._default_meta = self.request.event.meta_data
|
||||
|
||||
for p in self.request.organizer.meta_properties.all():
|
||||
inst = SubEventMetaValue(property=p)
|
||||
if len(matches[p.id]) == 1 and matches[p.id][0]['c'] == total:
|
||||
inst.value = matches[p.id][0]['value']
|
||||
formlist.append(SubEventMetaValueForm(
|
||||
prefix='prop-{}'.format(p.pk),
|
||||
property=p,
|
||||
default=self._default_meta.get(p.name, ''),
|
||||
instance=inst,
|
||||
data=(self.request.POST if self.is_submitted else None)
|
||||
))
|
||||
return formlist
|
||||
|
||||
@cached_property
|
||||
def quota_formset(self):
|
||||
extra = 0
|
||||
kwargs = {}
|
||||
|
||||
if self.sampled_quotas is not None:
|
||||
kwargs['instance'] = self.get_queryset()[0]
|
||||
|
||||
formsetclass = inlineformset_factory(
|
||||
SubEvent, Quota,
|
||||
form=QuotaForm, formset=QuotaFormSet, min_num=0, validate_min=False,
|
||||
can_order=False, can_delete=True, extra=extra,
|
||||
)
|
||||
return formsetclass(
|
||||
self.request.POST if self.is_submitted else None,
|
||||
event=self.request.event, **kwargs
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def list_formset(self):
|
||||
extra = 0
|
||||
kwargs = {}
|
||||
|
||||
if self.sampled_lists is not None:
|
||||
kwargs['instance'] = self.get_queryset()[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
formsetclass = inlineformset_factory(
|
||||
SubEvent, CheckinList,
|
||||
form=SimpleCheckinListForm, formset=CheckinListFormSet, min_num=0, validate_min=False,
|
||||
can_order=False, can_delete=True, extra=extra,
|
||||
)
|
||||
return formsetclass(
|
||||
self.request.POST if self.is_submitted else None,
|
||||
event=self.request.event, **kwargs
|
||||
)
|
||||
|
||||
def save_list_formset(self, log_entries):
|
||||
if not self.list_formset.has_changed() or self.sampled_lists is None:
|
||||
return
|
||||
qidx = 0
|
||||
subevents = list(self.get_queryset().prefetch_related('checkinlist_set'))
|
||||
to_save_products = []
|
||||
to_save_gates = []
|
||||
|
||||
for f in self.list_formset.forms:
|
||||
if self.list_formset._should_delete_form(f) and f in self.list_formset.extra_forms:
|
||||
continue
|
||||
|
||||
if self.list_formset._should_delete_form(f):
|
||||
for se in subevents:
|
||||
q = list(se.checkinlist_set.all())[qidx]
|
||||
log_entries += [
|
||||
q.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user, save=False),
|
||||
]
|
||||
q.delete()
|
||||
elif f in self.list_formset.extra_forms:
|
||||
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
|
||||
for se in subevents:
|
||||
q = copy.copy(f.instance)
|
||||
q.pk = None
|
||||
q.subevent = se
|
||||
q.event = self.request.event
|
||||
q.save()
|
||||
for _i in f.cleaned_data.get('limit_products', []):
|
||||
to_save_products.append(CheckinList.limit_products.through(checkinlist_id=q.pk, item_id=_i.pk))
|
||||
for _i in f.cleaned_data.get('gates', []):
|
||||
to_save_gates.append(CheckinList.gates.through(checkinlist_id=q.pk, gate_id=_i.pk))
|
||||
change_data['id'] = q.pk
|
||||
log_entries.append(
|
||||
q.log_action(action='pretix.event.checkinlist.added', user=self.request.user,
|
||||
data=change_data, save=False)
|
||||
)
|
||||
else:
|
||||
if f.changed_data:
|
||||
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
|
||||
for se in subevents:
|
||||
q = list(se.checkinlist_set.all())[qidx]
|
||||
for fname in ('name', 'all_products', 'include_pending', 'allow_entry_after_exit'):
|
||||
setattr(q, fname, f.cleaned_data.get(fname))
|
||||
q.save()
|
||||
if 'limit_products' in f.changed_data:
|
||||
q.limit_products.set(f.cleaned_data.get('limit_products', []))
|
||||
if 'gates' in f.changed_data:
|
||||
q.gates.set(f.cleaned_data.get('limit_products', []))
|
||||
log_entries.append(
|
||||
q.log_action(action='pretix.event.checkinlist.changed', user=self.request.user,
|
||||
data=change_data, save=False)
|
||||
)
|
||||
qidx += 1
|
||||
if to_save_products:
|
||||
CheckinList.limit_products.through.objects.bulk_create(to_save_products)
|
||||
if to_save_gates:
|
||||
CheckinList.gates.through.objects.bulk_create(to_save_gates)
|
||||
|
||||
def save_quota_formset(self, log_entries):
|
||||
if not self.quota_formset.has_changed():
|
||||
return
|
||||
qidx = 0
|
||||
subevents = list(self.get_queryset().prefetch_related('quotas'))
|
||||
to_save_items = []
|
||||
to_save_variations = []
|
||||
to_delete_quota_ids = []
|
||||
|
||||
if self.sampled_quotas is None:
|
||||
if len(self.quota_formset.forms) == 0:
|
||||
return
|
||||
else:
|
||||
for se in subevents:
|
||||
for q in se.quotas.all():
|
||||
to_delete_quota_ids.append(q.pk)
|
||||
log_entries += [
|
||||
q.log_action(action='pretix.event.quota.deleted', user=self.request.user, save=False),
|
||||
se.log_action('pretix.subevent.quota.deleted', user=self.request.user, data={
|
||||
'id': q.pk
|
||||
}, save=False)
|
||||
]
|
||||
|
||||
if to_delete_quota_ids:
|
||||
Quota.objects.filter(id__in=to_delete_quota_ids).delete()
|
||||
|
||||
for f in self.quota_formset.forms:
|
||||
if self.quota_formset._should_delete_form(f) and f in self.quota_formset.extra_forms:
|
||||
continue
|
||||
|
||||
selected_items = set(list(self.request.event.items.filter(id__in=[
|
||||
i.split('-')[0] for i in f.cleaned_data.get('itemvars', [])
|
||||
])))
|
||||
selected_variations = list(ItemVariation.objects.filter(item__event=self.request.event, id__in=[
|
||||
i.split('-')[1] for i in f.cleaned_data.get('itemvars', []) if '-' in i
|
||||
]))
|
||||
|
||||
if self.quota_formset._should_delete_form(f):
|
||||
for se in subevents:
|
||||
q = list(se.quotas.all())[qidx]
|
||||
log_entries += [
|
||||
q.log_action(action='pretix.event.quota.deleted', user=self.request.user, save=False),
|
||||
se.log_action('pretix.subevent.quota.deleted', user=self.request.user, data={
|
||||
'id': q.pk
|
||||
}, save=False)
|
||||
]
|
||||
q.delete()
|
||||
elif f in self.quota_formset.extra_forms:
|
||||
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
|
||||
for se in subevents:
|
||||
q = copy.copy(f.instance)
|
||||
q.pk = None
|
||||
q.subevent = se
|
||||
q.event = self.request.event
|
||||
q.save(clear_cache=False)
|
||||
for _i in selected_items:
|
||||
to_save_items.append(Quota.items.through(quota_id=q.pk, item_id=_i.pk))
|
||||
for _i in selected_variations:
|
||||
to_save_variations.append(Quota.variations.through(quota_id=q.pk, itemvariation_id=_i.pk))
|
||||
|
||||
change_data['id'] = q.pk
|
||||
log_entries.append(
|
||||
q.log_action(action='pretix.event.quota.added', user=self.request.user,
|
||||
data=change_data, save=False)
|
||||
)
|
||||
log_entries.append(
|
||||
se.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data,
|
||||
save=False)
|
||||
)
|
||||
else:
|
||||
if f.changed_data:
|
||||
change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
|
||||
for se in subevents:
|
||||
q = list(se.quotas.all())[qidx]
|
||||
for fname in ('size', 'name', 'release_after_exit'):
|
||||
setattr(q, fname, f.cleaned_data.get(fname))
|
||||
q.save(clear_cache=False)
|
||||
if 'itemvar' in f.changed_data:
|
||||
q.items.set(selected_items)
|
||||
q.variations.set(selected_variations)
|
||||
log_entries.append(
|
||||
q.log_action(action='pretix.event.quota.added', user=self.request.user,
|
||||
data=change_data, save=False)
|
||||
)
|
||||
qidx += 1
|
||||
if to_save_items:
|
||||
Quota.items.through.objects.bulk_create(to_save_items)
|
||||
if to_save_variations:
|
||||
Quota.variations.through.objects.bulk_create(to_save_variations)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['subevents'] = self.get_queryset()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['sampled_quotas'] = self.sampled_quotas
|
||||
ctx['sampled_lists'] = self.sampled_lists
|
||||
ctx['formset'] = self.quota_formset
|
||||
ctx['cl_formset'] = self.list_formset
|
||||
ctx['itemvar_forms'] = self.itemvar_forms
|
||||
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def sampled_quotas(self):
|
||||
all_quotas = Quota.objects.filter(
|
||||
subevent__in=self.get_queryset()
|
||||
).annotate(
|
||||
item_list=GroupConcat('items__id'),
|
||||
var_list=GroupConcat('variations__id'),
|
||||
).values(
|
||||
'item_list', 'var_list',
|
||||
*(f.name for f in Quota._meta.fields if f.name not in (
|
||||
'id', 'event', 'items', 'variations', 'cached_availability_state', 'cached_availability_number',
|
||||
'cached_availability_paid_orders', 'cached_availability_time', 'closed',
|
||||
))
|
||||
).order_by('subevent_id')
|
||||
|
||||
if not all_quotas:
|
||||
return Quota.objects.none()
|
||||
|
||||
quotas_by_subevent = defaultdict(list)
|
||||
for q in all_quotas:
|
||||
quotas_by_subevent[q.pop('subevent')].append(q)
|
||||
|
||||
prev = None
|
||||
for se in self.get_queryset():
|
||||
if se.pk not in quotas_by_subevent:
|
||||
return None
|
||||
|
||||
if prev is None:
|
||||
prev = quotas_by_subevent[se.pk]
|
||||
|
||||
if quotas_by_subevent[se.pk] != prev:
|
||||
return None
|
||||
return se.quotas.all()
|
||||
|
||||
@cached_property
|
||||
def sampled_lists(self):
|
||||
all_lists = CheckinList.objects.filter(
|
||||
subevent__in=self.get_queryset()
|
||||
).annotate(
|
||||
item_list=GroupConcat('limit_products__id'),
|
||||
gates_list=GroupConcat('gates__id'),
|
||||
).values(
|
||||
'item_list', 'gates_list',
|
||||
*(f.name for f in CheckinList._meta.fields if f.name not in (
|
||||
'id', 'event', 'limit_products', 'gates',
|
||||
))
|
||||
).order_by('subevent_id')
|
||||
|
||||
if not all_lists:
|
||||
return SubEvent.objects.none()
|
||||
|
||||
lists_by_subevent = defaultdict(list)
|
||||
for cl in all_lists:
|
||||
lists_by_subevent[cl.pop('subevent')].append(cl)
|
||||
|
||||
prev = None
|
||||
for se in self.get_queryset():
|
||||
if se.pk not in lists_by_subevent:
|
||||
return None
|
||||
|
||||
if prev is None:
|
||||
prev = lists_by_subevent[se.pk]
|
||||
|
||||
if lists_by_subevent[se.pk] != prev:
|
||||
return None
|
||||
return se.checkinlist_set.all()
|
||||
|
||||
@cached_property
|
||||
def is_submitted(self):
|
||||
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
|
||||
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
|
||||
# that behaviour
|
||||
return '_bulk' in self.request.POST
|
||||
|
||||
def get_form_kwargs(self):
|
||||
initial = {}
|
||||
mixed_values = set()
|
||||
qs = self.get_queryset()
|
||||
|
||||
qs = qs.annotate(
|
||||
**{
|
||||
# TODO: Once we're on Django 3.2, pass a tzinfo parameter
|
||||
# Before Django 3.2, it uses the current timezone, which is hopefully fine
|
||||
# as well in all cases we are concerned about
|
||||
# See also: https://code.djangoproject.com/ticket/31948
|
||||
k + '_day': TruncDate(k)
|
||||
for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end')
|
||||
},
|
||||
**{
|
||||
k + '_time': TruncTime(k)
|
||||
for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end')
|
||||
},
|
||||
)
|
||||
|
||||
fields = {
|
||||
'name',
|
||||
'location',
|
||||
'frontpage_text',
|
||||
'geo_lat',
|
||||
'geo_lon',
|
||||
'is_public',
|
||||
'active',
|
||||
'date_from_day',
|
||||
'date_from_time',
|
||||
'date_to_day',
|
||||
'date_to_time',
|
||||
'date_admission_day',
|
||||
'date_admission_time',
|
||||
'presale_start_day',
|
||||
'presale_start_time',
|
||||
'presale_end_day',
|
||||
'presale_end_time',
|
||||
}
|
||||
for k in fields:
|
||||
existing_values = list(qs.order_by(k).values(k).annotate(c=Count('*')))
|
||||
if len(existing_values) == 1:
|
||||
initial[k] = existing_values[0][k]
|
||||
elif len(existing_values) > 1:
|
||||
mixed_values.add(k)
|
||||
initial[k] = None
|
||||
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
kwargs['prefix'] = 'bulkedit'
|
||||
kwargs['initial'] = initial
|
||||
kwargs['queryset'] = self.get_queryset()
|
||||
kwargs['mixed_values'] = mixed_values
|
||||
if not self.is_submitted:
|
||||
kwargs['data'] = None
|
||||
kwargs['files'] = None
|
||||
return kwargs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
is_valid = (
|
||||
self.is_submitted and
|
||||
form.is_valid() and
|
||||
self.quota_formset.is_valid() and
|
||||
(not self.list_formset or self.list_formset.is_valid()) and
|
||||
all(f.is_valid() for f in self.itemvar_forms)and
|
||||
all(f.is_valid() for f in self.meta_forms)
|
||||
)
|
||||
if is_valid:
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
if self.is_submitted:
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return self.form_invalid(form)
|
||||
|
||||
def save_meta(self):
|
||||
for f in self.meta_forms:
|
||||
if f.prefix + 'value' not in self.request.POST.getlist('_bulk'):
|
||||
continue
|
||||
|
||||
if f.cleaned_data.get('value'):
|
||||
for obj in self.get_queryset():
|
||||
SubEventMetaValue.objects.update_or_create(
|
||||
property=f.instance.property,
|
||||
subevent=obj,
|
||||
defaults={
|
||||
'value': f.cleaned_data['value']
|
||||
}
|
||||
)
|
||||
else:
|
||||
SubEventMetaValue.objects.filter(
|
||||
property=f.instance.property,
|
||||
subevent__in=self.get_queryset()
|
||||
).delete()
|
||||
|
||||
def save_itemvars(self):
|
||||
for f in self.itemvar_forms:
|
||||
u = {}
|
||||
if f.prefix + 'price' in self.request.POST.getlist('_bulk'):
|
||||
u['price'] = f.cleaned_data.get('price')
|
||||
if f.prefix + 'disabled' in self.request.POST.getlist('_bulk'):
|
||||
u['disabled'] = f.cleaned_data.get('disabled')
|
||||
|
||||
if not u:
|
||||
continue
|
||||
|
||||
if isinstance(f, SubEventItemForm):
|
||||
if u.get('price') is None and not u.get('disabled'):
|
||||
SubEventItem.objects.filter(
|
||||
subevent__in=self.get_queryset(),
|
||||
item=f.instance.item,
|
||||
).delete()
|
||||
else:
|
||||
for obj in self.get_queryset():
|
||||
SubEventItem.objects.update_or_create(
|
||||
subevent=obj,
|
||||
item=f.instance.item,
|
||||
defaults=u
|
||||
)
|
||||
elif isinstance(f, SubEventItemVariationForm):
|
||||
if u.get('price') is None and not u.get('disabled'):
|
||||
SubEventItemVariation.objects.filter(
|
||||
subevent__in=self.get_queryset(),
|
||||
variation=f.instance.variation,
|
||||
).delete()
|
||||
else:
|
||||
for obj in self.get_queryset():
|
||||
SubEventItemVariation.objects.update_or_create(
|
||||
subevent=obj,
|
||||
variation=f.instance.variation,
|
||||
defaults=u
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form):
|
||||
log_entries = []
|
||||
|
||||
# Main form
|
||||
form.save()
|
||||
data = {
|
||||
k: v for k, v in form.cleaned_data.items() if k in form.changed_data
|
||||
}
|
||||
data['_raw_bulk_data'] = self.request.POST.dict()
|
||||
for obj in self.get_queryset():
|
||||
log_entries.append(
|
||||
obj.log_action('pretix.subevent.changed', data=data, user=self.request.user, save=False)
|
||||
)
|
||||
|
||||
# Formsets
|
||||
if '__quotas' in self.request.POST.getlist('_bulk'):
|
||||
self.save_quota_formset(log_entries)
|
||||
if '__checkinlists' in self.request.POST.getlist('_bulk'):
|
||||
self.save_list_formset(log_entries)
|
||||
|
||||
self.save_itemvars()
|
||||
self.save_meta()
|
||||
|
||||
if connections['default'].features.can_return_rows_from_bulk_insert:
|
||||
LogEntry.objects.bulk_create(log_entries, batch_size=200)
|
||||
LogEntry.bulk_postprocess(log_entries)
|
||||
else:
|
||||
for le in log_entries:
|
||||
le.save()
|
||||
LogEntry.bulk_postprocess(log_entries)
|
||||
|
||||
self.request.event.cache.clear()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
@@ -67,7 +67,8 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
|
||||
headers = [
|
||||
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
|
||||
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat')
|
||||
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
|
||||
_('Comment')
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
@@ -92,7 +93,8 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
v.tag,
|
||||
str(v.redeemed),
|
||||
str(v.max_usages),
|
||||
str(v.seat) if v.seat else ""
|
||||
str(v.seat) if v.seat else "",
|
||||
str(v.comment) if v.comment else ""
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
22
src/pretix/helpers/reportlab.py
Normal file
22
src/pretix/helpers/reportlab.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib.utils import ImageReader
|
||||
|
||||
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"PO-Revision-Date: 2020-07-30 19:00+0000\n"
|
||||
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -62,7 +62,7 @@ msgid "Contacting your bank …"
|
||||
msgstr "الاتصال البنك الذي تتعامل معه ..."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:116
|
||||
#: pretix/static/pretixbase/js/asynctask.js:117
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -75,7 +75,7 @@ msgstr ""
|
||||
"يستغرق ما يصل الى بضع دقائق."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:121
|
||||
#: pretix/static/pretixbase/js/asynctask.js:122
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -86,7 +86,7 @@ msgstr ""
|
||||
"يستغرق ما يصل الى بضع دقائق."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:127
|
||||
#: pretix/static/pretixbase/js/asynctask.js:128
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -96,35 +96,35 @@ msgstr ""
|
||||
"وقتا أطول من دقيقتين، يرجى الاتصال بنا أو العودة في المتصفح الخاص بك وحاول "
|
||||
"مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:86
|
||||
#: pretix/static/pretixbase/js/asynctask.js:159
|
||||
#: pretix/static/pretixbase/js/asynctask.js:164
|
||||
#: pretix/static/pretixbase/js/asynctask.js:87
|
||||
#: pretix/static/pretixbase/js/asynctask.js:169
|
||||
#: pretix/static/pretixbase/js/asynctask.js:174
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "حدث خطأ من نوع {كود}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr "لم نستطع معالجة طلبك، ولكننا نواصل المحاولة. رمز الخطأ : {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:141
|
||||
#: pretix/static/pretixbase/js/asynctask.js:142
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "استغرقت العملية فترة طويلة، الرجاء المحاولة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:167
|
||||
#: pretix/static/pretixbase/js/asynctask.js:177
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr "لم نستطع معالجة طلبك، ولكننا نواصل المحاولة. رمز الخطأ : {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:189
|
||||
#: pretix/static/pretixbase/js/asynctask.js:199
|
||||
msgid "We are processing your request …"
|
||||
msgstr "جاري معالجة طلبك …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:197
|
||||
#: pretix/static/pretixbase/js/asynctask.js:207
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -133,7 +133,7 @@ msgstr ""
|
||||
"يجري الآن معالجة طلبك، اذا أخذت العملية أكثر من دقيقة، يرجى التحقق من اتصالك "
|
||||
"بالإنترنت ثم حاول مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:235
|
||||
#: pretix/static/pretixbase/js/asynctask.js:245
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:34
|
||||
msgid "Close message"
|
||||
msgstr "رسالة ثيقة"
|
||||
@@ -215,56 +215,62 @@ msgid "Tolerance (minutes)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:237
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:441
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:444
|
||||
msgid "Add condition"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:43
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:69
|
||||
msgid "Lead Scan QR"
|
||||
msgstr "يؤدي مسح QR"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:45
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:71
|
||||
msgid "Check-in QR"
|
||||
msgstr "تحقق في QR"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:269
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:311
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:461
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:517
|
||||
msgid "Group of objects"
|
||||
msgstr "مجموعة من الكائنات"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:467
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:523
|
||||
msgid "Text object"
|
||||
msgstr "كائن النص"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:469
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:525
|
||||
msgid "Barcode area"
|
||||
msgstr "منطقة الباركود"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:471
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:527
|
||||
#, fuzzy
|
||||
#| msgid "Barcode area"
|
||||
msgid "Image area"
|
||||
msgstr "منطقة الباركود"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:529
|
||||
msgid "Powered by pretix"
|
||||
msgstr "مدعوم من pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:473
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:531
|
||||
msgid "Object"
|
||||
msgstr "موضوع"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:477
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:535
|
||||
msgid "Ticket design"
|
||||
msgstr "تصميم تذكرة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:734
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:808
|
||||
msgid "Saving failed."
|
||||
msgstr "فشل الادخار."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:783
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:821
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:857
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:896
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr "خطأ أثناء تحميل ملف PDF الخاصة بك، يرجى المحاولة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:806
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:881
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr "هل تريد حقا أن تترك المحرر دون حفظ التغييرات؟"
|
||||
|
||||
@@ -302,15 +308,15 @@ msgstr "الكل"
|
||||
msgid "None"
|
||||
msgstr "لا شيء"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:801
|
||||
msgid "Use a different name internally"
|
||||
msgstr "استخدام اسم مختلف داخليا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
||||
msgid "Click to close"
|
||||
msgstr "انقر لقريب"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:873
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user