Merge branch 'master' into a11y-add-landmarks

This commit is contained in:
Richard Schreiber
2021-02-23 11:58:18 +01:00
62 changed files with 2882 additions and 1306 deletions

View File

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

View File

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

View File

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

View File

@@ -25,14 +25,6 @@ is_addon boolean If ``true``, it
defining add-ons for other products. 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 Endpoints
--------- ---------

View File

@@ -36,22 +36,6 @@ rules object Custom check-in
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response. 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 .. versionchanged:: 3.9
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``, The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
@@ -68,10 +52,6 @@ exit_all_at datetime Automatically c
Endpoints Endpoints
--------- ---------
.. versionchanged:: 1.15
The ``../status/`` detail endpoint has been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/ .. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/
Returns a list of all check-in lists within a given event. Returns a list of all check-in lists within a given event.
@@ -380,29 +360,6 @@ Endpoints
Order position 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/ .. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/
Returns a list of all order positions within a given event. The result is the same as Returns a list of all order positions within a given event. The result is the same as

View File

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

View File

@@ -46,24 +46,6 @@ internal_reference string Customer's refe
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
.. versionchanged:: 1.6
The attribute ``invoice_no`` has been dropped in favor of ``number`` which includes the number including the prefix,
since the prefix can now vary. Also, invoices now need to be identified by their ``number`` instead of the raw
number.
.. versionchanged:: 1.7
The attributes ``lines.tax_name``, ``foreign_currency_display``, ``foreign_currency_rate``, and
``foreign_currency_rate_date`` have been added.
.. versionchanged:: 1.9
The attribute ``internal_reference`` has been added.
.. versionchanged:: 3.4 .. versionchanged:: 3.4
The attribute ``lines.number`` has been added. The attribute ``lines.number`` has been added.

View File

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

View File

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

View File

@@ -26,14 +26,6 @@ description multi-lingual string A public descri
position integer An integer, used for sorting 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 Endpoints
--------- ---------

View File

@@ -118,44 +118,6 @@ bundles list of objects Definition of b
meta_data object Values set for event-specific meta data parameters. 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 .. versionchanged:: 3.7
The attribute ``meta_data`` has been added. The attribute ``meta_data`` has been added.

View File

@@ -94,60 +94,6 @@ last_modified datetime Last modificati
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
.. versionchanged:: 1.6
The ``invoice_address.country`` attribute contains a two-letter country code for all new orders. For old orders,
a custom text might still be returned.
.. versionchanged:: 1.7
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
deprecated in favor of the new ``fees`` attribute but will still be served and removed in 1.9.
.. versionchanged:: 1.9
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
The attribute ``invoice_address.internal_reference`` has been added.
.. versionchanged:: 1.13
The field ``checkin_attention`` has been added.
.. versionchanged:: 1.15
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate``, ``order.payment_fee_tax_value`` and
``order.payment_fee_tax_rule`` have finally been removed.
.. versionchanged:: 1.16
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
An endpoint for order creation as well as ``…/mark_refunded/`` has been added.
.. versionchanged:: 2.0
The ``order.payment_date`` and ``order.payment_provider`` attributes have been deprecated in favor of the new
nested ``payments`` and ``refunds`` resources, but will still be served and removed in 2.2. The ``require_approval``
attribute has been added, as have been the ``…/approve/`` and ``…/deny/`` endpoints.
.. versionchanged:: 2.3
The ``sales_channel`` attribute has been added.
.. versionchanged:: 2.4
``order.status`` can no longer be ``r``, ``…/mark_canceled/`` now accepts a ``cancellation_fee`` parameter and
``…/mark_refunded/`` has been deprecated.
.. versionchanged:: 2.5
The ``testmode`` attribute has been added and ``DELETE`` has been implemented for orders.
.. versionchanged:: 3.1
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
vouchers are now supported and many fields are now optional.
.. versionchanged:: 3.5 .. versionchanged:: 3.5
The ``order.fees.canceled`` attribute has been added. The ``order.fees.canceled`` attribute has been added.
@@ -233,30 +179,6 @@ pdf_data object Data object req
``pdf_data=true`` query parameter to your request. ``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 .. versionchanged:: 3.3
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
@@ -306,14 +228,6 @@ details object Payment-specifi
the object is empty. 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:
Order refund resource Order refund resource
@@ -334,17 +248,9 @@ execution_date datetime Date and time o
provider string Identification string of the payment provider provider string Identification string of the payment provider
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
.. versionchanged:: 2.0
This resource has been added.
List of all orders List of all orders
------------------ ------------------
.. versionchanged:: 1.15
Filtering for emails or order codes is now case-insensitive.
.. versionchanged:: 3.5 .. versionchanged:: 3.5
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added. The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
@@ -1450,21 +1356,6 @@ Sending e-mails
List of all order positions 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 .. versionchanged:: 3.5
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added. The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
@@ -1804,10 +1695,6 @@ Manipulating individual positions
Order payment endpoints Order payment endpoints
----------------------- -----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. versionchanged:: 3.6 .. versionchanged:: 3.6
Payments can now be created through the API. Payments can now be created through the API.
@@ -2087,10 +1974,6 @@ Order payment endpoints
Order refund endpoints Order refund endpoints
---------------------- ----------------------
.. versionchanged:: 2.0
These endpoints have been added.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/ .. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/refunds/
Returns a list of all refunds for an order. Returns a list of all refunds for an order.

View File

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

View File

@@ -75,28 +75,6 @@ dependency_value string An old version
for one value. **Deprecated.** 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 .. versionchanged:: 3.5
The attribute ``help_text`` has been added. The attribute ``help_text`` has been added.

View File

@@ -30,14 +30,6 @@ release_after_exit boolean Whether the quo
have been scanned at an exit. 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 .. versionchanged:: 3.10
The attribute ``release_after_exit`` has been added. The attribute ``release_after_exit`` has been added.

View File

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

View File

@@ -33,6 +33,7 @@ date_to datetime The sub-event's
date_admission datetime The sub-event's admission date (or ``null``) 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_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``) 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``) location multi-lingual string The sub-event location (or ``null``)
geo_lat float Latitude of the location (or ``null``) geo_lat float Latitude of the location (or ``null``)
geo_lon float Longitude of the location (or ``null``) geo_lon float Longitude of the location (or ``null``)
@@ -54,25 +55,6 @@ seat_category_mapping object An object mappi
last_modified datetime Last modification of this object 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 .. versionchanged:: 3.3
The attributes ``geo_lat`` and ``geo_lon`` have been added. The attributes ``geo_lat`` and ``geo_lon`` have been added.

View File

@@ -24,14 +24,6 @@ home_country string Merchant countr
``null`` or empty string ``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 Endpoints
--------- ---------

View File

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

View File

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

After

Width:  |  Height:  |  Size: 67 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

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

View File

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

View File

@@ -24,14 +24,6 @@ item_assignments list of objects Products this l
└ item integer Item ID └ 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 Endpoints
--------- ---------

View File

@@ -437,11 +437,6 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
</script> </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 .. versionchanged:: 3.6
Dynamically opening the widget has been added in pretix 3.6. Dynamically opening the widget has been added in pretix 3.6.

View File

@@ -409,8 +409,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
model = SubEvent model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission', fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public', 'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data', 'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
'seat_category_mapping', 'last_modified') 'meta_data', 'seat_category_mapping', 'last_modified')
def validate(self, data): def validate(self, data):
data = super().validate(data) data = super().validate(data)

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ from datetime import date, datetime, time
from django.core.validators import MinLengthValidator, RegexValidator from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, make_aware, now from django.utils.timezone import get_current_timezone, make_aware, now
@@ -88,6 +89,15 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self) return ObjectRelatedCache(self)
@cached_property
def all_logentries_link(self):
return reverse(
'control:organizer.log',
kwargs={
'organizer': self.slug,
}
)
@property @property
def has_gift_cards(self): def has_gift_cards(self):
return self.cache.get_or_set( return self.cache.get_or_set(

View File

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

View File

@@ -104,7 +104,7 @@ class ConfirmPaymentForm(forms.Form):
class CancelForm(ConfirmPaymentForm): class CancelForm(ConfirmPaymentForm):
send_email = forms.BooleanField( send_email = forms.BooleanField(
required=False, required=False,
label=_('Notify user by e-mail'), label=_('Notify customer by email'),
initial=True initial=True
) )
cancellation_fee = forms.DecimalField( cancellation_fee = forms.DecimalField(
@@ -139,6 +139,11 @@ class CancelForm(ConfirmPaymentForm):
class MarkPaidForm(ConfirmPaymentForm): class MarkPaidForm(ConfirmPaymentForm):
send_email = forms.BooleanField(
required=False,
label=_('Notify customer by email'),
initial=True
)
amount = forms.DecimalField( amount = forms.DecimalField(
required=True, required=True,
max_digits=10, decimal_places=2, max_digits=10, decimal_places=2,

View File

@@ -1,4 +1,4 @@
from bootstrap3.renderers import FieldRenderer from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
from bootstrap3.text import text_value from bootstrap3.text import text_value
from django.forms import CheckboxInput from django.forms import CheckboxInput
from django.forms.utils import flatatt from django.forms.utils import flatatt
@@ -58,3 +58,40 @@ class ControlFieldRenderer(FieldRenderer):
optional=not required and not isinstance(self.widget, CheckboxInput) optional=not required and not isinstance(self.widget, CheckboxInput)
) + html ) + html
return 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,8 +1,9 @@
from datetime import timedelta from datetime import datetime, timedelta
from urllib.parse import urlencode from urllib.parse import urlencode
from django import forms from django import forms
from django.forms import formset_factory from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -11,6 +12,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm 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.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField from pretix.base.reldate import RelativeDateTimeField
@@ -88,6 +90,142 @@ class SubEventBulkForm(SubEventForm):
del self.fields['date_admission'] 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: class SubEventItemOrVariationFormMixin:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item') self.item = kwargs.pop('item')
@@ -162,7 +300,7 @@ class SubEventMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property') self.property = kwargs.pop('property')
self.default = kwargs.pop('default', None) self.default = kwargs.pop('default', None)
self.disabled = kwargs.pop('disabled') self.disabled = kwargs.pop('disabled', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.property.allowed_values: if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField( self.fields['value'] = forms.ChoiceField(

View File

@@ -273,8 +273,15 @@ def _display_checkin(event, logentry):
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
plains = { plains = {
'pretix.object.cloned': _('This object has been created by cloning.'), '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.comment': _('The event\'s internal comment has been updated.'),
'pretix.event.canceled': _('The event has been canceled.'), '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.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'), 'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'), 'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),

View File

@@ -24,6 +24,7 @@
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
{% bootstrap_field form.amount layout='horizontal' %} {% bootstrap_field form.amount layout='horizontal' %}
{% bootstrap_field form.payment_date layout='horizontal' %} {% bootstrap_field form.payment_date layout='horizontal' %}
{% bootstrap_field form.send_email layout='horizontal' %}
{% if form.force %} {% if form.force %}
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %} {% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
{% endif %} {% endif %}

View File

@@ -15,7 +15,8 @@
{% endblocktrans %} {% endblocktrans %}
</a> </a>
</h1> </h1>
<form method="post" href=""> <form method="post" href=""
>
{% csrf_token %} {% csrf_token %}
<fieldset class="form-inline form-refund-choose"> <fieldset class="form-inline form-refund-choose">
<legend>{% trans "How should the refund be sent?" %}</legend> <legend>{% trans "How should the refund be sent?" %}</legend>

View File

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

View File

@@ -22,54 +22,68 @@
{% csrf_token %} {% csrf_token %}
{% bootstrap_form_errors sform %} {% bootstrap_form_errors sform %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
<div class="tabbed-form"> <div class="row">
<fieldset> <div class="col-xs-12 col-lg-10">
<legend>{% trans "General" %}</legend> <div class="tabbed-form">
{% bootstrap_field form.name layout="control" %} <fieldset>
{% bootstrap_field form.slug layout="control" %} <legend>{% trans "General" %}</legend>
{% if form.domain %} {% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.domain layout="control" %} {% bootstrap_field form.slug layout="control" %}
{% endif %} {% if form.domain %}
{% bootstrap_field sform.imprint_url layout="control" %} {% bootstrap_field form.domain layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %} {% endif %}
{% bootstrap_field sform.organizer_info_text layout="control" %} {% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field sform.event_team_provisioning layout="control" %} {% bootstrap_field sform.contact_mail layout="control" %}
</fieldset> {% bootstrap_field sform.organizer_info_text layout="control" %}
<fieldset> {% bootstrap_field sform.event_team_provisioning layout="control" %}
<legend>{% trans "Organizer page" %}</legend> </fieldset>
{% bootstrap_field sform.organizer_logo_image layout="control" %} <fieldset>
{% bootstrap_field sform.organizer_logo_image_large layout="control" %} <legend>{% trans "Organizer page" %}</legend>
{% bootstrap_field sform.organizer_homepage_text layout="control" %} {% bootstrap_field sform.organizer_logo_image layout="control" %}
{% bootstrap_field sform.event_list_type layout="control" %} {% bootstrap_field sform.organizer_logo_image_large layout="control" %}
{% bootstrap_field sform.event_list_availability layout="control" %} {% bootstrap_field sform.organizer_homepage_text layout="control" %}
{% bootstrap_field sform.organizer_link_back layout="control" %} {% bootstrap_field sform.event_list_type layout="control" %}
</fieldset> {% bootstrap_field sform.event_list_availability layout="control" %}
<fieldset> {% bootstrap_field sform.organizer_link_back layout="control" %}
<legend>{% trans "Localization" %}</legend> </fieldset>
{% bootstrap_field sform.locales layout="control" %} <fieldset>
{% bootstrap_field sform.region layout="control" %} <legend>{% trans "Localization" %}</legend>
</fieldset> {% bootstrap_field sform.locales layout="control" %}
<fieldset> {% bootstrap_field sform.region layout="control" %}
<legend>{% trans "Shop design" %}</legend> </fieldset>
<p class="help-block"> <fieldset>
{% blocktrans trimmed %} <legend>{% trans "Shop design" %}</legend>
These settings will be used for the organizer page as well as for the default settings <p class="help-block">
for all events in this account that do not have their own design settings. {% blocktrans trimmed %}
{% endblocktrans %} These settings will be used for the organizer page as well as for the default settings
</p> for all events in this account that do not have their own design settings.
{% bootstrap_field sform.primary_color layout="control" %} {% endblocktrans %}
{% bootstrap_field sform.theme_color_success layout="control" %} </p>
{% bootstrap_field sform.theme_color_danger layout="control" %} {% bootstrap_field sform.primary_color layout="control" %}
{% bootstrap_field sform.theme_color_background layout="control" %} {% bootstrap_field sform.theme_color_success layout="control" %}
{% bootstrap_field sform.theme_round_borders layout="control" %} {% bootstrap_field sform.theme_color_danger layout="control" %}
{% bootstrap_field sform.primary_font layout="control" %} {% bootstrap_field sform.theme_color_background layout="control" %}
{% bootstrap_field sform.favicon layout="control" %} {% bootstrap_field sform.theme_round_borders layout="control" %}
</fieldset> {% bootstrap_field sform.primary_font layout="control" %}
<fieldset> {% bootstrap_field sform.favicon layout="control" %}
<legend>{% trans "Gift cards" %}</legend> </fieldset>
{% bootstrap_field sform.giftcard_expiry_years layout="control" %} <fieldset>
{% bootstrap_field sform.giftcard_length layout="control" %} <legend>{% trans "Gift cards" %}</legend>
</fieldset> {% 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>
{% include "pretixcontrol/includes/logs.html" with obj=organizer %}
</div>
</div>
</div> </div>
<div class="form-group submit-group"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">

View File

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

View File

@@ -0,0 +1,85 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Organizer logs" %}{% endblock %}
{% block inside %}
<h1>{% trans "Organizer logs" %}</h1>
<form class="form-inline helper-display-inline" action="" method="get">
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
<input type="hidden" name="object" value="{{ request.GET.object }}">
<p>
<select name="user" class="form-control">
<option value="">{% trans "All actions" %}</option>
{% for up in userlist %}
{% if up.user__id %}
<option value="{{ up.user__id }}"
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
{{ up.user__email }}
</option>
{% endif %}
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
<ul class="list-group">
{% for log in logs %}
<li class="list-group-item logentry">
<div class="row">
<div class="col-lg-2 col-sm-6 col-xs-12">
<span class="fa fa-clock-o"></span>
{{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if log.shredded %}
<span class="fa fa-eraser fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "Personal data was cleared from this log entry." %}">
</span>
{% endif %}
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
</span>
{% else %}
<span class="fa fa-user fa-fw"></span>
{% endif %}
{{ log.user.get_full_name }}
{% if log.oauth_application %}
<br><span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.device %}
<span class="fa fa-mobile fa-fw"></span>
{{ log.device.name }}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">
{% if log.display_object %}
<span class="fa fa-flag"></span> {{ log.display_object|safe }}
{% endif %}
</div>
<div class="col-lg-6 col-sm-12 col-xs-12">
{{ log.display }}
{% if staff_session %}
<a href="" class="btn btn-default btn-xs" data-expandlogs data-id="{{ log.pk }}">
<span class="fa-eye fa fa-fw"></span>
{% trans "Inspect" %}
</a>
{% endif %}
</div>
</div>
</li>
{% empty %}
<div class="list-group-item">
<em>{% trans "No results" %}</em>
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

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

@@ -22,14 +22,17 @@
</div> </div>
{% else %} {% else %}
<form class="row filter-form" action="" method="get"> <form class="row filter-form" action="" method="get">
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query layout='inline' %} {% bootstrap_field filter_form.query layout='inline' %}
</div> </div>
<div class="col-md-3 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status layout='inline' %} {% bootstrap_field filter_form.status layout='inline' %}
</div> </div>
<div class="col-md-2 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.date layout='inline' %} {% bootstrap_field filter_form.date_from layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.date_until layout='inline' %}
</div> </div>
<div class="col-md-2 col-sm-6 col-xs-12"> <div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.weekday layout='inline' %} {% bootstrap_field filter_form.weekday layout='inline' %}
@@ -43,23 +46,28 @@
</button> </button>
</div> </div>
</form> </form>
<p> {% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}" <p>
class="btn btn-default"><i class="fa fa-plus"></i> <a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
{% trans "Create a new date" context "subevent" %}</a> class="btn btn-default"><i class="fa fa-plus"></i>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}" {% trans "Create a new date" context "subevent" %}</a>
class="btn btn-default"><i class="fa fa-plus"></i> <a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
{% trans "Create many new dates" context "subevent" %}</a> class="btn btn-default"><i class="fa fa-plus"></i>
</p> {% trans "Create many new dates" context "subevent" %}</a>
</p>
{% endif %}
<form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post"> <form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
{% csrf_token %} {% csrf_token %}
<div class="hidden">
{{ filter_form.as_p }}
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-quotas"> <table class="table table-hover table-quotas">
<thead> <thead>
<tr> <tr>
<th> <th>
{% if "can_change_event_settings" in request.eventpermset %} {% if "can_change_event_settings" in request.eventpermset %}
<input type="checkbox" data-toggle-table/> <label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %} {% endif %}
</th> </th>
<th> <th>
@@ -67,28 +75,40 @@
</th> </th>
<th> <th>
{% trans "Begin" %} {% trans "Begin" %}
<a href="?{% url_replace request '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-down"></i></a>
<a href="?{% url_replace request 'ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'filter-ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
<th> <th>
{% trans "Paid tickets per quota" %} {% trans "Paid tickets per quota" %}
<a href="?{% url_replace request 'ordering' '-sum_tickets_paid' %}"><i class="fa fa-caret-down"></i></a> <a href="?{% url_replace request 'filter-ordering' '-sum_tickets_paid' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'sum_tickets_paid' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'filter-ordering' 'sum_tickets_paid' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
<th> <th>
{% trans "Status" %} {% trans "Status" %}
<a href="?{% url_replace request 'ordering' '-active' %}"><i class="fa fa-caret-down"></i></a> <a href="?{% url_replace request 'filter-ordering' '-active' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'active' %}"><i class="fa fa-caret-up"></i></a> <a href="?{% url_replace request 'filter-ordering' 'active' %}"><i class="fa fa-caret-up"></i></a>
</th> </th>
<th></th> <th></th>
</tr> </tr>
{% if "can_change_event_settings" in request.eventpermset %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all">
</td>
<td colspan="5">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead> </thead>
<tbody> <tbody>
{% for s in subevents %} {% for s in subevents %}
<tr> <tr>
<td> <td>
{% if "can_change_event_settings" in request.eventpermset %} {% if "can_change_event_settings" in request.eventpermset %}
<input type="checkbox" name="subevent" class="" value="{{ s.pk }}"/> <label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label"><input type="checkbox" name="subevent" class="" value="{{ s.pk }}"/></label>
{% endif %} {% endif %}
</td> </td>
<td> <td>
@@ -150,6 +170,10 @@
<button type="submit" class="btn btn-default btn-save" name="action" value="delete"> <button type="submit" class="btn btn-default btn-save" name="action" value="delete">
{% trans "Delete selected" %} {% trans "Delete selected" %}
</button> </button>
<button type="submit" class="btn btn-default btn-save" name="action" value="disable"
formaction="{% url "control:event.subevents.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Change selected" %}
</button>
<button type="submit" class="btn btn-default btn-save" name="action" value="enable"> <button type="submit" class="btn btn-default btn-save" name="action" value="enable">
{% trans "Enable selected" %} {% trans "Enable selected" %}
</button> </button>

View File

@@ -122,6 +122,7 @@ urlpatterns = [
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(), url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(),
name='organizer.team.delete'), name='organizer.team.delete'),
url(r'^organizer/(?P<organizer>[^/]+)/slugrng', main.SlugRNG.as_view(), name='events.add.slugrng'), 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/$', organizer.ExportView.as_view(), name='organizer.export'),
url(r'^organizer/(?P<organizer>[^/]+)/export/do$', organizer.ExportDoView.as_view(), name='organizer.export.do'), 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'), url(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
@@ -173,6 +174,7 @@ urlpatterns = [
url(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'), 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_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_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/$', item.ItemList.as_view(), name='event.items'),
url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'), 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'), url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),

View File

@@ -1105,6 +1105,7 @@ class OrderTransition(OrderView):
try: try:
p.confirm(user=self.request.user, count_waitinglist=False, payment_date=payment_date, 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)) force=self.mark_paid_form.cleaned_data.get('force', False))
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
p.state = OrderPayment.PAYMENT_STATE_FAILED p.state = OrderPayment.PAYMENT_STATE_FAILED
@@ -2079,11 +2080,17 @@ class ExportMixin:
exporters.append(ex) exporters.append(ex)
return exporters return exporters
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['exporters'] = self.exporters
return ctx
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View):
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
permission = 'can_view_orders' permission = 'can_view_orders'
known_errortypes = ['ExportError'] known_errortypes = ['ExportError']
task = export task = export
template_name = 'pretixcontrol/orders/export.html'
def get_success_message(self, value): def get_success_message(self, value):
return None return None
@@ -2103,6 +2110,11 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
if ex.identifier == self.request.POST.get("exporter"): if ex.identifier == self.request.POST.get("exporter"):
return ex 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): def post(self, request, *args, **kwargs):
if not self.exporter: if not self.exporter:
messages.error(self.request, _('The selected exporter was not found.')) messages.error(self.request, _('The selected exporter was not found.'))
@@ -2112,16 +2124,8 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
})) }))
if not self.exporter.form.is_valid(): if not self.exporter.form.is_valid():
messages.error( messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
self.request, return self.get(request, *args, **kwargs)
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 = CachedFile(web_download=True, session_key=request.session.session_key)
cf.date = now() cf.date = now()
@@ -2134,11 +2138,6 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, TemplateView):
permission = 'can_view_orders' permission = 'can_view_orders'
template_name = 'pretixcontrol/orders/export.html' 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): class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
model = OrderRefund model = OrderRefund

View File

@@ -51,6 +51,7 @@ from pretix.control.forms.organizer import (
GiftCardUpdateForm, OrganizerDeleteForm, OrganizerForm, GiftCardUpdateForm, OrganizerDeleteForm, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm,
) )
from pretix.control.logdisplay import OVERVIEW_BANLIST
from pretix.control.permissions import ( from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin, AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
) )
@@ -1147,8 +1148,9 @@ class ExportMixin:
organizer=self.request.organizer organizer=self.request.organizer
) )
responses = register_multievent_data_exporters.send(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)): for ex in sorted([response(events) for r, response in responses if response], key=lambda ex: str(ex.verbose_name)):
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"): if id and ex.identifier != id:
continue continue
# Use form parse cycle to generate useful defaults # Use form parse cycle to generate useful defaults
@@ -1180,10 +1182,16 @@ class ExportMixin:
exporters.append(ex) exporters.append(ex)
return exporters return exporters
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['exporters'] = self.exporters
return ctx
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, View):
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
known_errortypes = ['ExportError'] known_errortypes = ['ExportError']
task = multiexport task = multiexport
template_name = 'pretixcontrol/organizers/export.html'
def get_success_message(self, value): def get_success_message(self, value):
return None return None
@@ -1202,6 +1210,11 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
if ex.identifier == self.request.POST.get("exporter"): if ex.identifier == self.request.POST.get("exporter"):
return ex 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): def post(self, request, *args, **kwargs):
if not self.exporter: if not self.exporter:
messages.error(self.request, _('The selected exporter was not found.')) messages.error(self.request, _('The selected exporter was not found.'))
@@ -1231,11 +1244,6 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, TemplateView): class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, TemplateView):
template_name = 'pretixcontrol/organizers/export.html' 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): class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Gate model = Gate
@@ -1427,3 +1435,24 @@ class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionR
self.object.delete() self.object.delete()
messages.success(request, _('The selected property has been deleted.')) messages.success(request, _('The selected property has been deleted.'))
return redirect(success_url) 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

@@ -1,14 +1,17 @@
import copy import copy
from collections import defaultdict
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
from django.contrib import messages from django.contrib import messages
from django.core.files import File from django.core.files import File
from django.db import connections, transaction from django.db import connections, transaction
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum from django.db.models import (
from django.db.models.functions import Coalesce Count, F, IntegerField, OuterRef, Prefetch, Subquery, Sum,
)
from django.db.models.functions import Coalesce, TruncDate, TruncTime
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.formats import get_format from django.utils.formats import get_format
@@ -16,7 +19,9 @@ from django.utils.functional import cached_property
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View from django.views import View
from django.views.generic import CreateView, DeleteView, ListView, UpdateView from django.views.generic import (
CreateView, DeleteView, FormView, ListView, UpdateView,
)
from pretix.base.models import CartPosition, LogEntry from pretix.base.models import CartPosition, LogEntry
from pretix.base.models.checkin import CheckinList from pretix.base.models.checkin import CheckinList
@@ -31,24 +36,27 @@ from pretix.control.forms.checkin import SimpleCheckinListForm
from pretix.control.forms.filter import SubEventFilterForm from pretix.control.forms.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import ( from pretix.control.forms.subevents import (
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkForm, CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkEditForm,
SubEventForm, SubEventItemForm, SubEventItemVariationForm, SubEventBulkForm, SubEventForm, SubEventItemForm,
SubEventMetaValueForm, TimeFormSet, SubEventItemVariationForm, SubEventMetaValueForm, TimeFormSet,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import subevent_forms from pretix.control.signals import subevent_forms
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.control.views.event import MetaDataEditorMixin from pretix.control.views.event import MetaDataEditorMixin
from pretix.helpers import GroupConcat
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView): class SubEventQueryMixin:
model = SubEvent
context_object_name = 'subevents'
template_name = 'pretixcontrol/subevents/index.html'
permission = 'can_change_settings'
def get_queryset(self): @cached_property
def request_data(self):
if self.request.method == "POST":
return self.request.POST
return self.request.GET
def get_queryset(self, list=False):
sum_tickets_paid = Quota.objects.filter( sum_tickets_paid = Quota.objects.filter(
subevent=OuterRef('pk') subevent=OuterRef('pk')
).order_by().values('subevent').annotate( ).order_by().values('subevent').annotate(
@@ -56,18 +64,39 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
).values( ).values(
's' 's'
) )
qs = self.request.event.subevents
qs = self.request.event.subevents.annotate( if list:
sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField()) qs = qs.annotate(
).prefetch_related( sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField())
Prefetch('quotas', ).prefetch_related(
queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'), Prefetch('quotas',
to_attr='first_quotas') queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
) to_attr='first_quotas')
)
if self.filter_form.is_valid(): if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs) 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 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): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form ctx['filter_form'] = self.filter_form
@@ -95,10 +124,6 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
) )
return ctx return ctx
@cached_property
def filter_form(self):
return SubEventFilterForm(data=self.request.GET)
class SubEventDelete(EventPermissionRequiredMixin, DeleteView): class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
model = SubEvent model = SubEvent
@@ -535,19 +560,13 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
return formlist return formlist
class SubEventBulkAction(EventPermissionRequiredMixin, View): class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View):
permission = 'can_change_settings' permission = 'can_change_settings'
@cached_property
def objects(self):
return self.request.event.subevents.filter(
id__in=self.request.POST.getlist('subevent')
)
@transaction.atomic @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'disable': if request.POST.get('action') == 'disable':
for obj in self.objects: for obj in self.get_queryset():
obj.log_action( obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={ 'pretix.subevent.changed', user=self.request.user, data={
'active': False 'active': False
@@ -557,7 +576,7 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
obj.save(update_fields=['active']) obj.save(update_fields=['active'])
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.')) messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.'))
elif request.POST.get('action') == 'enable': elif request.POST.get('action') == 'enable':
for obj in self.objects: for obj in self.get_queryset():
obj.log_action( obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={ 'pretix.subevent.changed', user=self.request.user, data={
'active': True 'active': True
@@ -568,11 +587,11 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.')) messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.'))
elif request.POST.get('action') == 'delete': elif request.POST.get('action') == 'delete':
return render(request, 'pretixcontrol/subevents/delete_bulk.html', { return render(request, 'pretixcontrol/subevents/delete_bulk.html', {
'allowed': self.objects.filter(orderposition__isnull=True), 'allowed': self.get_queryset().filter(orderposition__isnull=True),
'forbidden': self.objects.filter(orderposition__isnull=False), 'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(),
}) })
elif request.POST.get('action') == 'delete_confirm': elif request.POST.get('action') == 'delete_confirm':
for obj in self.objects: for obj in self.get_queryset():
if obj.allow_delete(): if obj.allow_delete():
CartPosition.objects.filter(addon_to__subevent=obj).delete() CartPosition.objects.filter(addon_to__subevent=obj).delete()
obj.cartposition_set.all().delete() obj.cartposition_set.all().delete()
@@ -899,3 +918,537 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
messages.error(self.request, _('We could not save your changes. See below for details.')) messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.form_invalid(form) 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,7 +67,8 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
headers = [ headers = [
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'), _('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat') _('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
_('Comment')
] ]
writer.writerow(headers) writer.writerow(headers)
@@ -92,7 +93,8 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
v.tag, v.tag,
str(v.redeemed), str(v.redeemed),
str(v.max_usages), str(v.max_usages),
str(v.seat) if v.seat else "" str(v.seat) if v.seat else "",
str(v.comment) if v.comment else ""
] ]
writer.writerow(row) writer.writerow(row)

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-27 17:45+0000\n" "POT-Creation-Date: 2021-01-27 17:45+0000\n"
"PO-Revision-Date: 2020-12-14 10:00+0000\n" "PO-Revision-Date: 2021-02-16 06:00+0000\n"
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n" "Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/" "Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix/cs/>"
">\n" "\n"
"Language: cs\n" "Language: cs\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 3.10.3\n" "X-Generator: Weblate 4.4.2\n"
#: htmlcov/pretix_control_views_dashboards_py.html:898 #: htmlcov/pretix_control_views_dashboards_py.html:898
#: pretix/control/templates/pretixcontrol/events/index.html:144 #: pretix/control/templates/pretixcontrol/events/index.html:144
@@ -218,17 +218,17 @@ msgstr ""
#: pretix/api/serializers/organizer.py:142 #: pretix/api/serializers/organizer.py:142
#: pretix/control/views/organizer.py:539 #: pretix/control/views/organizer.py:539
msgid "pretix account invitation" msgid "pretix account invitation"
msgstr "" msgstr "pozvánka k pretix účtu"
#: pretix/api/serializers/organizer.py:164 #: pretix/api/serializers/organizer.py:164
#: pretix/control/views/organizer.py:638 #: pretix/control/views/organizer.py:638
msgid "This user already has been invited for this team." msgid "This user already has been invited for this team."
msgstr "Tento uživatel byl již pozván do této skupiny." msgstr "Tento uživatel byl již pozván do tohoto týmu."
#: pretix/api/serializers/organizer.py:180 #: pretix/api/serializers/organizer.py:180
#: pretix/control/views/organizer.py:655 #: pretix/control/views/organizer.py:655
msgid "This user already has permissions for this team." msgid "This user already has permissions for this team."
msgstr "" msgstr "Tento uživatel již má nastavena práva pro tento tým."
#: pretix/api/views/oauth.py:85 pretix/control/logdisplay.py:356 #: pretix/api/views/oauth.py:85 pretix/control/logdisplay.py:356
#, python-brace-format #, python-brace-format
@@ -240,7 +240,7 @@ msgstr ""
#: pretix/api/views/order.py:460 pretix/control/views/orders.py:1186 #: pretix/api/views/order.py:460 pretix/control/views/orders.py:1186
#: pretix/presale/views/order.py:663 pretix/presale/views/order.py:728 #: pretix/presale/views/order.py:663 pretix/presale/views/order.py:728
msgid "You cannot generate an invoice for this order." msgid "You cannot generate an invoice for this order."
msgstr "" msgstr "Nemůžete vygenerovat fakturu pro tuto objednávku."
#: pretix/api/views/order.py:465 pretix/control/views/orders.py:1188 #: pretix/api/views/order.py:465 pretix/control/views/orders.py:1188
#: pretix/presale/views/order.py:665 pretix/presale/views/order.py:730 #: pretix/presale/views/order.py:665 pretix/presale/views/order.py:730
@@ -1868,7 +1868,7 @@ msgstr "Časové pásmo"
#: pretix/base/models/auth.py:108 #: pretix/base/models/auth.py:108
msgid "Two-factor authentication is required to log in" msgid "Two-factor authentication is required to log in"
msgstr "" msgstr "Pro přihlášení je vyžadována dvoufaktorová autentizace"
#: pretix/base/models/auth.py:112 #: pretix/base/models/auth.py:112
msgid "Receive notifications according to my settings below" msgid "Receive notifications according to my settings below"
@@ -1991,7 +1991,7 @@ msgstr ""
#: pretix/base/models/devices.py:91 #: pretix/base/models/devices.py:91
#: pretix/control/templates/pretixcontrol/organizers/gates.html:16 #: pretix/control/templates/pretixcontrol/organizers/gates.html:16
msgid "Gate" msgid "Gate"
msgstr "" msgstr "Brána"
#: pretix/base/models/devices.py:109 #: pretix/base/models/devices.py:109
#: pretix/control/templates/pretixcontrol/organizers/devices.html:38 #: pretix/control/templates/pretixcontrol/organizers/devices.html:38
@@ -2008,7 +2008,7 @@ msgstr ""
#: pretix/base/models/event.py:46 #: pretix/base/models/event.py:46
msgid "The end of the event has to be later than its start." msgid "The end of the event has to be later than its start."
msgstr "" msgstr "Konec události musí být pozdější než začátek."
#: pretix/base/models/event.py:353 #: pretix/base/models/event.py:353
msgid "" msgid ""
@@ -2030,7 +2030,7 @@ msgstr ""
#: pretix/base/models/event.py:369 #: pretix/base/models/event.py:369
msgid "Shop is live" msgid "Shop is live"
msgstr "" msgstr "Obchod je spuštěný"
#: pretix/base/models/event.py:371 #: pretix/base/models/event.py:371
msgid "Event currency" msgid "Event currency"
@@ -2057,7 +2057,7 @@ msgstr ""
#: pretix/base/models/event.py:380 pretix/base/models/event.py:1106 #: pretix/base/models/event.py:380 pretix/base/models/event.py:1106
msgid "Show in lists" msgid "Show in lists"
msgstr "" msgstr "Zobrazit v seznamu"
#: pretix/base/models/event.py:381 #: pretix/base/models/event.py:381
msgid "" msgid ""
@@ -2068,7 +2068,7 @@ msgstr ""
#: pretix/base/models/event.py:384 pretix/base/models/event.py:1120 #: pretix/base/models/event.py:384 pretix/base/models/event.py:1120
#: pretix/control/forms/subevents.py:75 #: pretix/control/forms/subevents.py:75
msgid "End of presale" msgid "End of presale"
msgstr "" msgstr "Konec předprodeje"
#: pretix/base/models/event.py:385 pretix/base/models/event.py:1121 #: pretix/base/models/event.py:385 pretix/base/models/event.py:1121
#: pretix/control/forms/subevents.py:76 #: pretix/control/forms/subevents.py:76
@@ -2080,7 +2080,7 @@ msgstr ""
#: pretix/base/models/event.py:390 pretix/base/models/event.py:1126 #: pretix/base/models/event.py:390 pretix/base/models/event.py:1126
#: pretix/control/forms/subevents.py:69 #: pretix/control/forms/subevents.py:69
msgid "Start of presale" msgid "Start of presale"
msgstr "" msgstr "Začátek předprodeje"
#: pretix/base/models/event.py:391 pretix/base/models/event.py:1127 #: pretix/base/models/event.py:391 pretix/base/models/event.py:1127
#: pretix/control/forms/subevents.py:70 #: pretix/control/forms/subevents.py:70
@@ -2101,13 +2101,13 @@ msgstr ""
#: pretix/base/models/event.py:416 pretix/control/navigation.py:44 #: pretix/base/models/event.py:416 pretix/control/navigation.py:44
msgid "Plugins" msgid "Plugins"
msgstr "" msgstr "Zásuvné moduly"
#: pretix/base/models/event.py:419 #: pretix/base/models/event.py:419
#: pretix/control/templates/pretixcontrol/event/index.html:143 #: pretix/control/templates/pretixcontrol/event/index.html:143
#: pretix/control/templates/pretixcontrol/order/index.html:865 #: pretix/control/templates/pretixcontrol/order/index.html:865
msgid "Internal comment" msgid "Internal comment"
msgstr "" msgstr "Interní poznámka"
#: pretix/base/models/event.py:423 pretix/control/forms/event.py:211 #: pretix/base/models/event.py:423 pretix/control/forms/event.py:211
#: pretix/control/forms/filter.py:988 #: pretix/control/forms/filter.py:988
@@ -2133,7 +2133,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/search/orders.html:44 #: pretix/control/templates/pretixcontrol/search/orders.html:44
#: pretix/presale/templates/pretixpresale/event/waitinglist.html:18 #: pretix/presale/templates/pretixpresale/event/waitinglist.html:18
msgid "Event" msgid "Event"
msgstr "" msgstr "Událost"
#: pretix/base/models/event.py:437 pretix/control/navigation.py:305 #: pretix/base/models/event.py:437 pretix/control/navigation.py:305
#: pretix/control/navigation.py:407 #: pretix/control/navigation.py:407
@@ -2144,7 +2144,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/webhooks.html:37 #: pretix/control/templates/pretixcontrol/organizers/webhooks.html:37
#: pretix/control/views/organizer.py:1205 #: pretix/control/views/organizer.py:1205
msgid "Events" msgid "Events"
msgstr "" msgstr "Události"
#: pretix/base/models/event.py:951 #: pretix/base/models/event.py:951
msgid "" msgid ""
@@ -2182,7 +2182,7 @@ msgstr ""
#: pretix/control/forms/filter.py:1250 #: pretix/control/forms/filter.py:1250
#: pretix/control/templates/pretixcontrol/users/index.html:46 #: pretix/control/templates/pretixcontrol/users/index.html:46
msgid "Active" msgid "Active"
msgstr "" msgstr "Aktivní"
#: pretix/base/models/event.py:1103 #: pretix/base/models/event.py:1103
msgid "" msgid ""
@@ -6399,7 +6399,7 @@ msgstr ""
#: pretix/base/settings.py:1694 #: pretix/base/settings.py:1694
msgid "Primary color" msgid "Primary color"
msgstr "" msgstr "Hlavní barva"
#: pretix/base/settings.py:1714 #: pretix/base/settings.py:1714
msgid "Accent color for success" msgid "Accent color for success"
@@ -9737,7 +9737,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/dashboard.html:3 #: pretix/control/templates/pretixcontrol/dashboard.html:3
#: pretix/control/templates/pretixcontrol/dashboard.html:5 #: pretix/control/templates/pretixcontrol/dashboard.html:5
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr "Nástěnka"
#: pretix/control/navigation.py:28 pretix/control/navigation.py:329 #: pretix/control/navigation.py:28 pretix/control/navigation.py:329
#: pretix/control/navigation.py:424 #: pretix/control/navigation.py:424
@@ -10328,7 +10328,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:123 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:123
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:80 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:80
msgid "Filter" msgid "Filter"
msgstr "" msgstr "Filtrovat"
#: pretix/control/templates/pretixcontrol/checkin/index.html:52 #: pretix/control/templates/pretixcontrol/checkin/index.html:52
msgid "No attendee record was found." msgid "No attendee record was found."
@@ -10558,11 +10558,11 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/dashboard.html:9 #: pretix/control/templates/pretixcontrol/dashboard.html:9
msgid "Go to event" msgid "Go to event"
msgstr "" msgstr "Jít na událost"
#: pretix/control/templates/pretixcontrol/dashboard.html:15 #: pretix/control/templates/pretixcontrol/dashboard.html:15
msgid "Your upcoming events" msgid "Your upcoming events"
msgstr "" msgstr "Vaše nadcházející události"
#: pretix/control/templates/pretixcontrol/dashboard.html:20 #: pretix/control/templates/pretixcontrol/dashboard.html:20
#: pretix/control/templates/pretixcontrol/events/create_base.html:4 #: pretix/control/templates/pretixcontrol/events/create_base.html:4
@@ -10571,11 +10571,11 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/events/index.html:57 #: pretix/control/templates/pretixcontrol/events/index.html:57
#: pretix/control/templates/pretixcontrol/organizers/detail.html:12 #: pretix/control/templates/pretixcontrol/organizers/detail.html:12
msgid "Create a new event" msgid "Create a new event"
msgstr "" msgstr "Vytvořit novou událost"
#: pretix/control/templates/pretixcontrol/dashboard.html:39 #: pretix/control/templates/pretixcontrol/dashboard.html:39
msgid "View all upcoming events" msgid "View all upcoming events"
msgstr "" msgstr "Zobrazit všechny nadcházející události"
#: pretix/control/templates/pretixcontrol/dashboard.html:44 #: pretix/control/templates/pretixcontrol/dashboard.html:44
msgid "Your most recent events" msgid "Your most recent events"
@@ -13546,7 +13546,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/orders/index.html:98 #: pretix/control/templates/pretixcontrol/orders/index.html:98
msgid "Remove filter" msgid "Remove filter"
msgstr "" msgstr "Odstranit filtr"
#: pretix/control/templates/pretixcontrol/orders/index.html:116 #: pretix/control/templates/pretixcontrol/orders/index.html:116
msgid "Order paid / total" msgid "Order paid / total"
@@ -13618,7 +13618,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/orders/overview.html:69 #: pretix/control/templates/pretixcontrol/orders/overview.html:69
#: pretix/plugins/reports/exporters.py:259 #: pretix/plugins/reports/exporters.py:259
msgid "Purchased" msgid "Purchased"
msgstr "" msgstr "Zakoupeno"
#: pretix/control/templates/pretixcontrol/orders/overview.html:178 #: pretix/control/templates/pretixcontrol/orders/overview.html:178
msgid "" msgid ""
@@ -13679,7 +13679,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/device_connect.html:14 #: pretix/control/templates/pretixcontrol/organizers/device_connect.html:14
msgid "Download pretixSCAN" msgid "Download pretixSCAN"
msgstr "" msgstr "Stáhnout pretixSCAN"
#: pretix/control/templates/pretixcontrol/organizers/device_connect.html:18 #: pretix/control/templates/pretixcontrol/organizers/device_connect.html:18
msgid "" msgid ""
@@ -17735,12 +17735,12 @@ msgstr ""
#: pretix/plugins/reports/exporters.py:120 #: pretix/plugins/reports/exporters.py:120
#, python-format #, python-format
msgid "Page %d" msgid "Page %d"
msgstr "" msgstr "Strana %d"
#: pretix/plugins/reports/exporters.py:122 #: pretix/plugins/reports/exporters.py:122
#, python-format #, python-format
msgid "Created: %s" msgid "Created: %s"
msgstr "" msgstr "Vytvořeno: %s"
#: pretix/plugins/reports/exporters.py:162 #: pretix/plugins/reports/exporters.py:162
msgid "Order overview (PDF)" msgid "Order overview (PDF)"
@@ -17810,10 +17810,8 @@ msgstr ""
#: pretix/plugins/reports/exporters.py:668 #: pretix/plugins/reports/exporters.py:668
#: pretix/plugins/reports/exporters.py:713 #: pretix/plugins/reports/exporters.py:713
#, fuzzy
#| msgid "Country"
msgid "Country code" msgid "Country code"
msgstr "Stát" msgstr "Kód země"
#: pretix/plugins/returnurl/__init__.py:9 #: pretix/plugins/returnurl/__init__.py:9
#: pretix/plugins/returnurl/__init__.py:12 #: pretix/plugins/returnurl/__init__.py:12

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-12-22 11:05+0000\n" "POT-Creation-Date: 2020-12-22 11:05+0000\n"
"PO-Revision-Date: 2021-01-26 03:00+0000\n" "PO-Revision-Date: 2021-02-15 05:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n" "Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix/"
"fi/>\n" "fi/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.10.3\n" "X-Generator: Weblate 4.4.2\n"
#: htmlcov/pretix_control_views_dashboards_py.html:898 #: htmlcov/pretix_control_views_dashboards_py.html:898
#: pretix/control/templates/pretixcontrol/events/index.html:144 #: pretix/control/templates/pretixcontrol/events/index.html:144
@@ -3136,10 +3136,8 @@ msgstr "Ulkoinen"
#: pretix/base/models/orders.py:1720 #: pretix/base/models/orders.py:1720
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:191 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:191
#, fuzzy
#| msgid "Refund order"
msgid "Refund reason" msgid "Refund reason"
msgstr "Hyvitä tilaus" msgstr "Hyvityksen syy"
#: pretix/base/models/orders.py:1721 #: pretix/base/models/orders.py:1721
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:192 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:192
@@ -4537,10 +4535,8 @@ msgstr ""
#: pretix/base/services/cancelevent.py:200 #: pretix/base/services/cancelevent.py:200
#: pretix/base/services/cancelevent.py:258 #: pretix/base/services/cancelevent.py:258
#, fuzzy
#| msgid "Event created"
msgid "Event canceled" msgid "Event canceled"
msgstr "Tapahtuma luotu" msgstr "Tapahtuma peruttu"
#: pretix/base/services/cart.py:52 pretix/base/services/orders.py:73 #: pretix/base/services/cart.py:52 pretix/base/services/orders.py:73
msgid "" msgid ""
@@ -7817,16 +7813,12 @@ msgid "Order placed before"
msgstr "Tilattu jälkeen" msgstr "Tilattu jälkeen"
#: pretix/control/forms/filter.py:468 #: pretix/control/forms/filter.py:468
#, fuzzy
#| msgid "Order payments and refunds"
msgid "Minimal sum of payments and refunds" msgid "Minimal sum of payments and refunds"
msgstr "Tilauksen maksut ja palautukset" msgstr "Maksujen ja palautusten vähimmäissumma"
#: pretix/control/forms/filter.py:473 #: pretix/control/forms/filter.py:473
#, fuzzy
#| msgid "Order payments and refunds"
msgid "Maximal sum of payments and refunds" msgid "Maximal sum of payments and refunds"
msgstr "Tilauksen maksut ja palautukset" msgstr "Tilausten ja maksujen enimmäissumma"
#: pretix/control/forms/filter.py:515 pretix/control/forms/filter.py:520 #: pretix/control/forms/filter.py:515 pretix/control/forms/filter.py:520
#: pretix/control/forms/filter.py:546 pretix/control/forms/filter.py:551 #: pretix/control/forms/filter.py:546 pretix/control/forms/filter.py:551
@@ -8487,11 +8479,9 @@ msgid "Automatically refund money if possible"
msgstr "" msgstr ""
#: pretix/control/forms/orders.py:619 #: pretix/control/forms/orders.py:619
#, fuzzy
#| msgid "This payment method does not support automatic refunds."
msgid "" msgid ""
"Create manual refund if the payment method does not support automatic refunds" "Create manual refund if the payment method does not support automatic refunds"
msgstr "Tätä maksutapa ei tue automaattisia hyvityksiä." msgstr "Luo manuaalinen hyvitys, jos maksutapa ei tue automaattista hyvitystä"
#: pretix/control/forms/orders.py:623 #: pretix/control/forms/orders.py:623
msgid "" msgid ""
@@ -12876,10 +12866,8 @@ msgid ""
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:104 #: pretix/control/templates/pretixcontrol/order/index.html:104
#, fuzzy
#| msgid "Refund order"
msgid "Refund for overpayment" msgid "Refund for overpayment"
msgstr "Hyvitä tilaus" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:106 #: pretix/control/templates/pretixcontrol/order/index.html:106
#, python-format #, python-format
@@ -13262,16 +13250,12 @@ msgid ""
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:30 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:30
#, fuzzy
#| msgid "Refund order"
msgid "Refund to original payment method" msgid "Refund to original payment method"
msgstr "Hyvitä tilaus" msgstr "Hyvitä alkuperäiseen maksutapaan"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:36 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:36
#, fuzzy
#| msgid "Payment method"
msgid "Payment details" msgid "Payment details"
msgstr "Maksutapa" msgstr "Maksun tiedot"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:37 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:37
msgid "Amount not refunded" msgid "Amount not refunded"
@@ -13279,10 +13263,8 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:38 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:38
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:85 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:85
#, fuzzy
#| msgid "Refund order"
msgid "Refund amount" msgid "Refund amount"
msgstr "Hyvitä tilaus" msgstr "Hyvitettävä summa"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:66 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:66
msgid "Full amount" msgid "Full amount"
@@ -13293,16 +13275,12 @@ msgid "This payment method does not support automatic refunds."
msgstr "Tätä maksutapa ei tue automaattisia hyvityksiä." msgstr "Tätä maksutapa ei tue automaattisia hyvityksiä."
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:78 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:78
#, fuzzy
#| msgid "Refund order"
msgid "Refund to a different payment method" msgid "Refund to a different payment method"
msgstr "Hyvitä tilaus" msgstr "Hyvitä eri maksutavalle"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:84 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:84
#, fuzzy
#| msgid "Question options"
msgid "Recipient / options" msgid "Recipient / options"
msgstr "Kysymysvaihtoehdot" msgstr "Vastaanottaja / vaihtoehdot"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:112 #: pretix/control/templates/pretixcontrol/order/refund_choose.html:112
msgid "Transfer to other order" msgid "Transfer to other order"
@@ -14500,16 +14478,12 @@ msgid "Times"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:339 #: pretix/control/templates/pretixcontrol/subevents/bulk.html:339
#, fuzzy
#| msgid "Start of presale"
msgid "Start of first slot" msgid "Start of first slot"
msgstr "Ennakkomyynnin alku" msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:345 #: pretix/control/templates/pretixcontrol/subevents/bulk.html:345
#, fuzzy
#| msgid "End of presale"
msgid "End of time slots" msgid "End of time slots"
msgstr "Ennakkomyynnin loppu" msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:351 #: pretix/control/templates/pretixcontrol/subevents/bulk.html:351
msgid "Length of slots" msgid "Length of slots"
@@ -14525,11 +14499,8 @@ msgid "Break between slots"
msgstr "" msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:370 #: pretix/control/templates/pretixcontrol/subevents/bulk.html:370
#, fuzzy
#| msgctxt "payment_state"
#| msgid "created"
msgid "Create" msgid "Create"
msgstr "luotu" msgstr "Luo"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:377 #: pretix/control/templates/pretixcontrol/subevents/bulk.html:377
msgid "Add a single time slot" msgid "Add a single time slot"
@@ -16187,10 +16158,8 @@ msgid "The selected exporter was not found."
msgstr "" msgstr ""
#: pretix/control/views/orders.py:2117 #: pretix/control/views/orders.py:2117
#, fuzzy
#| msgid "We are processing your request …"
msgid "There was a problem processing your input:" msgid "There was a problem processing your input:"
msgstr "Pyyntöäsi käsitellään …" msgstr "Ongelma syötteen käsittelyssä:"
#: pretix/control/views/orders.py:2213 #: pretix/control/views/orders.py:2213
msgid "All orders have been canceled." msgid "All orders have been canceled."
@@ -16949,17 +16918,12 @@ msgid "Can only create a bank transfer refund from an existing payment."
msgstr "" msgstr ""
#: pretix/plugins/banktransfer/payment.py:349 #: pretix/plugins/banktransfer/payment.py:349
#, fuzzy
#| msgid "Price (optional)"
msgid "BIC (optional)" msgid "BIC (optional)"
msgstr "Hinta (valinnainen)" msgstr "BIC (valinnainen)"
#: pretix/plugins/banktransfer/payment.py:388 #: pretix/plugins/banktransfer/payment.py:388
#, fuzzy
#| msgid "Your order has been placed successfully. See below for details."
msgid "Your input was invalid, please see below for details." msgid "Your input was invalid, please see below for details."
msgstr "" msgstr "Epäkelpo syöte, katso tiedot alta."
"Tilauksesi on vastaanotettu onnistuneesti. Katso tilauksen tiedot alta."
#: pretix/plugins/banktransfer/refund_export.py:25 #: pretix/plugins/banktransfer/refund_export.py:25
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:5 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html:5
@@ -20213,10 +20177,8 @@ msgid "You chose an invalid cancellation fee."
msgstr "" msgstr ""
#: pretix/presale/views/order.py:869 #: pretix/presale/views/order.py:869
#, fuzzy
#| msgid "Any customer"
msgid "Canceled by customer" msgid "Canceled by customer"
msgstr "Kaikki asiakkaat" msgstr "Asiakkaan peruuttama"
#: pretix/presale/views/order.py:880 #: pretix/presale/views/order.py:880
msgid "The cancellation has been requested." msgid "The cancellation has been requested."

View File

@@ -720,6 +720,8 @@ BOOTSTRAP3 = {
'default': 'bootstrap3.renderers.FieldRenderer', 'default': 'bootstrap3.renderers.FieldRenderer',
'inline': 'bootstrap3.renderers.InlineFieldRenderer', 'inline': 'bootstrap3.renderers.InlineFieldRenderer',
'control': 'pretix.control.forms.renderers.ControlFieldRenderer', 'control': 'pretix.control.forms.renderers.ControlFieldRenderer',
'bulkedit': 'pretix.control.forms.renderers.BulkEditFieldRenderer',
'bulkedit_inline': 'pretix.control.forms.renderers.InlineBulkEditFieldRenderer',
'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer', 'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer',
}, },
} }
@@ -758,3 +760,6 @@ OAUTH2_PROVIDER = {
COUNTRIES_OVERRIDE = { COUNTRIES_OVERRIDE = {
'XK': _('Kosovo'), 'XK': _('Kosovo'),
} }
DATA_UPLOAD_MAX_NUMBER_FIELDS = 25000
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB

View File

@@ -71,6 +71,7 @@ function async_task_check_error(jqXHR, textStatus, errorThrown) {
jqXHR.responseText.indexOf("</body") jqXHR.responseText.indexOf("</body")
)); ));
form_handlers($("body")); form_handlers($("body"));
setup_collapsible_details($("body"));
} else if (c.length > 0) { } else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay // This is some kind of 500/404/403 page, show it in an overlay
$("body").data('ajaxing', false); $("body").data('ajaxing', false);
@@ -146,11 +147,20 @@ function async_task_error(jqXHR, textStatus, errorThrown) {
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) { if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it // This is a failed form validation, let's just use it
waitingDialog.hide(); waitingDialog.hide();
$("body").html(jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"), if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
jqXHR.responseText.indexOf("</body") $("#page-wrapper").html(respdom.find("#page-wrapper").html());
)); form_handlers($("#page-wrapper"));
form_handlers($("body")); setup_collapsible_details($("#page-wrapper"));
} else {
$("body").html(jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
form_handlers($("body"));
setup_collapsible_details($("body"));
}
} else if (c.length > 0) { } else if (c.length > 0) {
waitingDialog.hide(); waitingDialog.hide();
ajaxErrDialog.show(c.first().html()); ajaxErrDialog.show(c.first().html());

View File

@@ -1,11 +1,8 @@
/*global $ */ /*global $ */
$(function () { setup_collapsible_details = function (el) {
"use strict";
var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
el.find("details summary, details summary a[data-toggle=variations]").click(function (e) {
$("details summary, details summary a[data-toggle=variations]").click(function (e) {
if (this.tagName !== "A" && $(e.target).closest("a").length > 0) { if (this.tagName !== "A" && $(e.target).closest("a").length > 0) {
return true; return true;
} }
@@ -44,7 +41,12 @@ $(function () {
$detailsNotSummary = $details.children(':not(summary)'); $detailsNotSummary = $details.children(':not(summary)');
$details.prop('open', typeof $details.attr('open') == 'string'); $details.prop('open', typeof $details.attr('open') == 'string');
if (!$details.prop('open')) { if (!$details.prop('open')) {
$detailsNotSummary.hide(); if ($details.find(".has-error, .alert-danger").length) {
$details.addClass("details-open");
$details.prop('open', true);
} else {
$detailsNotSummary.hide();
}
} else { } else {
$details.addClass("details-open"); $details.addClass("details-open");
} }
@@ -55,4 +57,10 @@ $(function () {
return false; return false;
}); });
}); });
};
$(function () {
"use strict";
setup_collapsible_details($("body"));
}); });

View File

@@ -362,6 +362,9 @@ $(document).ready(function () {
} else { } else {
this.$set(this.rule[this.operator], 1, time); this.$set(this.rule[this.operator], 1, time);
} }
if (event.target.value === "custom") {
this.$set(this.rule[this.operator], 2, 0);
}
}, },
setTimeValue: function (val) { setTimeValue: function (val) {
console.log(val); console.log(val);

View File

@@ -546,6 +546,25 @@ var form_handlers = function (el) {
); );
}); });
el.find(".bulk-edit-field-group").each(function () {
var $checkbox = $(this).find("input[type=checkbox][name=_bulk]");
var $content = $(this).find(".field-content");
var $fields = $content.find("input, select, textarea, button");
var update = function () {
var isChecked = $checkbox.prop("checked");
$content.toggleClass("enabled", isChecked);
$fields.attr("tabIndex", isChecked ? 0 : -1);
}
$content.on("focusin change click", function () {
if ($checkbox.prop("checked")) return;
$checkbox.prop("checked", true);
update();
});
$checkbox.on('change', update)
update();
});
el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent); el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent);
questions_toggle_dependent(); questions_toggle_dependent();
}; };
@@ -563,7 +582,6 @@ $(function () {
} }
); );
$("[data-formset]").on("formAdded", "div", function (event) { $("[data-formset]").on("formAdded", "div", function (event) {
console.log("formAdded")
form_handlers($(event.target)); form_handlers($(event.target));
}); });
$(document).on("click", ".variations .variations-select-all", function (e) { $(document).on("click", ".variations .variations-select-all", function (e) {
@@ -672,29 +690,104 @@ $(function () {
// Tables with bulk selection, e.g. subevent list // Tables with bulk selection, e.g. subevent list
$("input[data-toggle-table]").each(function (ev) { $("input[data-toggle-table]").each(function (ev) {
var $toggle = $(this); var $toggle = $(this);
var $table = $toggle.closest("table");
var update = function () { var $selectAll = $table.find(".table-select-all");
var all_true = true; var $rows = $table.find("tbody tr");
var all_false = true; var $checkboxes = $rows.find("td:first-child input[type=checkbox]");
$toggle.closest("table").find("td:first-child input[type=checkbox]").each(function () { var firstIndex, lastIndex, selectionChecked, onChangeSelectionHappened = false;
if ($(this).prop("checked")) { var updateSelection = function(a, b, checked) {
all_false = false; if (a > b) {
} else { //[a, b] = [b, a];// ES6 not ready yet for pretix
all_true = false; var tmp = a;
} a = b;
}); b = tmp;
if (all_true) { }
$toggle.prop("checked", true).prop("indeterminate", false); for (var i = a; i <= b; i++) {
} else if (all_false) { var checkbox = $checkboxes.get(i);
$toggle.prop("checked", false).prop("indeterminate", false); if (!checkbox.hasAttribute("data-inital")) checkbox.setAttribute("data-inital", checkbox.checked);
} else { if (checked === undefined || checked === null) checkbox.checked = checkbox.getAttribute("data-inital") === "true";
$toggle.prop("checked", false).prop("indeterminate", true); else checkbox.checked = checked;
} }
}; };
var onChangeSelection = function(ev) {
onChangeSelectionHappened = true;
$(this).closest("table").find("td:first-child input[type=checkbox]").change(update); var row = ev.target.closest("tr");
$(this).change(function (ev) { var currentIndex = 0;
$(this).closest("table").find("td:first-child input[type=checkbox]").prop("checked", $(this).prop("checked")); while(row = row.previousSibling) {
if (row.tagName) currentIndex++;
}
var dCurrent = currentIndex - firstIndex;
var dLast = lastIndex - firstIndex;
if (dCurrent*dLast < 0) {
// direction of selection changed => reset all previously selected
updateSelection(lastIndex, firstIndex);
}
else if (Math.abs(dCurrent) < Math.abs(dLast)) {
// selection distance decreased => reset unselected
updateSelection(currentIndex, lastIndex);
}
lastIndex = currentIndex;
updateSelection(firstIndex, currentIndex, selectionChecked);
ev.preventDefault();
};
$table.on("pointerdown", function(ev) {
if (!ev.target.closest("td:first-child")) return;
var row = ev.target.closest("tr");
selectionChecked = !row.querySelector("td:first-child input").checked;
firstIndex = 0;
while(row = row.previousSibling) {
if (row.tagName) firstIndex++;
}
lastIndex = firstIndex;
ev.preventDefault();
$rows.on("pointerenter", onChangeSelection);
$(document).one("pointerup", function(ev) {
if (onChangeSelectionHappened) {
ev.preventDefault();
onChangeSelectionHappened = false;
$checkboxes.removeAttr("data-inital");
update();
}
$rows.off("pointerenter", onChangeSelection);
});
});
var update = function () {
var all_same;
var checkboxes = $checkboxes.toArray();
var i = checkboxes.length;
while (i--) {
if (all_same === undefined) {
all_same = checkboxes[i].checked;
continue;
}
if (all_same != checkboxes[i].checked) {
$toggle.prop("checked", false).prop("indeterminate", true).trigger("change");
return;
}
}
$toggle.prop("checked", all_same).prop("indeterminate", false).trigger("change");
};
var debounceUpdate;
$checkboxes.change(function() {
//$(this).closest("tr").toggleClass("warning", this.checked);
// when changing the $toggles checked-property, lots of change events
// get triggered => debounce
if (debounceUpdate) window.clearTimeout(debounceUpdate);
debounceUpdate = window.setTimeout(update, 10);
});
$toggle.change(function (ev) {
if (!this.indeterminate) $checkboxes.prop("checked", this.checked);//.trigger("change");
$selectAll.toggleClass("hidden", !this.checked).prop("hidden", !this.checked);
if (!this.checked) $selectAll.find("input").prop("checked", false);
}); });
}); });

View File

@@ -613,3 +613,41 @@ table td > .checkbox input[type="checkbox"] {
border-bottom: 1px solid $input-border; border-bottom: 1px solid $input-border;
padding-bottom: 5px; padding-bottom: 5px;
} }
.batch-select-label {
display: block;
width: 100%;
height: 1.5em;
cursor: pointer;
}
.bulk-edit-field-group {
.field-toggle {
font-weight: normal;
display: inline-block;
background: $gray-lighter;
padding: 2px 8px 4px;
border-top-left-radius: $border-radius-base;
border-top-right-radius: $border-radius-base;
margin-bottom: 0;
input {
position: relative;
top: 2px;
}
}
.field-content {
border: 2px solid $gray-lighter;
padding: 15px;
opacity: 0.5;
.datepickerfield::placeholder, .timepickerfield::placeholder {
opacity: 0;
}
&.enabled {
opacity: 1;
.datepickerfield::placeholder, .timepickerfield::placeholder {
opacity: 1;
}
}
}
}

View File

@@ -72,6 +72,7 @@ TEST_SUBEVENT_RES = {
'date_to': None, 'date_to': None,
'date_admission': None, 'date_admission': None,
'name': {'en': 'Foobar'}, 'name': {'en': 'Foobar'},
'frontpage_text': None,
'date_from': '2017-12-27T10:00:00Z', 'date_from': '2017-12-27T10:00:00Z',
'presale_end': None, 'presale_end': None,
'seating_plan': None, 'seating_plan': None,

View File

@@ -34,16 +34,32 @@ def extract_form_fields(soup):
if field['type'] in ('checkbox', 'radio'): if field['type'] in ('checkbox', 'radio'):
if field.has_attr('checked') and field.has_attr('name'): if field.has_attr('checked') and field.has_attr('name'):
data[field['name']] = field.get('value', 'on') if field['name'] in data:
if not isinstance(data[field['name']], list):
data[field['name']] = [data[field['name']]]
data[field['name']].append(field.get('value', 'on'))
else:
data[field['name']] = field.get('value', 'on')
continue continue
elif field.has_attr('name'): elif field.has_attr('name'):
# single element name/value fields # single element name/value fields
data[field['name']] = field.get('value', '') value = field.get('value', '')
if field['name'] in data:
if not isinstance(data[field['name']], list):
data[field['name']] = [data[field['name']]]
data[field['name']].append(value)
else:
data[field['name']] = value
continue continue
# textareas # textareas
for textarea in soup.findAll('textarea'): for textarea in soup.findAll('textarea'):
data[textarea['name']] = textarea.text or '' if textarea['name'] in data:
if not isinstance(data[textarea['name']], list):
data[textarea['name']] = [data[textarea['name']]]
data[textarea['name']].append(textarea.text or '')
else:
data[textarea['name']] = textarea.text or ''
# select fields # select fields
for select in soup.find_all('select'): for select in soup.find_all('select'):
@@ -66,6 +82,11 @@ def extract_form_fields(soup):
else: else:
value = [option['value'] for option in selected_options] value = [option['value'] for option in selected_options]
data[select['name']] = value if select['name'] in data:
if not isinstance(data[select['name']], list):
data[select['name']] = [data[select['name']]]
data[select['name']].append(value)
else:
data[select['name']] = value
return data return data

View File

@@ -9,10 +9,7 @@ from i18nfield.strings import LazyI18nString
from pytz import timezone from pytz import timezone
from tests.base import SoupTest, extract_form_fields from tests.base import SoupTest, extract_form_fields
from pretix.base.models import ( from pretix.base.models import Event, Order, Organizer, Team, User
Event, Order, OrderPosition, Organizer, SubEvent, Team, User,
)
from pretix.base.models.items import SubEventItem
from pretix.testutils.mock import mocker_context from pretix.testutils.mock import mocker_context
@@ -996,646 +993,6 @@ class EventsTest(SoupTest):
assert doc.select(".has-error") assert doc.select(".has-error")
class SubEventsTest(SoupTest):
@scopes_disabled()
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
self.event1 = Event.objects.create(
organizer=self.orga1, name='30C3', slug='30c3',
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
plugins='pretix.plugins.banktransfer,tests.testdummy',
has_subevents=True
)
t = Team.objects.create(organizer=self.orga1, can_create_events=True, can_change_event_settings=True,
can_change_items=True)
t.members.add(self.user)
t.limit_events.add(self.event1)
self.ticket = self.event1.items.create(name='Early-bird ticket',
category=None, default_price=23,
admission=True)
self.client.login(email='dummy@dummy.dummy', password='dummy')
self.subevent1 = self.event1.subevents.create(name='SE1', date_from=now())
self.subevent2 = self.event1.subevents.create(name='SE2', date_from=now())
def test_list(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/')
tabletext = doc.select("#page-wrapper .table")[0].text
self.assertIn("SE1", tabletext)
def test_create(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/add')
assert doc.select("input[name=quotas-TOTAL_FORMS]")
doc = self.post_doc('/control/event/ccc/30c3/subevents/add', {
'name_0': 'SE2',
'active': 'on',
'date_from_0': '2017-07-01',
'date_from_1': '10:00:00',
'date_to_0': '2017-07-01',
'date_to_1': '12:00:00',
'location_0': 'Hamburg',
'presale_start_0': '2017-06-20',
'presale_start_1': '10:00:00',
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-name': 'Default',
'checkinlist_set-0-all_products': 'on',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'item-%d-price' % self.ticket.pk: '12'
})
assert doc.select(".alert-success")
with scopes_disabled():
se = self.event1.subevents.first()
assert str(se.name) == "SE2"
assert se.active
assert se.date_from.isoformat() == "2017-07-01T10:00:00+00:00"
assert se.date_to.isoformat() == "2017-07-01T12:00:00+00:00"
assert str(se.location) == "Hamburg"
assert se.presale_start.isoformat() == "2017-06-20T10:00:00+00:00"
assert not se.presale_end
assert se.quotas.count() == 1
q = se.quotas.last()
assert q.name == "Q1"
assert q.size == 50
assert list(q.items.all()) == [self.ticket]
sei = SubEventItem.objects.get(subevent=se, item=self.ticket)
assert sei.price == 12
assert se.checkinlist_set.count() == 1
def test_modify(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk)
assert doc.select("input[name=quotas-TOTAL_FORMS]")
doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk, {
'name_0': 'SE2',
'active': 'on',
'date_from_0': '2017-07-01',
'date_from_1': '10:00:00',
'date_to_0': '2017-07-01',
'date_to_1': '12:00:00',
'location_0': 'Hamburg',
'presale_start_0': '2017-06-20',
'presale_start_1': '10:00:00',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-name': 'Default',
'checkinlist_set-0-all_products': 'on',
'item-%d-price' % self.ticket.pk: '12'
})
assert doc.select(".alert-success")
self.subevent1.refresh_from_db()
se = self.subevent1
assert str(se.name) == "SE2"
assert se.active
assert se.date_from.isoformat() == "2017-07-01T10:00:00+00:00"
assert se.date_to.isoformat() == "2017-07-01T12:00:00+00:00"
assert str(se.location) == "Hamburg"
assert se.presale_start.isoformat() == "2017-06-20T10:00:00+00:00"
assert not se.presale_end
with scopes_disabled():
assert se.quotas.count() == 1
q = se.quotas.last()
assert q.name == "Q1"
assert q.size == 50
assert list(q.items.all()) == [self.ticket]
sei = SubEventItem.objects.get(subevent=se, item=self.ticket)
assert sei.price == 12
assert se.checkinlist_set.count() == 1
def test_delete(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk)
assert doc.select("button")
doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {})
assert doc.select(".alert-success")
# deleting the second event
doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent2.pk, {})
assert doc.select(".alert-success")
with scopes_disabled():
assert not SubEvent.objects.filter(pk=self.subevent2.pk).exists()
assert not SubEvent.objects.filter(pk=self.subevent1.pk).exists()
def test_delete_with_orders(self):
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en'
)
OrderPosition.objects.create(
order=o,
item=self.ticket,
subevent=self.subevent1,
price=Decimal("14"),
)
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, follow=True)
assert doc.select(".alert-danger")
doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}, follow=True)
assert doc.select(".alert-danger")
with scopes_disabled():
assert self.event1.subevents.filter(pk=self.subevent1.pk).exists()
def test_create_bulk(self):
with scopes_disabled():
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add')
assert doc.select("input[name=rruleformset-TOTAL_FORMS]")
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '1',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-interval': '1',
'rruleformset-0-freq': 'yearly',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-yearly_bysetpos': '1',
'rruleformset-0-yearly_byweekday': 'MO',
'rruleformset-0-yearly_bymonth': '1',
'rruleformset-0-monthly_same': 'on',
'rruleformset-0-monthly_bysetpos': '1',
'rruleformset-0-monthly_byweekday': 'MO',
'rruleformset-0-end': 'count',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'timeformset-TOTAL_FORMS': '1',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '13:29:31',
'timeformset-0-time_to': '15:29:31',
'name_0': 'Foo',
'active': 'on',
'location_0': 'Loc',
'time_admission': '',
'frontpage_text_0': '',
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-id': '',
'quotas-0-name': 'Bar',
'quotas-0-size': '12',
'quotas-0-itemvars': str(self.ticket.pk),
'item-%d-price' % self.ticket.pk: '16',
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-id': '',
'checkinlist_set-0-name': 'Foo',
'checkinlist_set-0-limit_products': str(self.ticket.pk),
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 10
assert str(ses[0].name) == "Foo"
assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00"
assert ses[0].date_to.isoformat() == "2018-04-03T13:29:31+00:00"
assert not ses[0].presale_start
assert ses[0].presale_end.isoformat() == "2018-04-02T11:29:31+00:00"
with scopes_disabled():
assert ses[0].quotas.count() == 1
assert list(ses[0].quotas.first().items.all()) == [self.ticket]
assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16
assert ses[0].checkinlist_set.count() == 1
assert str(ses[1].name) == "Foo"
assert ses[1].date_from.isoformat() == "2019-04-03T11:29:31+00:00"
assert ses[1].date_to.isoformat() == "2019-04-03T13:29:31+00:00"
assert not ses[1].presale_start
assert ses[1].presale_end.isoformat() == "2019-04-02T11:29:31+00:00"
with scopes_disabled():
assert ses[1].quotas.count() == 1
assert list(ses[1].quotas.first().items.all()) == [self.ticket]
assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16
assert ses[1].checkinlist_set.count() == 1
assert ses[-1].date_from.isoformat() == "2027-04-03T11:29:31+00:00"
def test_create_bulk_daily_interval(self):
with scopes_disabled():
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add')
assert doc.select("input[name=rruleformset-TOTAL_FORMS]")
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '1',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-interval': '2',
'rruleformset-0-freq': 'daily',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-yearly_bysetpos': '1',
'rruleformset-0-yearly_byweekday': 'MO',
'rruleformset-0-yearly_bymonth': '1',
'rruleformset-0-monthly_same': 'on',
'rruleformset-0-monthly_bysetpos': '1',
'rruleformset-0-monthly_byweekday': 'MO',
'rruleformset-0-end': 'until',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'timeformset-TOTAL_FORMS': '1',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '13:29:31',
'timeformset-0-time_to': '15:29:31',
'name_0': 'Foo',
'active': 'on',
'frontpage_text_0': '',
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '1',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '0',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 183
assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00"
assert ses[110].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :)
assert ses[-1].date_from.isoformat() == "2019-04-02T11:29:31+00:00"
def test_create_bulk_daily_interval_multiple_times(self):
with scopes_disabled():
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add')
assert doc.select("input[name=rruleformset-TOTAL_FORMS]")
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '1',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-interval': '2',
'rruleformset-0-freq': 'daily',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-yearly_bysetpos': '1',
'rruleformset-0-yearly_byweekday': 'MO',
'rruleformset-0-yearly_bymonth': '1',
'rruleformset-0-monthly_same': 'on',
'rruleformset-0-monthly_bysetpos': '1',
'rruleformset-0-monthly_byweekday': 'MO',
'rruleformset-0-end': 'until',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'timeformset-TOTAL_FORMS': '2',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '13:29:31',
'timeformset-0-time_to': '15:29:31',
'timeformset-1-time_from': '15:29:31',
'timeformset-1-time_to': '17:29:31',
'name_0': 'Foo',
'active': 'on',
'frontpage_text_0': '',
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '0',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 183 * 2
assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00"
assert ses[1].date_from.isoformat() == "2018-04-03T13:29:31+00:00"
assert ses[220].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :)
assert ses[-1].date_from.isoformat() == "2019-04-02T13:29:31+00:00"
def test_create_bulk_exclude(self):
with scopes_disabled():
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add')
assert doc.select("input[name=rruleformset-TOTAL_FORMS]")
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '2',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-interval': '1',
'rruleformset-0-freq': 'daily',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-yearly_bysetpos': '1',
'rruleformset-0-yearly_byweekday': 'MO',
'rruleformset-0-yearly_bymonth': '1',
'rruleformset-0-monthly_same': 'on',
'rruleformset-0-monthly_bysetpos': '1',
'rruleformset-0-monthly_byweekday': 'MO',
'rruleformset-0-end': 'until',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'rruleformset-1-interval': '1',
'rruleformset-1-freq': 'weekly',
'rruleformset-1-dtstart': '2018-04-03',
'rruleformset-1-yearly_same': 'on',
'rruleformset-1-yearly_bysetpos': '1',
'rruleformset-1-yearly_byweekday': 'MO',
'rruleformset-1-yearly_bymonth': '1',
'rruleformset-1-monthly_same': 'on',
'rruleformset-1-monthly_bysetpos': '1',
'rruleformset-1-monthly_byweekday': 'MO',
'rruleformset-1-weekly_byweekday': 'MO',
'rruleformset-1-end': 'until',
'rruleformset-1-count': '10',
'rruleformset-1-until': '2019-04-03',
'rruleformset-1-exclude': 'on',
'timeformset-TOTAL_FORMS': '1',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '13:29:31',
'timeformset-0-time_to': '15:29:31',
'name_0': 'Foo',
'active': 'on',
'frontpage_text_0': '',
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '0',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 314
assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00"
assert ses[5].date_from.isoformat() == "2018-04-08T11:29:31+00:00"
assert ses[6].date_from.isoformat() == "2018-04-10T11:29:31+00:00"
def test_create_bulk_monthly_interval(self):
with scopes_disabled():
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '1',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-interval': '1',
'rruleformset-0-freq': 'monthly',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-yearly_bysetpos': '1',
'rruleformset-0-yearly_byweekday': 'MO',
'rruleformset-0-yearly_bymonth': '1',
'rruleformset-0-monthly_same': 'off',
'rruleformset-0-monthly_bysetpos': '-1',
'rruleformset-0-monthly_byweekday': 'MO,TU,WE,TH,FR',
'rruleformset-0-weekly_byweekday': 'TH',
'rruleformset-0-end': 'until',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'timeformset-TOTAL_FORMS': '1',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '13:29:31',
'timeformset-0-time_to': '15:29:31',
'name_0': 'Foo',
'active': 'on',
'frontpage_text_0': '',
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '0',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 12
assert ses[0].date_from.isoformat() == "2018-04-30T11:29:31+00:00"
assert ses[1].date_from.isoformat() == "2018-05-31T11:29:31+00:00"
assert ses[-1].date_from.isoformat() == "2019-03-29T12:29:31+00:00"
def test_create_bulk_weekly_interval(self):
with scopes_disabled():
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '1',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-interval': '1',
'rruleformset-0-freq': 'weekly',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-yearly_bysetpos': '1',
'rruleformset-0-yearly_byweekday': 'MO',
'rruleformset-0-yearly_bymonth': '1',
'rruleformset-0-monthly_same': 'on',
'rruleformset-0-monthly_bysetpos': '-1',
'rruleformset-0-monthly_byweekday': 'MO,TU,WE,TH,FR',
'rruleformset-0-weekly_byweekday': 'TH',
'rruleformset-0-end': 'until',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'timeformset-TOTAL_FORMS': '1',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '13:29:31',
'timeformset-0-time_to': '15:29:31',
'name_0': 'Foo',
'active': 'on',
'frontpage_text_0': '',
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '0',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 52
assert ses[0].date_from.isoformat() == "2018-04-05T11:29:31+00:00"
assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00"
assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00"
def test_delete_bulk(self):
self.subevent2.active = True
self.subevent2.save()
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en'
)
OrderPosition.objects.create(
order=o,
item=self.ticket,
subevent=self.subevent1,
price=Decimal("14"),
)
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', {
'subevent': [str(self.subevent1.pk), str(self.subevent2.pk)],
'action': 'delete_confirm'
}, follow=True)
assert doc.select(".alert-success")
with scopes_disabled():
assert not self.event1.subevents.filter(pk=self.subevent2.pk).exists()
assert self.event1.subevents.get(pk=self.subevent1.pk).active is False
def test_disable_bulk(self):
self.subevent2.active = True
self.subevent2.save()
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', {
'subevent': str(self.subevent2.pk),
'action': 'disable'
}, follow=True)
assert doc.select(".alert-success")
with scopes_disabled():
assert self.event1.subevents.get(pk=self.subevent2.pk).active is False
def test_enable_bulk(self):
self.subevent2.active = False
self.subevent2.save()
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', {
'subevent': str(self.subevent2.pk),
'action': 'enable'
}, follow=True)
assert doc.select(".alert-success")
with scopes_disabled():
assert self.event1.subevents.get(pk=self.subevent2.pk).active is True
class EventDeletionTest(SoupTest): class EventDeletionTest(SoupTest):
@scopes_disabled() @scopes_disabled()
def setUp(self): def setUp(self):

File diff suppressed because it is too large Load Diff

View File

@@ -93,9 +93,9 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
doc = self.client.get('/control/event/%s/%s/vouchers/?download=yes' % (self.orga.slug, self.event.slug)) doc = self.client.get('/control/event/%s/%s/vouchers/?download=yes' % (self.orga.slug, self.event.slug))
assert doc.content.decode().strip() == '"Voucher code","Valid until","Product","Reserve quota",' \ assert doc.content.decode().strip() == '"Voucher code","Valid until","Product","Reserve quota",' \
'"Bypass quota","Price effect","Value","Tag","Redeemed",' \ '"Bypass quota","Price effect","Value","Tag","Redeemed",' \
'"Maximum usages","Seat"' \ '"Maximum usages","Seat","Comment"' \
'\r\n"ABCDEFG","","Early-bird ticket","No","No","No effect","","","0",' \ '\r\n"ABCDEFG","","Early-bird ticket","No","No","No effect","","","0",' \
'"1",""' '"1","",""'
def test_filter_status_valid(self): def test_filter_status_valid(self):
with scopes_disabled(): with scopes_disabled():