Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
8a3a6a471e draft 2021-02-05 15:31:46 +01:00
210 changed files with 48541 additions and 61045 deletions

View File

@@ -1,23 +0,0 @@
---
name: Bug report
about: Please only create issues for bug reports. Feature requests or general questions
should start as a "Discussion" on GitHub.
title: ''
labels: ''
assignees: ''
---
<!-- Please only create issues for bug reports. Feature requests or general questions should start as a "Discussion" on GitHub. -->
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -33,7 +33,7 @@ jobs:
- name: Install system packages
run: sudo apt update && sudo apt install enchant hunspell aspell-en
- name: Install Dependencies
run: pip3 install -Ur doc/requirements.txt
run: pip3 install --no-use-pep517 -Ur doc/requirements.txt
- name: Spellcheck docs
run: make spelling
working-directory: ./doc

View File

@@ -31,7 +31,7 @@ jobs:
- name: Install system packages
run: sudo apt update && sudo apt install gettext
- name: Install Dependencies
run: pip3 install -Ur src/requirements.txt
run: pip3 install --no-use-pep517 -Ur src/requirements.txt
- name: Compile messages
run: python manage.py compilemessages
working-directory: ./src
@@ -56,7 +56,7 @@ jobs:
- name: Install system packages
run: sudo apt update && sudo apt install enchant hunspell hunspell-de-de aspell-en aspell-de
- name: Install Dependencies
run: pip3 install -Ur src/requirements/dev.txt
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
- name: Spellcheck translations
run: potypo
working-directory: ./src

View File

@@ -29,7 +29,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -Ur src/requirements/dev.txt
run: pip3 install --no-use-pep517 -Ur src/requirements/dev.txt
- name: Run isort
run: isort -c .
working-directory: ./src
@@ -49,7 +49,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt
- name: Run flake8
run: flake8 .
working-directory: ./src

View File

@@ -57,7 +57,7 @@ jobs:
- name: Install system dependencies
run: sudo apt update && sudo apt install gettext mysql-client
- name: Install Python dependencies
run: pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
run: pip3 install -r src/requirements.txt --no-use-pep517 -Ur src/requirements/dev.txt mysqlclient psycopg2-binary
- name: Run checks
run: python manage.py check
working-directory: ./src

View File

@@ -2,8 +2,9 @@
file=/tmp/supervisor.sock
[supervisord]
logfile=/dev/stdout
logfile_maxbytes=0
logfile=/tmp/supervisord.log
logfile_maxbytes=50MB
logfile_backups=10
loglevel=info
pidfile=/tmp/supervisord.pid
nodaemon=false

View File

@@ -3,7 +3,5 @@ command=/usr/sbin/nginx
autostart=true
autorestart=true
priority=10
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
stdout_events_enabled=true
stderr_events_enabled=true

View File

@@ -4,7 +4,3 @@ autostart=true
autorestart=true
priority=5
user=pretixuser
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0

View File

@@ -5,7 +5,3 @@ autorestart=true
priority=5
user=pretixuser
environment=HOME=/pretix
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0

View File

@@ -8,5 +8,4 @@ This part of the documentation contains how-to guides on some special use cases
.. toctree::
:maxdepth: 2
order_lifecycle
custom_checkout

View File

@@ -1,56 +0,0 @@
Understanding the life cycle of orders
======================================
When integrating pretix with other systems, it is important that you understand how orders and related objects
such as order positions, fees, payments, refunds, and invoices work together, in order to react to their changes
properly and map them to processes in your system.
Order states
------------
Generally, an order can be in six states. For compatibility reasons, the ``status`` field only allows four values
and the two remaining states are modeled through the ``require_approval`` field and the number of positions within
an order. The states and their allowed changes are shown in the following graph:
.. image:: /images/order_states.png
Object types
------------
Order
One order represents one purchase. It's the main object you interact with and bundles all the other objects
together. Orders can change in many ways during their lifetime, but will never be deleted (unless ``testmode``
is set to ``true``).
Order position
An order position represents one product contained in the order. Orders can usually have multiple positions.
There might be a parent-child relation between order positions if one position is an add-on to another position.
Order positions can change in many ways during their lifetime, and can also be removed or added to an order.
Order fees
A fee represents a charge that is not related to a product. Examples include shipping fees, service fees, and
cancellation fees.
Order fees can change in many ways during their lifetime, and can also be removed or added to an order.
Order payment
An order payment represents one payment attempt with a specific payment method and amount. An order can have
multiple payments attached.
Order payments have their own state diagram. Apart from their state and their meta information (e.g. used
credit card, …) they usually don't change. They may be added at any time, but will never be deleted.
Order refund
An order payment represents one refund attempt with a specific payment method and amount. An order can have
multiple refunds attached.
Order refunds have their own state diagram. Apart from their state and their meta information (e.g. used
credit card, …) they usually don't change. They may be added at any time, but will never be deleted.
Invoice
An invoice represents a legal document stating the contents of an order. While the backend technically allows
to update an invoice in some situations, invoices are generally considered immutable. Once they are issued,
they no longer change. If the order changes substantially (e.g. prices change), an invoice is canceled through
creation of a new invoice with the opposite amount, plus the issuance of a new invoice.
Here's an example of how they all play together:
.. image:: /images/order_objects.png

View File

@@ -42,6 +42,10 @@ seat objects The assigned se
└ seat_guid string Identifier of the seat within the seating plan
===================================== ========================== =======================================================
.. versionchanged:: 1.17
This resource has been added.
.. versionchanged:: 3.0
This ``seat`` attribute has been added.

View File

@@ -25,6 +25,14 @@ is_addon boolean If ``true``, it
defining add-ons for other products.
===================================== ========================== =======================================================
.. versionchanged:: 1.14
The operations POST, PATCH, PUT and DELETE have been added.
.. versionchanged:: 1.16
The field ``internal_name`` has been added.
Endpoints
---------

View File

@@ -36,6 +36,22 @@ rules object Custom check-in
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
This resource has been added.
.. versionchanged:: 1.11
The ``positions`` endpoints have been added.
.. versionchanged:: 1.13
The ``include_pending`` field has been added.
.. versionchanged:: 3.2
The ``auto_checkin_sales_channels`` field has been added.
.. versionchanged:: 3.9
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
@@ -52,6 +68,10 @@ exit_all_at datetime Automatically c
Endpoints
---------
.. versionchanged:: 1.15
The ``../status/`` detail endpoint has been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
Returns a list of all check-in lists within a given event.
@@ -360,6 +380,29 @@ Endpoints
Order position endpoints
------------------------
.. versionchanged:: 1.15
The order positions endpoint has been extended by the filter queries ``item__in``, ``variation__in``,
``order__status__in``, ``subevent__in``, ``addon_to__in``, and ``search``. The search for attendee names and order
codes is now case-insensitive.
The ``.../redeem/`` endpoint has been added.
.. versionchanged:: 2.0
The order positions endpoint has been extended by the filter queries ``voucher`` and ``voucher__code``.
.. versionchanged:: 2.7
The resource now contains the new attributes ``require_attention`` and ``order__status`` and accepts the new
``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint
returns ``400`` instead of ``404`` on tickets which are known but not paid.
.. versionchanged:: 3.2
The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed
automatically by the system.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as

View File

@@ -52,6 +52,31 @@ sales_channels list A list of sales
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The ``meta_data`` field has been added.
.. versionchanged:: 1.15
The ``plugins`` field has been added.
The operations POST, PATCH, PUT and DELETE have been added.
.. versionchanged:: 2.1
Filters have been added to the list of events.
.. versionchanged:: 2.5
The ``testmode`` attribute has been added.
.. versionchanged:: 2.8
When cloning events, the ``testmode`` attribute will now be cloned, too.
.. versionchanged:: 3.0
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.

View File

@@ -46,6 +46,24 @@ 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.

View File

@@ -28,6 +28,10 @@ multi_allowed boolean Adding the same
price_included boolean Adding this add-on to the item is free
===================================== ========================== =======================================================
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------

View File

@@ -30,6 +30,10 @@ designated_price money (string) Designated pric
taxation. This is not added to the price.
===================================== ========================== =======================================================
.. versionchanged:: 2.6
This resource has been added.
Endpoints
---------

View File

@@ -26,6 +26,14 @@ description multi-lingual string A public descri
position integer An integer, used for sorting
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added.
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------

View File

@@ -118,6 +118,44 @@ bundles list of objects Definition of b
meta_data object Values set for event-specific meta data parameters.
===================================== ========================== =======================================================
.. versionchanged:: 2.7
The attribute ``original_price`` has been added for ``variations``.
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
``checkin_attention`` has been added.
.. versionchanged:: 1.12
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
The attribute ``price_included`` has been added to ``addons``.
.. versionchanged:: 1.16
The ``internal_name`` and ``original_price`` fields have been added.
.. versionchanged:: 2.0
The field ``require_approval`` has been added.
.. versionchanged:: 2.3
The ``sales_channels`` attribute has been added.
.. versionchanged:: 2.4
The ``generate_tickets`` attribute has been added.
.. versionchanged:: 2.6
The ``bundles`` and ``require_bundling`` attributes have been added.
.. versionchanged:: 3.0
The ``show_quota_left``, ``allow_waitinglist``, and ``hidden_if_available`` attributes have been added.
.. versionchanged:: 3.7
The attribute ``meta_data`` has been added.

View File

@@ -94,6 +94,60 @@ last_modified datetime Last modificati
===================================== ========================== =======================================================
.. versionchanged:: 1.6
The ``invoice_address.country`` attribute contains a two-letter country code for all new orders. For old orders,
a custom text might still be returned.
.. versionchanged:: 1.7
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
deprecated in favor of the new ``fees`` attribute but will still be served and removed in 1.9.
.. versionchanged:: 1.9
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
The attribute ``invoice_address.internal_reference`` has been added.
.. versionchanged:: 1.13
The field ``checkin_attention`` has been added.
.. versionchanged:: 1.15
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate``, ``order.payment_fee_tax_value`` and
``order.payment_fee_tax_rule`` have finally been removed.
.. versionchanged:: 1.16
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
An endpoint for order creation as well as ``…/mark_refunded/`` has been added.
.. versionchanged:: 2.0
The ``order.payment_date`` and ``order.payment_provider`` attributes have been deprecated in favor of the new
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2. The ``require_approval``
attribute has been added, as have been the ``…/approve/`` and ``…/deny/`` endpoints.
.. versionchanged:: 2.3
The ``sales_channel`` attribute has been added.
.. versionchanged:: 2.4
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
``…/mark_refunded/`` has been deprecated.
.. versionchanged:: 2.5
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. versionchanged:: 3.1
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
vouchers are now supported and many fields are now optional.
.. versionchanged:: 3.5
The ``order.fees.canceled`` attribute has been added.
@@ -166,7 +220,7 @@ downloads list of objects List of ticket
└ url string Download URL
answers list of objects Answers to user-defined questions
├ question integer Internal ID of the answered question
├ answer string Text representation of the answer (URL if answer is a file)
├ answer string Text representation of the answer
├ question_identifier string The question's ``identifier`` field
├ options list of integers Internal IDs of selected option(s)s (only for choice types)
└ option_identifiers list of strings The ``identifier`` fields of the selected option(s)s
@@ -179,6 +233,30 @@ pdf_data object Data object req
``pdf_data=true`` query parameter to your request.
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The attribute ``tax_rule`` has been added.
.. versionchanged:: 1.11
The attribute ``checkins.list`` has been added.
.. versionchanged:: 1.14
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
.. versionchanged:: 1.16
The attributes ``pseudonymization_id`` and ``pdf_data`` have been added.
.. versionchanged:: 3.0
The attribute ``seat`` has been added.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. versionchanged:: 3.3
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
@@ -196,10 +274,6 @@ pdf_data object Data object req
The ``checkin.type`` attribute has been added.
.. versionchanged:: 3.16
Answers to file questions are now returned as an URL.
.. _order-payment-resource:
Order payment resource
@@ -228,6 +302,14 @@ details object Payment-specifi
the object is empty.
===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
.. versionchanged:: 3.1
The attributes ``payment_url`` and ``details`` have been added.
.. _order-refund-resource:
Order refund resource
@@ -248,9 +330,17 @@ 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.
@@ -1356,6 +1446,21 @@ Sending e-mails
List of all order positions
---------------------------
.. versionchanged:: 1.15
The order positions endpoint has been extended by the filter queries ``item__in``, ``variation__in``,
``order__status__in``, ``subevent__in``, ``addon_to__in`` and ``search``. The search for attendee names and order
codes is now case-insensitive.
.. versionchanged:: 2.0
The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and
``pseudonymization_id``.
.. versionchanged:: 3.2
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
.. versionchanged:: 3.5
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
@@ -1695,6 +1800,10 @@ Manipulating individual positions
Order payment endpoints
-----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. versionchanged:: 3.6
Payments can now be created through the API.
@@ -1974,6 +2083,10 @@ Order payment endpoints
Order refund endpoints
----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
Returns a list of all refunds for an order.

View File

@@ -19,6 +19,10 @@ identifier string An arbitrary st
answer multi-lingual string The displayed value of this option
===================================== ========================== =======================================================
.. versionchanged:: 1.12
This resource has been added.
Endpoints
---------

View File

@@ -75,6 +75,28 @@ dependency_value string An old version
for one value. **Deprecated.**
===================================== ========================== =======================================================
.. versionchanged:: 1.12
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed and the ``ask_during_checkin`` field has
been added.
.. versionchanged:: 1.14
Write methods have been added. The attribute ``identifier`` has been added to both the resource itself and the
options resource. The ``position`` attribute has been added to the options resource.
.. versionchanged:: 2.7
The attribute ``hidden`` and the question type ``CC`` have been added.
.. versionchanged:: 3.0
The attribute ``dependency_values`` has been added.
.. versionchanged:: 3.1
The attribute ``print_on_invoice`` has been added.
.. versionchanged:: 3.5
The attribute ``help_text`` has been added.

View File

@@ -30,6 +30,14 @@ release_after_exit boolean Whether the quo
have been scanned at an exit.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 3.0
The attributes ``close_when_sold_out`` and ``closed`` have been added.
.. versionchanged:: 3.10
The attribute ``release_after_exit`` has been added.

View File

@@ -20,6 +20,10 @@ layout object JSON representa
still evolves. The version in use can be found `here`_.
===================================== ========================== =======================================================
.. versionchanged:: 3.0
This endpoint has been added.
Endpoints
---------

View File

@@ -33,7 +33,6 @@ date_to datetime The sub-event's
date_admission datetime The sub-event's admission date (or ``null``)
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
frontpage_text multi-lingual string The description of the event (or ``null``)
location multi-lingual string The sub-event location (or ``null``)
geo_lat float Latitude of the location (or ``null``)
geo_lon float Longitude of the location (or ``null``)
@@ -55,6 +54,25 @@ seat_category_mapping object An object mappi
last_modified datetime Last modification of this object
===================================== ========================== =======================================================
.. versionchanged:: 1.7
The ``meta_data`` field has been added.
.. versionchanged:: 2.1
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
.. versionchanged:: 2.6
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 2.7
The attribute ``is_public`` has been added.
.. versionchanged:: 3.0
The attributes ``seating_plan`` and ``seat_category_mapping`` have been added.
.. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added.

View File

@@ -24,6 +24,14 @@ home_country string Merchant countr
``null`` or empty string
===================================== ========================== =======================================================
.. versionchanged:: 1.7
This resource has been added.
.. versionchanged:: 1.9
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
Endpoints
---------

View File

@@ -46,6 +46,14 @@ show_hidden_items boolean Only if set to
===================================== ========================== =======================================================
.. versionchanged:: 1.9
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
.. versionchanged:: 3.0
The attribute ``show_hidden_items`` has been added.
.. versionchanged:: 3.4
The attribute ``seat`` has been added.

View File

@@ -79,7 +79,7 @@ Ticket designs
""""""""""""""
.. automodule:: pretix.base.signals
:members: layout_text_variables, layout_image_variables
:members: layout_text_variables
.. automodule:: pretix.plugins.ticketoutputpdf.signals
:members: override_layout

View File

@@ -82,15 +82,11 @@ Orders
^^^^^^
If a customer completes the checkout process, an **Order** will be created containing all the entered information.
An order can be in one of currently six states that are listed in the diagram below:
An order can be in one of currently four states that are listed in the diagram below:
.. image:: /images/order_states.png
The dotted lines represent status changes that usually do not happen as part of the regular process, but can be
performed manually in the admin backend.
For historical reasons, there are only four valid values of the ``status`` field, and the two additional states are
represented differently:
There are additional "fake" states that are displayed like states but not represented as states in the system:
* An order is considered **canceled (with paid fee)** if it is in **paid** status but does not include any non-cancelled positions.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,34 +0,0 @@
@startuml
participant User
collections "OrderPayment\nOrderRefund" as P
collections "Order\nOrderPosition" as O
collections "Invoice\nInvoiceLine" as I
User -> O: Order placed (€100)
rnote over O #6DD96D: Order A1B2C\nstatus = **n**\ntotal = €100
O -> P: Payment created
O -> I: Invoice created\n(can also happen later)
rnote over I #6DD96D: Invoice 00001\n€100
rnote over P #6DD96D: OrderPayment A1B2C-P-1\nstate = **created**
P -> User: Payment details (web, email)
User -> P: Payment performed
rnote over P #EFF46B: OrderPayment A1B2C-P-1\nstate = **confirmed**
P -> O: Order marked as paid
rnote over O #EFF46B: Order A1B2C\nstatus = **p**\ntotal = €100
User -> O: Data change (e.g. invoice address)
O -> I: Invoice reissued
rnote over I #6DD96D: Invoice 00002\n€-100
rnote over I #6DD96D: Invoice 00003\n€100
rnote over O #EFF46B: Order A1B2C\nstatus = **p**\ntotal = €100
User -> O: Order canceled
rnote over O #EFF46B: Order A1B2C\nstatus = **c**
O -> I: Invoice canceled
rnote over I #6DD96D: Invoice 00004\n€-100
O -> P: Refund started
rnote over P #6DD96D: OrderRefund\nA1B2C-R-1\nstate = **created**
P -> User: Money sent
rnote over P #EFF46B: OrderRefund\nA1B2C-R-1\nstate = **done**
@enduml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,39 +1,19 @@
@startuml
state "Approval Pending" as AP
state "Canceled (with paid fee)" as CP
AP: status = "n"
AP: require_approval = true
Pending: status = "n"
Pending: require_approval = false
Pending: Tickets reserved: yes
Expired: status = "e"
Expired: Tickets reserved: no
Paid: status = "p"
Paid: count(positions | !canceled) > 0
Paid: Tickets reserved: yes
CP: status = "p"
CP: count(positions | !canceled) = 0
Canceled: status = "c"
Canceled: Tickets reserved: no
Pending: Order is expecting payment\nOrder reduces quotas
Expired: Payment period is over\nOrder does not affect quotas
Paid: Order was successful\nOrder reduces quotas
Canceled: Order has been canceled\nOrder does not affect quotas
[*] -> Pending: order placed\ntotal > 0
[*] -> Paid: order placed\ntotal = 0
[*] -> AP: order placed\napproval required
Pending --> Paid: order paid
Pending --> Expired: after payment\ndeadline
Expired --> Paid: order paid\n(only if quota left)
Expired -[dashed]-> Canceled
Expired -[dashed]-> Pending: order extended
Paid --> Canceled: order canceled
Pending --> Canceled: order canceled
Paid -[dashed]-> Pending: refund
AP --> Pending: order approved
AP --> Canceled: order denied
Paid --> CP: order canceled\n(with cancellation fee)
Canceled -[dashed]-> Pending: order reactivated
Canceled -[dashed]-> Paid: order reactivated
CP -[dashed]-> Canceled: fee canceled
[*] --> Pending: customer\nplaces order
Pending --> Paid: successful payment
Pending --> Expired: automatically\nor manually\non admin action
Expired --> Paid: if payment is received\nonly if quota left
Expired --> Canceled
Expired --> Pending: manually\non admin action
Paid --> Canceled: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund
Pending --> Canceled: on admin or\ncustomer action
Paid -> Pending: manually on admin action
[*] --> Paid: customer\nplaces free order
@enduml

View File

@@ -22,6 +22,10 @@ item_assignments list of objects Products this l
└ item integer Item ID
===================================== ========================== =======================================================
.. versionchanged:: 1.16
This resource has been added.
Endpoints
---------

View File

@@ -24,6 +24,14 @@ item_assignments list of objects Products this l
└ item integer Item ID
===================================== ========================== =======================================================
.. versionchanged:: 1.16
This resource has been added.
.. versionchanged:: 2.3
The ``item_assignments.sales_channel`` field has been added.
Endpoints
---------

View File

@@ -437,6 +437,11 @@ 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.

View File

@@ -1 +1 @@
__version__ = "3.17.0.dev0"
__version__ = "3.16.0.dev0"

View File

@@ -41,7 +41,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:order-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
@@ -69,7 +68,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:checkinlist-status'),
('POST', 'api-v1:checkinlistpos-redeem'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'),
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
)
@@ -99,7 +97,6 @@ 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'),

View File

@@ -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 Device, Event, TaxRule, TeamAPIToken
from pretix.base.models import Event, TaxRule
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
@@ -174,12 +174,9 @@ class EventSerializer(I18nAwareModelSerializer):
}
def validate_meta_data(self, value):
for key, v in value['meta_data'].items():
for key in value['meta_data'].keys():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@cached_property
@@ -226,14 +223,6 @@ class EventSerializer(I18nAwareModelSerializer):
return value
@cached_property
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
@@ -249,11 +238,10 @@ class EventSerializer(I18nAwareModelSerializer):
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
if key not in self.ignored_meta_properties:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
# Item Meta properties
if item_meta_properties is not None:
@@ -291,21 +279,19 @@ class EventSerializer(I18nAwareModelSerializer):
if meta_data is not None:
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
for key, value in meta_data.items():
if key not in self.ignored_meta_properties:
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in self.ignored_meta_properties:
if prop.name not in meta_data:
current_object.delete()
if prop.name not in meta_data:
current_object.delete()
# Item Meta properties
if item_meta_properties is not None:
@@ -409,8 +395,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
'meta_data', 'seat_category_mapping', 'last_modified')
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
'seat_category_mapping', 'last_modified')
def validate(self, data):
data = super().validate(data)
@@ -458,22 +444,11 @@ class SubEventSerializer(I18nAwareModelSerializer):
}
def validate_meta_data(self, value):
for key, v in value['meta_data'].items():
for key in value['meta_data'].keys():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@cached_property
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@transaction.atomic
def create(self, validated_data):
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
@@ -490,11 +465,10 @@ class SubEventSerializer(I18nAwareModelSerializer):
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
if key not in self.ignored_meta_properties:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
# Seats
if subevent.seating_plan:
@@ -540,21 +514,19 @@ class SubEventSerializer(I18nAwareModelSerializer):
if meta_data is not None:
current = {mv.property: mv for mv in subevent.meta_values.select_related('property')}
for key, value in meta_data.items():
if key not in self.ignored_meta_properties:
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in self.ignored_meta_properties:
if prop.name not in meta_data:
current_object.delete()
if prop.name not in meta_data:
current_object.delete()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
@@ -596,7 +568,6 @@ class EventSettingsSerializer(SettingsSerializer):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'checkout_success_text',
'banner_text',
'banner_text_bottom',
'show_dates_on_frontpage',
@@ -657,7 +628,6 @@ class EventSettingsSerializer(SettingsSerializer):
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',

View File

@@ -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_images, get_variables
from pretix.base.pdf import get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import NoLockManager
from pretix.base.services.pricing import get_price
@@ -112,17 +112,6 @@ 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')
@@ -276,9 +265,6 @@ class PdfDataSerializer(serializers.Field):
if 'vars' not in self.context:
self.context['vars'] = get_variables(self.context['request'].event)
if 'vars_images' not in self.context:
self.context['vars_images'] = get_images(self.context['request'].event)
for k, f in self.context['vars'].items():
res[k] = f['evaluate'](instance, instance.order, ev)
@@ -293,28 +279,7 @@ class PdfDataSerializer(serializers.Field):
for k, v in instance.item._cached_meta_data.items():
res['itemmeta:' + k] = v
res['images'] = {}
for k, f in self.context['vars_images'].items():
if 'etag' in f:
has_image = etag = f['etag'](instance, instance.order, ev)
else:
has_image = f['etag'](instance, instance.order, ev)
etag = None
if has_image:
url = reverse('api-v1:orderposition-pdf_image', kwargs={
'organizer': instance.order.event.organizer.slug,
'event': instance.order.event.slug,
'pk': instance.pk,
'key': k,
}, request=self.context['request'])
if etag:
url += f'#etag={etag}'
res['images'][k] = url
else:
res['images'][k] = None
return res
return res
class OrderPositionSerializer(I18nAwareModelSerializer):

View File

@@ -1,6 +1,4 @@
import datetime
import mimetypes
import os
from decimal import Decimal
import django_filters
@@ -14,7 +12,6 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from PIL import Image
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import (
@@ -38,9 +35,8 @@ from pretix.base.models import (
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent,
TaxRule, TeamAPIToken, generate_secret,
)
from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret
from pretix.base.models.orders import RevokedTicketSecret
from pretix.base.payment import PaymentException
from pretix.base.pdf import get_images
from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import tickets
from pretix.base.services.invoices import (
@@ -917,62 +913,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
'tax_rule': tr.pk if tr else None,
})
@action(detail=True, url_name='answer', url_path=r'answer/(?P<question>\d+)')
def answer(self, request, **kwargs):
pos = self.get_object()
answer = get_object_or_404(
QuestionAnswer,
orderposition=self.get_object(),
question_id=kwargs.get('question')
)
if not answer.file:
raise NotFound()
ftype, ignored = mimetypes.guess_type(answer.file.name)
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
)
return resp
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
def pdf_image(self, request, key, **kwargs):
pos = self.get_object()
image_vars = get_images(request.event)
if key not in image_vars:
raise NotFound('Unknown key')
image_file = image_vars[key]['evaluate'](pos, pos.order, pos.subevent or self.request.event)
if image_file is None:
raise NotFound('No image available')
if getattr(image_file, 'name', ''):
ftype, ignored = mimetypes.guess_type(image_file.name)
extension = os.path.basename(image_file.name).split('.')[-1]
else:
img = Image.open(image_file)
ftype = Image.MIME[img.format]
extensions = {
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
}
extension = extensions.get(img.format, 'bin')
if hasattr(image_file, 'seek'):
image_file.seek(0)
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
)
return resp
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
provider = self._get_output_provider(output)

View File

@@ -468,8 +468,7 @@ def base_placeholders(sender, **kwargs):
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
'voucher_list', ['voucher_list'], lambda voucher_list: '\n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(

View File

@@ -1,5 +1,4 @@
import io
import re
import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
@@ -11,21 +10,11 @@ from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES
from openpyxl.cell.cell import KNOWN_TYPES
from pretix.base.models import Event
def excel_safe(val):
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
class BaseExporter:
"""
This is the base class for all data exporters
@@ -192,7 +181,7 @@ class ListExporter(BaseExporter):
total = line.total
continue
ws.append([
excel_safe(val) if not isinstance(val, KNOWN_TYPES) else val
str(val) if not isinstance(val, KNOWN_TYPES) else val
for val in line
])
if total:
@@ -312,7 +301,7 @@ class MultiSheetListExporter(ListExporter):
total = line.total
continue
ws.append([
excel_safe(val)
str(val) if not isinstance(val, KNOWN_TYPES) else val
for val in line
])
if total:

View File

@@ -13,7 +13,6 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _, pgettext
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
from ..services.export import ExportError
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
@@ -112,8 +111,6 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
if not i.file:
raise ExportError('Could not generate PDF for invoice {nr}'.format(nr=i.full_invoice_no))
i.file.open('rb')
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
i.file.close()

View File

@@ -444,7 +444,6 @@ class BaseQuestionsForm(forms.Form):
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
cc = None
state = None
if fprefix + 'country' in self.data:
cc = str(self.data[fprefix + 'country'])
elif country:
@@ -453,7 +452,6 @@ class BaseQuestionsForm(forms.Form):
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
state = (cartpos.state if cartpos else orderpos.state)
elif fprefix + 'state' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'state']
@@ -462,7 +460,6 @@ class BaseQuestionsForm(forms.Form):
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
initial=state,
widget=forms.Select(attrs={
'autocomplete': 'address-level1',
}),

View File

@@ -12,10 +12,12 @@ from django.utils.formats import date_format, localize
from django.utils.translation import (
get_language, gettext, gettext_lazy, pgettext,
)
from PIL.Image import BICUBIC
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfbase.ttfonts import TTFont
@@ -29,7 +31,6 @@ from pretix.base.decimal import round_decimal
from pretix.base.models import Event, Invoice, Order
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ThumbnailingImageReader
logger = logging.getLogger(__name__)
@@ -220,6 +221,26 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
class ThumbnailingImageReader(ImageReader):
def resize(self, width, height, dpi):
if width is None:
width = height * self._image.size[0] / self._image.size[1]
if height is None:
height = width * self._image.size[1] / self._image.size[0]
self._image.thumbnail(
size=(int(width * dpi / 72), int(height * dpi / 72)),
resample=BICUBIC
)
self._data = None
return width, height
def _jpeg_fh(self):
# Bypass a reportlab-internal optimization that falls back to the original
# file handle if the file is a JPEG, and therefore does not respect the
# (smaller) size of the modified image.
return None
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
@@ -255,10 +276,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = Paragraph(
bleach.clean(self.invoice.full_invoice_from, tags=[]).strip().replace('\n', '<br />\n'),
style=self.stylesheet['InvoiceFrom']
)
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
'InvoiceFrom'])
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
@@ -363,7 +382,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
@@ -444,18 +462,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = []
if self.invoice.custom_field:
story.append(Paragraph(
'{}: {}'.format(
bleach.clean(self.invoice.event.settings.invoice_address_custom_field, tags=[]).strip().replace('\n', '<br />\n'),
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
),
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
self.stylesheet['Normal']
))
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=bleach.clean(self.invoice.internal_reference, tags=[]).strip().replace('\n', '<br />\n'),
),
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
self.stylesheet['Normal']
))
@@ -474,10 +487,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
))
if self.invoice.introductory_text:
story.append(Paragraph(
bleach.clean(self.invoice.introductory_text, tags=[]).strip().replace('\n', '<br />\n'),
self.stylesheet['Normal']
))
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
story.append(Spacer(1, 10 * mm))
return story
@@ -577,16 +587,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 15 * mm))
if self.invoice.payment_provider_text:
story.append(Paragraph(
bleach.clean(self.invoice.payment_provider_text, tags=[]).strip().replace('\n', '<br />\n'),
self.stylesheet['Normal']
))
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
if self.invoice.additional_text:
story.append(Paragraph(
bleach.clean(self.invoice.additional_text, tags=[]).strip().replace('\n', '<br />\n'),
self.stylesheet['Normal']
))
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
story.append(Spacer(1, 15 * mm))
tstyledata = [
@@ -718,10 +722,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas):
if not self.invoice.invoice_from:
return
c = [
bleach.clean(l, tags=[]).strip().replace('\n', '<br />\n')
for l in self.invoice.address_invoice_from.strip().split('\n')
]
c = self.invoice.address_invoice_from.strip().split('\n')
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)

View File

@@ -30,7 +30,7 @@ class Command(BaseCommand):
continue
if verbosity > 1:
self.stdout.write(f'INFO Running {name}')
self.stdout.write(f'Running {name}')
t0 = time.time()
try:
r = receiver(signal=periodic_task, sender=self)
@@ -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'ERROR runperiodic {str(err)}\n'))
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
else:
self.stdout.write(self.style.ERROR(f'ERROR runperiodic {str(err)}\n'))
self.stdout.write(self.style.ERROR(f'FAIL: {str(err)}\n'))
traceback.print_exc()
else:
if options.get('verbosity') > 1:
if r is SKIPPED:
self.stdout.write(self.style.SUCCESS(f'INFO Skipped {name}'))
self.stdout.write(self.style.SUCCESS(f'Skipped {name}'))
else:
self.stdout.write(self.style.SUCCESS(f'INFO Completed {name} in {round(time.time() - t0, 3)}s'))
self.stdout.write(self.style.SUCCESS(f'Completed {name} in {round(time.time() - t0, 3)}s'))

View File

@@ -1,28 +0,0 @@
# Generated by Django 3.0.11 on 2021-02-05 15:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0175_orderrefund_comment'),
]
operations = [
migrations.AddField(
model_name='eventmetaproperty',
name='allowed_values',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='eventmetaproperty',
name='protected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='eventmetaproperty',
name='required',
field=models.BooleanField(default=False),
),
]

View File

@@ -16,12 +16,11 @@ 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, gettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField
@@ -862,7 +861,7 @@ class Event(EventMixin, LoggedModel):
Returns the currently configured ticket secret generator.
"""
tsgs = self.ticket_secret_generators
return tsgs.get(self.settings.ticket_secret_generator, tsgs.get('random'))
return tsgs[self.settings.ticket_secret_generator]
def get_data_shredders(self) -> dict:
"""
@@ -954,18 +953,6 @@ class Event(EventMixin, LoggedModel):
if not self.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
for mp in self.organizer.meta_properties.all():
if mp.required and not self.meta_data.get(mp.name):
issues.append(
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
property=mp.name,
a_attr='href="%s#id_prop-%d-value"' % (
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
mp.pk
)
)
)
responses = event_live_issues.send(self)
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
if response:
@@ -1376,26 +1363,7 @@ class EventMetaProperty(LoggedModel):
],
verbose_name=_("Name"),
)
default = models.TextField(blank=True, verbose_name=_("Default value"))
protected = models.BooleanField(default=False,
verbose_name=_("Can only be changed by organizer-level administrators"))
required = models.BooleanField(
default=False, verbose_name=_("Required for events"),
help_text=_("If checked, an event can only be taken live if the property is set. In event series, its always "
"optional to set a value for individual dates")
)
allowed_values = models.TextField(
null=True, blank=True,
verbose_name=_("Valid values"),
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
)
def full_clean(self, exclude=None, validate_unique=True):
super().full_clean(exclude, validate_unique)
if self.default and self.required:
raise ValidationError(_("A property can either be required or have a default value, not both."))
if self.default and self.allowed_values and self.default not in self.allowed_values.splitlines():
raise ValidationError(_("You cannot set a default value that is not a valid value."))
default = models.TextField(blank=True)
class EventMetaValue(LoggedModel):

View File

@@ -4,7 +4,6 @@ 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
@@ -89,15 +88,6 @@ 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(

View File

@@ -1,5 +1,4 @@
import copy
import hashlib
import itertools
import logging
import os
@@ -36,9 +35,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, Question
from pretix.base.models import Order, OrderPosition
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.signals import 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
@@ -155,11 +154,6 @@ 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"),
@@ -345,47 +339,6 @@ 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")
@@ -416,8 +369,6 @@ 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),
@@ -436,15 +387,6 @@ 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)
@@ -485,7 +427,6 @@ 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()
@@ -573,47 +514,6 @@ 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']:
@@ -672,8 +572,6 @@ 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":

View File

@@ -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 import (
from cryptography.hazmat.primitives.serialization.base import (
Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key,
load_pem_public_key,
)

View File

@@ -102,11 +102,9 @@ 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
@@ -115,7 +113,6 @@ 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))
@@ -125,7 +122,6 @@ 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]))
@@ -138,7 +134,6 @@ 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]
@@ -146,14 +141,9 @@ 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):
@@ -173,16 +163,18 @@ 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()}
@@ -192,85 +184,81 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
_save_answers(op, answers, given_answers)
with transaction.atomic():
# Lock order positions
op = OrderPosition.all.select_for_update().get(pk=op.pk)
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
)
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'
)
device = None
if isinstance(auth, Device):
device = auth
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 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
)
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:
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 ticket has already been redeemed.'),
'already_redeemed',
_('This entry is not permitted due to custom rules.'),
'rules'
)
device = None
if isinstance(auth, Device):
device = auth
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):

View File

@@ -291,8 +291,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
order = None
else:
with language(order.locale, event.settings.region):
if not event.settings.mail_attach_tickets:
attach_tickets = False
if position:
try:
position = order.positions.get(pk=position)

View File

@@ -327,7 +327,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
cancellation_fee=None, keep_fees=None):
cancellation_fee=None, keep_fees=None, cancel_invoice=True):
"""
Mark this order as canceled
:param order: The order to change
@@ -351,9 +351,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.'))
invoices = []
i = order.invoices.filter(is_cancellation=False).last()
if i and not i.refered.exists():
invoices.append(generate_cancellation(i))
if cancel_invoice:
i = order.invoices.filter(is_cancellation=False).last()
if i and not i.refered.exists():
invoices.append(generate_cancellation(i))
for position in order.positions.all():
for gc in position.issued_gift_cards.all():
@@ -403,7 +404,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date', 'total'])
if i:
if cancel_invoice and i:
invoices.append(generate_invoice(order))
else:
with order.event.lock():
@@ -2152,11 +2153,12 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
@scopes_disabled()
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None):
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None,
cancel_invoice=True):
try:
try:
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
cancellation_fee)
cancellation_fee, cancel_invoice=cancel_invoice)
if try_auto_refund:
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
comment=comment)

View File

@@ -13,7 +13,6 @@ from django.core.validators import (
MaxValueValidator, MinValueValidator, RegexValidator,
)
from django.db.models import Model
from django.utils.text import format_lazy
from django.utils.translation import (
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
)
@@ -1323,19 +1322,6 @@ DEFAULTS = {
'default': 'classic',
'type': str
},
'mail_attach_tickets': {
'default': 'True',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Attach ticket files"),
help_text=format_lazy(
_("Tickets will never be attached if they're larger than {size} to avoid email delivery problems."),
size='4 MB'
),
)
},
'mail_attach_ical': {
'default': 'False',
'type': bool,
@@ -1997,19 +1983,6 @@ Your {event} team"""))
"why you need information from them.")
)
},
'checkout_success_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Additional success message"),
help_text=_("This message will be shown after an order has been created successfully. It will be shown in additional "
"to the default text."),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
)
},
'checkout_phone_helptext': {
'default': '',
'type': LazyI18nString,

View File

@@ -613,7 +613,6 @@ 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.
@@ -628,35 +627,11 @@ 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

View File

@@ -28,13 +28,6 @@ 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')
@@ -96,7 +89,7 @@ class CheckinListForm(forms.ModelForm):
'class': 'scrolling-multiple-choice'
}),
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
'exit_all_at': forms.TimeInput(attrs={'class': 'timepickerfield'}),
}
field_classes = {
'limit_products': SafeModelMultipleChoiceField,

View File

@@ -265,32 +265,15 @@ 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
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']
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,
})
)
class Meta:
model = EventMetaValue
@@ -435,7 +418,6 @@ class EventSettingsForm(SettingsForm):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'checkout_success_text',
'show_dates_on_frontpage',
'show_date_to',
'show_times',
@@ -787,7 +769,6 @@ class MailSettingsForm(SettingsForm):
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',
]
mail_sales_channel_placed_paid = forms.MultipleChoiceField(

View File

@@ -1,4 +1,4 @@
from datetime import datetime, time, timedelta
from datetime import datetime, time
from decimal import Decimal
from urllib.parse import urlencode
@@ -766,15 +766,10 @@ class SubEventFilterForm(FilterForm):
),
required=False
)
date_from = forms.DateField(
label=_('Date from'),
date = forms.DateField(
label=_('Date'),
required=False,
widget=DatePickerWidget,
)
date_until = forms.DateField(
label=_('Date until'),
required=False,
widget=DatePickerWidget,
widget=DatePickerWidget
)
weekday = forms.ChoiceField(
label=_('Weekday'),
@@ -801,8 +796,7 @@ class SubEventFilterForm(FilterForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date_from'].widget = DatePickerWidget()
self.fields['date_until'].widget = DatePickerWidget()
self.fields['date'].widget = DatePickerWidget()
def filter_qs(self, qs):
fdata = self.cleaned_data
@@ -844,21 +838,19 @@ class SubEventFilterForm(FilterForm):
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
)
if fdata.get('date_until'):
date_end = make_aware(datetime.combine(
fdata.get('date_until') + timedelta(days=1),
if fdata.get('date'):
date_start = make_aware(datetime.combine(
fdata.get('date'),
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__lt=date_end) |
Q(date_to__isnull=False, date_to__lt=date_end)
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)
)
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())

View File

@@ -104,7 +104,7 @@ class ConfirmPaymentForm(forms.Form):
class CancelForm(ConfirmPaymentForm):
send_email = forms.BooleanField(
required=False,
label=_('Notify customer by email'),
label=_('Notify user by e-mail'),
initial=True
)
cancellation_fee = forms.DecimalField(
@@ -117,6 +117,11 @@ class CancelForm(ConfirmPaymentForm):
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
'tax will be calculated automatically.'),
)
cancel_invoice = forms.BooleanField(
label=_('Generate cancellation for invoice'),
initial=True,
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -130,6 +135,8 @@ class CancelForm(ConfirmPaymentForm):
self.fields['cancellation_fee'].max_value = prs
else:
del self.fields['cancellation_fee']
if not self.instance.invoices.exists():
del self.fields['cancel_invoice']
def clean_cancellation_fee(self):
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
@@ -139,11 +146,6 @@ 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,

View File

@@ -13,9 +13,7 @@ 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, EventMetaProperty, Gate, GiftCard, Organizer, Team,
)
from pretix.base.models import Device, Gate, GiftCard, Organizer, Team
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import SafeEventMultipleChoiceField
from pretix.multidomain.models import KnownDomain
@@ -127,8 +125,7 @@ class OrganizerUpdateForm(OrganizerForm):
class EventMetaPropertyForm(forms.ModelForm):
class Meta:
model = EventMetaProperty
fields = ['name', 'default', 'required', 'protected', 'allowed_values']
fields = ['name', 'default']
widgets = {
'default': forms.TextInput()
}

View File

@@ -1,4 +1,4 @@
from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
from bootstrap3.renderers import FieldRenderer
from bootstrap3.text import text_value
from django.forms import CheckboxInput
from django.forms.utils import flatatt
@@ -58,40 +58,3 @@ 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'

View File

@@ -1,9 +1,8 @@
from datetime import datetime, timedelta
from datetime import 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
@@ -12,7 +11,6 @@ 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
@@ -90,142 +88,6 @@ 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')
@@ -300,32 +162,15 @@ 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
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']
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,
})
)
class Meta:
model = SubEventMetaValue

View File

@@ -273,15 +273,8 @@ 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.'),

View File

@@ -427,13 +427,6 @@ 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:

View File

@@ -1,33 +0,0 @@
{% 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>

View File

@@ -1,8 +0,0 @@
{% 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>

View File

@@ -16,7 +16,6 @@
{% bootstrap_field form.mail_from_name layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
{% bootstrap_field form.mail_attach_tickets layout="control" %}
{% bootstrap_field form.mail_attach_ical layout="control" %}
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
</fieldset>

View File

@@ -24,7 +24,34 @@
{% endif %}
{% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" %}
{% include "pretixcontrol/event/fragment_geodata.html" %}
<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>
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}
@@ -43,7 +70,7 @@
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" error_types="all" %}
{% bootstrap_form form layout="inline" %}
</div>
</div>
{% endfor %}
@@ -188,7 +215,6 @@
</div>
</div>
{% bootstrap_field sform.checkout_success_text layout="control" %}
{% bootstrap_field sform.checkout_email_helptext layout="control" %}
{% bootstrap_field sform.checkout_phone_helptext layout="control" %}
{% bootstrap_field sform.banner_text layout="control" %}

View File

@@ -39,7 +39,30 @@
{% if form.date_to %}
{% bootstrap_field form.date_to layout="control" %}
{% endif %}
{% include "pretixcontrol/event/fragment_geodata.html" %}
<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>
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
</fieldset>

View File

@@ -23,13 +23,16 @@
<input type="hidden" name="status" value="c"/>
{% bootstrap_form_errors form %}
{% bootstrap_field form.send_email layout='' %}
{% if form.cancel_invoice %}
{% bootstrap_field form.cancel_invoice layout='' %}
{% endif %}
{% if form.cancellation_fee %}
{% bootstrap_field form.cancellation_fee layout='' %}
{% endif %}
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% trans "No, take me back" %}
</a>
</div>

View File

@@ -24,7 +24,6 @@
{% 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 %}

View File

@@ -15,8 +15,7 @@
{% 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>

View File

@@ -11,7 +11,7 @@
{% endif %}
</h1>
{% for e in exporters %}
<details class="panel panel-default" {% if "identifier" in request.GET or "exporter" in request.POST %}open{% endif %}>
<details class="panel panel-default" {% if "identifier" in request.GET %}open{% endif %}>
<summary class="panel-heading">
<h3 class="panel-title">
{{ e.verbose_name }}

View File

@@ -22,68 +22,113 @@
{% csrf_token %}
{% bootstrap_form_errors sform %}
{% bootstrap_form_errors form %}
<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>
</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 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>
<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>
{% include "pretixcontrol/includes/logs.html" with obj=organizer %}
<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>
</div>
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -11,7 +11,7 @@
{% endif %}
</h1>
{% for e in exporters %}
<details class="panel panel-default" {% if "identifier" in request.GET or "exporter" in request.POST %}open{% endif %}>
<details class="panel panel-default" {% if "identifier" in request.GET %}open{% endif %}>
<summary class="panel-heading">
<h3 class="panel-title">
{{ e.verbose_name }}

View File

@@ -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.gates" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
<a href="{% url "control:organizer.teams" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">

View File

@@ -1,85 +0,0 @@
{% 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 %}

View File

@@ -1,42 +0,0 @@
{% 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 %}

View File

@@ -1,19 +0,0 @@
{% 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 %}

View File

@@ -1,20 +0,0 @@
{% 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 %}

View File

@@ -228,18 +228,6 @@
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>
@@ -344,17 +332,6 @@
</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>
@@ -405,10 +382,6 @@
<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>

View File

@@ -384,7 +384,30 @@
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.active layout="control" %}
{% include "pretixcontrol/event/fragment_geodata.html" %}
<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>
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
{% if meta_forms %}
@@ -399,7 +422,7 @@
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" error_types="all" %}
{% bootstrap_form form layout="inline" %}
</div>
</div>
{% endfor %}

View File

@@ -1,352 +0,0 @@
{% 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>&nbsp;</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 %}

View File

@@ -25,7 +25,30 @@
{% bootstrap_field form.active layout="control" %}
{% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" %}
{% include "pretixcontrol/event/fragment_geodata.html" %}
<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>
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
@@ -41,7 +64,7 @@
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" error_types="all" %}
{% bootstrap_form form layout="inline" %}
</div>
</div>
{% endfor %}

View File

@@ -22,17 +22,14 @@
</div>
{% else %}
<form class="row filter-form" action="" method="get">
<div class="col-md-2 col-sm-6 col-xs-12">
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
<div class="col-md-3 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_from layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.date_until layout='inline' %}
{% bootstrap_field filter_form.date layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.weekday layout='inline' %}
@@ -46,28 +43,23 @@
</button>
</div>
</form>
{% 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 %}
<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>
<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 %}
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
<input type="checkbox" data-toggle-table/>
{% endif %}
</th>
<th>
@@ -75,40 +67,28 @@
</th>
<th>
{% trans "Begin" %}
<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>
<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>
</th>
<th>
{% trans "Paid tickets per quota" %}
<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>
<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>
</th>
<th>
{% trans "Status" %}
<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>
<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>
</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 %}
<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>
<input type="checkbox" name="subevent" class="" value="{{ s.pk }}"/>
{% endif %}
</td>
<td>
@@ -167,21 +147,15 @@
</table>
</div>
{% if "can_change_event_settings" in request.eventpermset %}
<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 type="submit" class="btn btn-default btn-save" name="action" value="delete">
{% trans "Delete selected" %}
</button>
<button type="submit" class="btn btn-default btn-save" name="action" value="enable">
<i class="fa fa-check"></i>{% trans "Activate selected" %}
{% trans "Enable selected" %}
</button>
<button type="submit" class="btn btn-default btn-save" name="action" value="disable">
<i class="fa fa-ban"></i>{% trans "Deactivate selected" %}
{% trans "Disable selected" %}
</button>
</div>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}

View File

@@ -77,13 +77,6 @@ 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'),
@@ -122,7 +115,6 @@ 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'),
@@ -174,7 +166,6 @@ 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'),

View File

@@ -91,10 +91,6 @@ 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)
)
@@ -175,7 +171,6 @@ 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 '

View File

@@ -151,6 +151,9 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
s = OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k')
i = Invoice.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k')
annotated = {
o['pk']: o
for o in
@@ -158,6 +161,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
pk__in=[o.pk for o in ctx['orders']]
).annotate(
pcnt=Subquery(s, output_field=IntegerField()),
icnt=Subquery(i, output_field=IntegerField()),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
).values(
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
@@ -1105,7 +1109,6 @@ 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
@@ -1134,6 +1137,7 @@ class OrderTransition(OrderView):
try:
cancel_order(self.order.pk, user=self.request.user,
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True),
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
except OrderError as e:
messages.error(self.request, str(e))
@@ -2080,17 +2084,11 @@ 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, TemplateView):
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View):
permission = 'can_view_orders'
known_errortypes = ['ExportError']
task = export
template_name = 'pretixcontrol/orders/export.html'
def get_success_message(self, value):
return None
@@ -2110,11 +2108,6 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
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.'))
@@ -2124,8 +2117,16 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
}))
if not self.exporter.form.is_valid():
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(request, *args, **kwargs)
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)
cf = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now()
@@ -2138,6 +2139,11 @@ 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

View File

@@ -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
from django.forms import DecimalField, inlineformset_factory
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@@ -51,7 +51,6 @@ 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,
)
@@ -270,10 +269,12 @@ 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():
@@ -320,11 +321,38 @@ 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():
if form.is_valid() and self.sform.is_valid() and self.formset.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
@@ -1148,9 +1176,8 @@ 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 id and ex.identifier != id:
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"):
continue
# Use form parse cycle to generate useful defaults
@@ -1182,16 +1209,10 @@ 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, TemplateView):
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, View):
known_errortypes = ['ExportError']
task = multiexport
template_name = 'pretixcontrol/organizers/export.html'
def get_success_message(self, value):
return None
@@ -1210,11 +1231,6 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
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.'))
@@ -1244,6 +1260,11 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
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
@@ -1344,115 +1365,3 @@ 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

View File

@@ -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_images, get_variables
from pretix.base.pdf import get_variables
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.database import rolledback_transaction
@@ -211,15 +211,11 @@ 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]

View File

@@ -1,17 +1,14 @@
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 (
Count, F, IntegerField, OuterRef, Prefetch, Subquery, Sum,
)
from django.db.models.functions import Coalesce, TruncDate, TruncTime
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
from django.db.models.functions import Coalesce
from django.forms import inlineformset_factory
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.formats import get_format
@@ -19,9 +16,7 @@ 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, FormView, ListView, UpdateView,
)
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from pretix.base.models import CartPosition, LogEntry
from pretix.base.models.checkin import CheckinList
@@ -30,33 +25,29 @@ 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, SubEventBulkEditForm,
SubEventBulkForm, SubEventForm, SubEventItemForm,
SubEventItemVariationForm, SubEventMetaValueForm, TimeFormSet,
CheckinListFormSet, QuotaFormSet, RRuleFormSet, 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 SubEventQueryMixin:
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
model = SubEvent
context_object_name = 'subevents'
template_name = 'pretixcontrol/subevents/index.html'
permission = 'can_change_settings'
@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):
def get_queryset(self):
sum_tickets_paid = Quota.objects.filter(
subevent=OuterRef('pk')
).order_by().values('subevent').annotate(
@@ -64,39 +55,18 @@ class SubEventQueryMixin:
).values(
's'
)
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')
)
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')
)
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
@@ -124,6 +94,10 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryM
)
return ctx
@cached_property
def filter_form(self):
return SubEventFilterForm(data=self.request.GET)
class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
model = SubEvent
@@ -192,10 +166,6 @@ 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)
@@ -458,7 +428,6 @@ 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:
@@ -560,13 +529,19 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
return formlist
class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View):
class SubEventBulkAction(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.get_queryset():
for obj in self.objects:
obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': False
@@ -576,7 +551,7 @@ class SubEventBulkAction(SubEventQueryMixin, 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.get_queryset():
for obj in self.objects:
obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={
'active': True
@@ -587,11 +562,11 @@ class SubEventBulkAction(SubEventQueryMixin, 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.get_queryset().filter(orderposition__isnull=True),
'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(),
'allowed': self.objects.filter(orderposition__isnull=True),
'forbidden': self.objects.filter(orderposition__isnull=False),
})
elif request.POST.get('action') == 'delete_confirm':
for obj in self.get_queryset():
for obj in self.objects:
if obj.allow_delete():
CartPosition.objects.filter(addon_to__subevent=obj).delete()
obj.cartposition_set.all().delete()
@@ -918,537 +893,3 @@ 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)

View File

@@ -67,8 +67,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
headers = [
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
_('Comment')
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat')
]
writer.writerow(headers)
@@ -93,8 +92,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
v.tag,
str(v.redeemed),
str(v.max_usages),
str(v.seat) if v.seat else "",
str(v.comment) if v.comment else ""
str(v.seat) if v.seat else ""
]
writer.writerow(row)

View File

@@ -1,22 +0,0 @@
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

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
"POT-Creation-Date: 2021-01-27 17:45+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:117
#: pretix/static/pretixbase/js/asynctask.js:116
#, 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:122
#: pretix/static/pretixbase/js/asynctask.js:121
#, 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:128
#: pretix/static/pretixbase/js/asynctask.js:127
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:87
#: pretix/static/pretixbase/js/asynctask.js:169
#: pretix/static/pretixbase/js/asynctask.js:174
#: pretix/static/pretixbase/js/asynctask.js:86
#: pretix/static/pretixbase/js/asynctask.js:159
#: pretix/static/pretixbase/js/asynctask.js:164
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr "حدث خطأ من نوع {كود}."
#: pretix/static/pretixbase/js/asynctask.js:90
#: pretix/static/pretixbase/js/asynctask.js:89
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr "لم نستطع معالجة طلبك، ولكننا نواصل المحاولة. رمز الخطأ : {code}"
#: pretix/static/pretixbase/js/asynctask.js:142
#: pretix/static/pretixbase/js/asynctask.js:141
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr "استغرقت العملية فترة طويلة، الرجاء المحاولة مرة أخرى."
#: pretix/static/pretixbase/js/asynctask.js:177
#: pretix/static/pretixbase/js/asynctask.js:167
#: 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:199
#: pretix/static/pretixbase/js/asynctask.js:189
msgid "We are processing your request …"
msgstr "جاري معالجة طلبك …"
#: pretix/static/pretixbase/js/asynctask.js:207
#: pretix/static/pretixbase/js/asynctask.js:197
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:245
#: pretix/static/pretixbase/js/asynctask.js:235
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "رسالة ثيقة"
@@ -215,62 +215,56 @@ msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:237
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:444
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:441
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:69
#: pretix/static/pretixcontrol/js/ui/editor.js:43
msgid "Lead Scan QR"
msgstr "يؤدي مسح QR"
#: pretix/static/pretixcontrol/js/ui/editor.js:71
#: pretix/static/pretixcontrol/js/ui/editor.js:45
msgid "Check-in QR"
msgstr "تحقق في QR"
#: pretix/static/pretixcontrol/js/ui/editor.js:311
#: pretix/static/pretixcontrol/js/ui/editor.js:269
msgid "The PDF background file could not be loaded for the following reason:"
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
#: pretix/static/pretixcontrol/js/ui/editor.js:517
#: pretix/static/pretixcontrol/js/ui/editor.js:461
msgid "Group of objects"
msgstr "مجموعة من الكائنات"
#: pretix/static/pretixcontrol/js/ui/editor.js:523
#: pretix/static/pretixcontrol/js/ui/editor.js:467
msgid "Text object"
msgstr "كائن النص"
#: pretix/static/pretixcontrol/js/ui/editor.js:525
#: pretix/static/pretixcontrol/js/ui/editor.js:469
msgid "Barcode area"
msgstr "منطقة الباركود"
#: pretix/static/pretixcontrol/js/ui/editor.js:527
#, fuzzy
#| msgid "Barcode area"
msgid "Image area"
msgstr "منطقة الباركود"
#: pretix/static/pretixcontrol/js/ui/editor.js:529
#: pretix/static/pretixcontrol/js/ui/editor.js:471
msgid "Powered by pretix"
msgstr "مدعوم من pretix"
#: pretix/static/pretixcontrol/js/ui/editor.js:531
#: pretix/static/pretixcontrol/js/ui/editor.js:473
msgid "Object"
msgstr "موضوع"
#: pretix/static/pretixcontrol/js/ui/editor.js:535
#: pretix/static/pretixcontrol/js/ui/editor.js:477
msgid "Ticket design"
msgstr "تصميم تذكرة"
#: pretix/static/pretixcontrol/js/ui/editor.js:808
#: pretix/static/pretixcontrol/js/ui/editor.js:734
msgid "Saving failed."
msgstr "فشل الادخار."
#: pretix/static/pretixcontrol/js/ui/editor.js:857
#: pretix/static/pretixcontrol/js/ui/editor.js:896
#: pretix/static/pretixcontrol/js/ui/editor.js:783
#: pretix/static/pretixcontrol/js/ui/editor.js:821
msgid "Error while uploading your PDF file, please try again."
msgstr "خطأ أثناء تحميل ملف PDF الخاصة بك، يرجى المحاولة مرة أخرى."
#: pretix/static/pretixcontrol/js/ui/editor.js:881
#: pretix/static/pretixcontrol/js/ui/editor.js:806
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "هل تريد حقا أن تترك المحرر دون حفظ التغييرات؟"
@@ -308,15 +302,15 @@ msgstr "الكل"
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:801
#: pretix/static/pretixcontrol/js/ui/main.js:710
msgid "Use a different name internally"
msgstr "استخدام اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:858
#: pretix/static/pretixcontrol/js/ui/main.js:767
msgid "Click to close"
msgstr "انقر لقريب"
#: pretix/static/pretixcontrol/js/ui/main.js:873
#: pretix/static/pretixcontrol/js/ui/main.js:782
msgid "You have unsaved changes!"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
"POT-Creation-Date: 2021-01-27 17:45+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -61,61 +61,61 @@ msgid "Contacting your bank …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:117
#: pretix/static/pretixbase/js/asynctask.js:116
msgid ""
"Your request is currently being processed. Depending on the size of your "
"event, this might take up to a few minutes."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:122
#: pretix/static/pretixbase/js/asynctask.js:121
msgid "Your request has been queued on the server and will soon be processed."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:128
#: pretix/static/pretixbase/js/asynctask.js:127
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 "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:87
#: pretix/static/pretixbase/js/asynctask.js:169
#: pretix/static/pretixbase/js/asynctask.js:174
#: pretix/static/pretixbase/js/asynctask.js:86
#: pretix/static/pretixbase/js/asynctask.js:159
#: pretix/static/pretixbase/js/asynctask.js:164
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:90
#: pretix/static/pretixbase/js/asynctask.js:89
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:142
#: pretix/static/pretixbase/js/asynctask.js:141
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:177
#: pretix/static/pretixbase/js/asynctask.js:167
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:199
#: pretix/static/pretixbase/js/asynctask.js:189
msgid "We are processing your request …"
msgstr "Estem processant la vostra sol·licitud …"
#: pretix/static/pretixbase/js/asynctask.js:207
#: pretix/static/pretixbase/js/asynctask.js:197
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 "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:245
#: pretix/static/pretixbase/js/asynctask.js:235
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr ""
@@ -194,60 +194,56 @@ msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:237
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:444
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:441
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:69
#: pretix/static/pretixcontrol/js/ui/editor.js:43
msgid "Lead Scan QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:71
#: pretix/static/pretixcontrol/js/ui/editor.js:45
msgid "Check-in QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:311
#: pretix/static/pretixcontrol/js/ui/editor.js:269
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:517
#: pretix/static/pretixcontrol/js/ui/editor.js:461
msgid "Group of objects"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:523
#: pretix/static/pretixcontrol/js/ui/editor.js:467
msgid "Text object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:525
#: pretix/static/pretixcontrol/js/ui/editor.js:469
msgid "Barcode area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:527
msgid "Image area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:529
#: pretix/static/pretixcontrol/js/ui/editor.js:471
msgid "Powered by pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:531
#: pretix/static/pretixcontrol/js/ui/editor.js:473
msgid "Object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:535
#: pretix/static/pretixcontrol/js/ui/editor.js:477
msgid "Ticket design"
msgstr "Disseny del tiquet"
#: pretix/static/pretixcontrol/js/ui/editor.js:808
#: pretix/static/pretixcontrol/js/ui/editor.js:734
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:857
#: pretix/static/pretixcontrol/js/ui/editor.js:896
#: pretix/static/pretixcontrol/js/ui/editor.js:783
#: pretix/static/pretixcontrol/js/ui/editor.js:821
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:881
#: pretix/static/pretixcontrol/js/ui/editor.js:806
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
@@ -285,15 +281,15 @@ msgstr ""
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:801
#: pretix/static/pretixcontrol/js/ui/main.js:710
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:858
#: pretix/static/pretixcontrol/js/ui/main.js:767
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:873
#: pretix/static/pretixcontrol/js/ui/main.js:782
msgid "You have unsaved changes!"
msgstr ""

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