Compare commits

..
297 changed files with 146463 additions and 161907 deletions
-2
View File
@@ -10,9 +10,7 @@ updates:
schedule:
interval: "daily"
versioning-strategy: increase
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/src/pretix/static/npm_dir"
schedule:
interval: "monthly"
open-pull-requests-limit: 5
+6 -10
View File
@@ -42,6 +42,7 @@ Example::
currency=EUR
datadir=/data
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
cookie_domain=.pretix.de
``instance_name``
The name of this installation. Default: ``pretix.de``
@@ -52,18 +53,10 @@ Example::
``currency``
The default currency as a three-letter code. Defaults to ``EUR``.
``cachedir``
The local path to a directory where temporary files will be stored.
Defaults to the ``cache`` directory below the ``datadir``.
``datadir``
The local path to a data directory that will be used for storing user uploads and similar
data. Defaults to the value of the environment variable ``DATA_DIR`` or ``data``.
``logdir``
The local path to a directory where log files will be stored.
Defaults to the ``logs`` directory below the ``datadir``.
``plugins_default``
A comma-separated list of plugins that are enabled by default for all new events.
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
@@ -78,6 +71,9 @@ Example::
``auth_backends``
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
``cookie_domain``
The cookie domain to be set. Defaults to ``None``.
``registration``
Enables or disables the registration of new admin users. Defaults to ``off``.
@@ -353,7 +349,7 @@ to speed up various operations::
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
or ``unix://[:password]@/path/to/socket.sock?db=0``
``sessions``
``session``
When this is set to ``True``, redis will be used as the session storage.
``sentinels``
@@ -529,4 +525,4 @@ pretix can optionally make use of a GeoIP database for some features. It needs a
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
+1 -3
View File
@@ -94,9 +94,7 @@ If you want the user to return to your application after the payment is complete
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
either enter ``https://example.org/order/`` or ``https://example.org/``.
Please note that in the latter case the trailing slash is required, ``https://example.org`` is not allowed to prevent.
Only base URLs with a secure (``https://``) or local (``http://localhost``) origin are permitted.
either enter ``https://example.org`` or ``https://example.org/order/``.
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
-11
View File
@@ -19,7 +19,6 @@ external_identifier string External ID of
the API, but is read-only for customers created through a
SSO integration.
email string Customer email address
phone string Customer phone number
name string Name of this customer (or ``null``)
name_parts object of strings Decomposition of name (i.e. given name, family name)
is_active boolean Whether this account is active
@@ -40,10 +39,6 @@ password string Can only be set
Passwords can now be set through the API during customer creation.
.. versionchanged:: 2024.3
The attribute ``phone`` has been added.
Endpoints
---------
@@ -76,7 +71,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "customer@example.org",
"phone": "+493012345678",
"name": "John Doe",
"name_parts": {
"_scheme": "full",
@@ -124,7 +118,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "customer@example.org",
"phone": "+493012345678",
"name": "John Doe",
"name_parts": {
"_scheme": "full",
@@ -162,7 +155,6 @@ Endpoints
{
"email": "test@example.org",
"phone": "+493012345678",
"password": "verysecret",
"send_email": true
}
@@ -179,7 +171,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "test@example.org",
"phone": "+493012345678",
...
}
@@ -224,7 +215,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": "test@example.org",
"phone": "+493012345678",
}
@@ -259,7 +249,6 @@ Endpoints
"identifier": "8WSAJCJ",
"external_identifier": null,
"email": null,
"phone": null,
}
-2
View File
@@ -36,8 +36,6 @@ geo_lon float Longitude of th
has_subevents boolean ``true`` if the event series feature is active for this
event. Cannot change after event is created.
meta_data object Values set for organizer-specific meta data parameters.
The allowed keys need to be set up as meta properties
in the organizer configuration.
plugins list A list of package names of the enabled plugins for this
event.
seating_plan integer If reserved seating is in use, the ID of a seating
-20
View File
@@ -45,16 +45,8 @@ sales_channels list of strings Sales channels
available.
available_from datetime The first date time at which this variation can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the available_from setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
available_until datetime The last date time at which this variation can be bought
(or ``null``).
available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the available_until setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
@@ -113,9 +105,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": {
"en": "Test2"
@@ -141,9 +131,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": {},
"position": 1,
@@ -204,9 +192,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -246,9 +232,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -279,9 +263,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 0,
@@ -343,9 +325,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"position": 1,
-46
View File
@@ -50,16 +50,8 @@ sales_channels list of strings Sales channel
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
available_from datetime The first date time at which this item can be bought
(or ``null``).
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``available_from`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
available_until datetime The last date time at which this item can be bought
(or ``null``).
available_until_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``available_until`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
set, this item won't be shown publicly as long as this
quota is available.
@@ -164,16 +156,8 @@ variations list of objects A list with o
available.
├ available_from datetime The first date time at which this variation can be bought
(or ``null``).
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the ``available_from`` setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
├ available_until datetime The last date time at which this variation can be bought
(or ``null``).
├ available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
if unavailable due to the ``available_until`` setting.
If ``info``, the variation is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
redemption process, but not in the normal shop
frontend.
@@ -295,9 +279,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -342,9 +324,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -364,9 +344,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -439,9 +417,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -487,9 +463,7 @@ Endpoints
"description": null,
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"meta_data": {},
"position": 0
@@ -508,9 +482,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -564,9 +536,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -610,9 +580,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -632,9 +600,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -676,9 +642,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -723,9 +687,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -745,9 +707,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -820,9 +780,7 @@ Endpoints
"position": 0,
"picture": null,
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"require_voucher": false,
@@ -867,9 +825,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
@@ -889,9 +845,7 @@ Endpoints
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
"available_from_mode": "hide",
"available_until": null,
"available_until_mode": "hide",
"hide_without_voucher": false,
"description": null,
"meta_data": {},
-9
View File
@@ -179,11 +179,6 @@ country string Attendee countr
state string Attendee state (ISO 3166-2 code). Only supported in
AU, BR, CA, CN, MY, MX, and US, otherwise ``null``.
voucher integer Internal ID of the voucher used for this position (or ``null``)
voucher_budget_use money (string) Amount of money discounted by the voucher, corresponding
to how much of the ``budget`` of the voucher is consumed.
**Important:** Do not rely on this amount to be a useful
value if the position's price, product or voucher
are changed *after* the order was created. Can be ``null``.
tax_rate decimal (string) VAT rate applied for this position
tax_value money (string) VAT included in this position
tax_rule integer The ID of the used tax rule (or ``null``)
@@ -372,7 +367,6 @@ List of all orders
"country": "DE",
"state": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_value": "0.00",
"tax_rule": null,
@@ -595,7 +589,6 @@ Fetching individual orders
"country": "DE",
"state": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
@@ -1548,7 +1541,6 @@ List of all order positions
},
"attendee_email": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
@@ -1662,7 +1654,6 @@ Fetching individual positions
},
"attendee_email": null,
"voucher": null,
"voucher_budget_use": null,
"tax_rate": "0.00",
"tax_rule": null,
"tax_value": "0.00",
+1 -2
View File
@@ -13,8 +13,7 @@ Core
.. automodule:: pretix.base.signals
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders
register_ticket_secret_generators, gift_card_transaction_display
Order events
""""""""""""
-2
View File
@@ -84,8 +84,6 @@ convenient to you:
.. automethod:: _register_fonts
.. automethod:: _register_event_fonts
.. automethod:: _on_first_page
.. automethod:: _on_other_page
+15 -24
View File
@@ -1,11 +1,10 @@
.. highlight:: python
:linenothreshold: 5
Writing a template placeholder plugin
=====================================
Writing an e-mail placeholder plugin
====================================
A template placeholder is a dynamic value that pretix users can use in their email templates and in other
configurable texts.
An email placeholder is a dynamic value that pretix users can use in their email templates.
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
@@ -13,31 +12,31 @@ Placeholder registration
------------------------
The placeholder API does not make a lot of usage from signals, however, it
does use a signal to get a list of all available placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.services.placeholders.BaseTextPlaceholder``:
does use a signal to get a list of all available email placeholders. Your plugin
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:
.. code-block:: python
from django.dispatch import receiver
from pretix.base.signals import register_text_placeholders
from pretix.base.signals import register_mail_placeholders
@receiver(register_text_placeholders, dispatch_uid="placeholder_custom")
def register_placeholder_renderers(sender, **kwargs):
from .placeholders import MyPlaceholderClass
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
def register_mail_renderers(sender, **kwargs):
from .email import MyPlaceholderClass
return MyPlaceholder()
Context mechanism
-----------------
Templates are used in different "contexts" within pretix. For example, many emails are rendered from
templates in the context of an order, but some are not, such as the notification of a waiting list voucher.
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
the context of an order, but some are not, such as the notification of a waiting list voucher.
Not all placeholders make sense everywhere, and placeholders usually depend on some parameters
Not all placeholders make sense in every email, and placeholders usually depend some parameters
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
what values they depend on and they will only be available in a context where all those dependencies are
what values they depend on and they will only be available in an email if all those dependencies are
met. Currently, placeholders can depend on the following context parameters:
* ``event``
@@ -52,7 +51,7 @@ There are a few more that are only to be used internally but not by plugins.
The placeholder class
---------------------
.. class:: pretix.base.services.placeholders.BaseTextPlaceholder
.. class:: pretix.base.email.BaseMailTextPlaceholder
.. autoattribute:: identifier
@@ -78,15 +77,7 @@ functions:
.. code-block:: python
placeholder = SimpleFunctionalTextPlaceholder(
placeholder = SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, sample='F8VVL'
)
Signals
-------
.. automodule:: pretix.base.signals
:members: register_text_placeholders
.. automodule:: pretix.base.signals
:members: register_mail_placeholders
+7 -33
View File
@@ -34,16 +34,12 @@ internal_id string Can be used for
contact_name string Contact person (or ``null``)
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
contact_email string Contact person email address (or ``null``)
contact_cc_email string Copy email addresses, can be multiple separated by comma (or ``null``)
booth string Booth number (or ``null``). Maximum 100 characters.
locale string Locale for communication with the exhibitor.
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
lead_scanning_access_code string Access code for the exhibitor to use the lead scanning app but not access data (read-only).
allow_lead_scanning boolean Enables lead scanning app
allow_lead_access boolean Enables access to data gathered by the lead scanning app
allow_voucher_access boolean Enables access to data gathered by exhibitor vouchers
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
per scanning device, instead of only per exhibitor.
comment string Internal comment, not shown to exhibitor
===================================== ========================== =======================================================
@@ -66,7 +62,6 @@ data list of objects Attendee data s
except in a few cases where it contains an additional list of objects
with ``value`` and ``label`` keys (e.g. splitting of names).
device_name string User-defined name for the device used for scanning (or ``null``).
device_uuid string UUID of device used for scanning (or ``null``).
===================================== ========================== =======================================================
Endpoints
@@ -110,12 +105,9 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -164,12 +156,9 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -368,7 +357,6 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"allow_lead_scanning": true,
@@ -398,12 +386,9 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -459,12 +444,9 @@ Endpoints
"title": "Dr"
},
"contact_email": "johnson@as.example.org",
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
"booth": "A2",
"locale": "de",
"access_code": "VKHZ2FU84",
"lead_scanning_access_code": "WVK2B8PZ",
"lead_scanning_scope_by_device": false,
"access_code": "VKHZ2FU8",
"allow_lead_scanning": true,
"allow_lead_access": true,
"allow_voucher_access": true,
@@ -579,7 +561,6 @@ name string Exhibitor name
booth string Booth number (or ``null``)
event object Object describing the event
├ name multi-lingual string Event name
├ end_date datetime End date of the event. After this time, the app could show a warning that the event is over.
├ imprint_url string URL to legal notice page. If not ``null``, a button in the app should link to this page.
├ privacy_url string URL to privacy notice page. If not ``null``, a button in the app should link to this page.
├ help_url string URL to help page. If not ``null``, a button in the app should link to this page.
@@ -615,7 +596,6 @@ scan_types list of objects Only used for a
"booth": "A2",
"event": {
"name": {"en": "Sample conference", "de": "Beispielkonferenz"},
"end_date": "2017-12-28T10:00:00+00:00",
"slug": "bigevents",
"imprint_url": null,
"privacy_url": null,
@@ -654,7 +634,6 @@ On the request, you should set the following properties:
* ``tags`` with the list of selected tags
* ``rating`` with the rating assigned by the exhibitor
* ``device_name`` with a user-specified name of the device used for scanning (max. 190 characters), or ``null``
* ``device_uuid`` with a auto-generated UUID of the device used for scanning, or ``null``
If you submit ``tags`` and ``rating`` to be ``null`` and ``notes`` to be ``""``, the server
responds with the previously saved information and will not delete that information. If you
@@ -689,8 +668,7 @@ The request for this looks like this:
"scan_type": "lead",
"tags": ["foo"],
"rating": 4,
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
"device_name": "DEV1"
}
**Example response:**
@@ -723,9 +701,7 @@ The request for this looks like this:
},
"rating": 4,
"tags": ["foo"],
"notes": "Great customer, wants our newsletter",
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
"notes": "Great customer, wants our newsletter"
}
:statuscode 200: No error, leads was not scanned for the first time
@@ -780,9 +756,7 @@ You can also fetch existing leads (if you are authorized to do so):
},
"rating": 4,
"tags": ["foo"],
"notes": "Great customer, wants our newsletter",
"device_name": "DEV1",
"device_uuid": "d8c2ec53-d602-4a08-882d-db4cf54344a2"
"notes": "Great customer, wants our newsletter"
}
]
}
+1 -1
View File
@@ -1,4 +1,4 @@
sphinx==7.2.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
+1 -1
View File
@@ -1,5 +1,5 @@
-e ../
sphinx==7.2.*
sphinx==7.0.*
jinja2==3.1.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
@@ -19,3 +19,4 @@ Then, head to the **Bundled products** tab of the "conference ticket" and add th
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
You can find more use cases in these specialized guides:
-21
View File
@@ -429,25 +429,4 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
});
</script>
Offering wallet payments (Apple Pay, Google Pay) within the widget
------------------------------------------------------------------
Some payment providers (such as Stripe) also offer Apple or Google Pay. But in order to use them, the domain of the
payment needs to be approved first. As of right now, pretix will take care of the domain verification process for you
automatically, when using Stripe. However, pretix can only validate the domain that is being used for your default,
"stand-alone" shop (such as https://pretix.eu/demo/democon/ ).
When embedding the widget on your website, the domain of the embedding page will also need to be validated in order to
be able to use it for wallet payments.
The details might vary from payment provider to payment provider, but generally speaking, it will either involve just
telling your payment provider the domain name and (for Apple Pay) placing an
``apple-developer-merchantid-domain-association``-file into the ``.well-known``-directory of your domain.
Further reading:
* `Stripe Payment Method Domain registration`_
.. _Let's Encrypt: https://letsencrypt.org/
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration
+35 -32
View File
@@ -31,41 +31,41 @@ dependencies = [
"BeautifulSoup4==4.12.*",
"bleach==5.0.*",
"celery==5.3.*",
"chardet==5.2.*",
"chardet==5.1.*",
"cryptography>=3.4.2",
"css-inline==0.13.*",
"css-inline==0.8.*",
"defusedcsv>=1.1.0",
"dj-static",
"Django==4.2.*",
"django-bootstrap3==23.6.*",
"django-compressor==4.4",
"django-bootstrap3==23.1.*",
"django-compressor==4.3.*",
"django-countries==7.5.*",
"django-filter==24.1",
"django-filter==23.2",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1",
"django-formtools==2.4.1",
"django-hierarkey==1.1.*",
"django-hijack==3.4.*",
"django-hijack==3.3.*",
"django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9",
"django-localflavor==4.0",
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.3.*",
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-oauth-toolkit==2.2.*",
"django-otp==1.2.*",
"django-phonenumber-field==7.1.*",
"django-redis==5.2.*",
"django-scopes==2.0.*",
"django-statici18n==2.4.*",
"django-statici18n==2.3.*",
"djangorestframework==3.14.*",
"dnspython==2.6.*",
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.3.*",
"libsass==0.23.*",
"libsass==0.22.*",
"lxml",
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*",
"oauthlib==3.2.*",
@@ -73,26 +73,27 @@ dependencies = [
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.8.*",
"PyJWT==2.7.*",
"phonenumberslite==8.13.*",
"Pillow==10.2.*",
"Pillow==9.5.*",
"pretix-plugin-build",
"protobuf==5.26.*",
"protobuf==4.23.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.21",
"pycryptodome==3.20.*",
"pycryptodome==3.18.*",
"pypdf==3.9.*",
"python-bidi==0.4.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"python-dateutil==2.8.*",
"python-u2flib-server==4.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==7.4.*",
"redis==5.0.*",
"reportlab==4.1.*",
"redis==4.6.*",
"reportlab==4.0.*",
"requests==2.31.*",
"sentry-sdk==1.42.*",
"sentry-sdk==1.15.*",
"sepaxml==2.6.*",
"slimit",
"static3==0.7.*",
@@ -102,31 +103,33 @@ dependencies = [
"tqdm==4.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.0.*",
"webauthn==0.4.*",
"zeep==4.2.*"
]
[project.optional-dependencies]
memcached = ["pylibmc"]
dev = [
"aiohttp==3.9.*",
"aiohttp==3.8.*",
"coverage",
"coveralls",
"fakeredis==2.21.*",
"flake8==7.0.*",
"fakeredis==2.18.*",
"flake8==6.0.*",
"freezegun",
"isort==5.13.*",
"isort==5.12.*",
"pep8-naming==0.13.*",
"potypo",
"pycodestyle==2.10.*",
"pyflakes==3.0.*",
"pytest-asyncio",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
"pytest-mock==3.12.*",
"pytest-rerunfailures==14.*",
"pytest-mock==3.10.*",
"pytest-rerunfailures==11.*",
"pytest-sugar",
"pytest-xdist==3.5.*",
"pytest==8.1.*",
"pytest-xdist==3.3.*",
"pytest==7.3.*",
"responses",
]
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2024.3.0"
__version__ = "2023.11.0.dev0"
-1
View File
@@ -111,7 +111,6 @@ LANGUAGES_RTL = {
LANGUAGES_INCUBATING = {
'fi', 'pt-br', 'gl',
}
LANGUAGES = ALL_LANGUAGES
LOCALE_PATHS = [
os.path.join(os.path.dirname(__file__), 'locale'),
]
-5
View File
@@ -19,8 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from django.contrib.auth.models import AnonymousUser
from django_scopes import scopes_disabled
from rest_framework import exceptions
@@ -31,8 +29,6 @@ from pretix.api.auth.devicesecurity import (
)
from pretix.base.models import Device
logger = logging.getLogger(__name__)
class DeviceTokenAuthentication(TokenAuthentication):
model = Device
@@ -50,7 +46,6 @@ class DeviceTokenAuthentication(TokenAuthentication):
raise exceptions.AuthenticationFailed('Device has not been initialized.')
if device.revoked:
logging.warning(f'Connection attempt of revoked device {device.pk}.')
raise exceptions.AuthenticationFailed('Device access has been revoked.')
return AnonymousUser(), device
-2
View File
@@ -185,7 +185,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('PATCH', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:order-mark-canceled'),
@@ -224,7 +223,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-list'),
)
-49
View File
@@ -1,49 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from rest_framework import exceptions
from rest_framework.authentication import (
SessionAuthentication as BaseSessionAuthentication,
)
from pretix.multidomain.middlewares import CsrfViewMiddleware
class CustomCSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
class SessionAuthentication(BaseSessionAuthentication):
# Override from DRF to user our custom CSRF middleware
def enforce_csrf(self, request):
def dummy_get_response(request): # pragma: no cover
return None
check = CustomCSRFCheck(dummy_get_response)
# populates request.META['CSRF_COOKIE'], which is used in process_view()
check.process_request(request)
reason = check.process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
+1 -1
View File
@@ -54,7 +54,7 @@ class IdempotencyMiddleware:
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
idempotency_key = request.headers.get('X-Idempotency-Key', '')
@@ -1,18 +0,0 @@
# Generated by Django 4.2.10 on 2024-02-12 11:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixapi", "0011_bigint"),
]
operations = [
migrations.AddField(
model_name="oauthapplication",
name="post_logout_redirect_uris",
field=models.TextField(default=""),
),
]
-5
View File
@@ -42,11 +42,6 @@ class OAuthApplication(AbstractApplication):
verbose_name=_("Redirection URIs"),
help_text=_("Allowed URIs list, space separated")
)
post_logout_redirect_uris = models.TextField(
blank=True, validators=[URIValidator],
help_text=_("Allowed Post Logout URIs list, space separated"),
default="",
)
client_id = models.CharField(
verbose_name=_("Client ID"),
max_length=100, unique=True, default=generate_client_id, db_index=True
-1
View File
@@ -687,7 +687,6 @@ class EventSettingsSerializer(SettingsSerializer):
'allow_modifications_after_checkin',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
+3 -6
View File
@@ -61,8 +61,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -86,8 +85,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
'require_membership', 'require_membership_types', 'require_membership_hidden',
'checkin_attention', 'checkin_text',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher', 'meta_data')
def __init__(self, *args, **kwargs):
@@ -237,8 +235,7 @@ class ItemSerializer(I18nAwareModelSerializer):
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
'personalized', 'position', 'picture',
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'personalized', 'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
+3 -5
View File
@@ -486,11 +486,11 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
'valid_from', 'valid_until', 'blocked')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked'
)
def __init__(self, *args, **kwargs):
@@ -1585,7 +1585,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.status = Order.STATUS_PAID
order.save()
order.payments.create(
@@ -1597,8 +1597,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
elif validated_data.get('status') == Order.STATUS_PAID:
if not payment_provider:
raise ValidationError('You cannot create a paid order without a payment provider.')
if validated_data.get('require_approval'):
raise ValidationError('You cannot create a paid order that requires approval.')
order.payments.create(
amount=order.total,
provider=payment_provider,
+2 -2
View File
@@ -79,8 +79,8 @@ class CustomerSerializer(I18nAwareModelSerializer):
class Meta:
model = Customer
fields = ('identifier', 'external_identifier', 'email', 'phone', 'name', 'name_parts', 'is_active',
'is_verified', 'last_login', 'date_joined', 'locale', 'last_modified', 'notes')
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
'locale', 'last_modified', 'notes')
def update(self, instance, validated_data):
if instance and instance.provider_id:
-29
View File
@@ -35,7 +35,6 @@ from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from packaging.version import parse
@@ -286,8 +285,6 @@ with scopes_disabled():
return queryset.filter(last_checked_in__isnull=not value)
def check_rules_qs(self, queryset, name, value):
if not value:
return queryset
if not self.checkinlist.rules:
return queryset
return queryset.filter(
@@ -587,32 +584,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
if media.linked_orderposition.order.event_id not in list_by_event:
# Medium exists but connected ticket is for the wrong event
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
'datetime': datetime,
'type': checkin_type,
'list': checkinlists[0].pk,
'barcode': raw_barcode,
'searched_lists': [cl.pk for cl in checkinlists]
}, user=user, auth=auth)
Checkin.objects.create(
position=None,
successful=False,
error_reason=Checkin.REASON_INVALID,
error_explanation=gettext('Medium connected to other event'),
**common_checkin_args,
)
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': gettext('Medium connected to other event'),
'require_attention': False,
'checkin_texts': [],
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())
+1 -1
View File
@@ -291,7 +291,7 @@ class EventViewSet(viewsets.ModelViewSet):
try:
with transaction.atomic():
instance.organizer.log_action(
'pretix.event.deleted', user=self.request.user, auth=self.request.auth,
'pretix.event.deleted', user=self.request.user,
data={
'event_id': instance.pk,
'name': str(instance.name),
+1 -1
View File
@@ -42,7 +42,7 @@ class IdempotencyQueryView(APIView):
idempotency_key = request.GET.get("key")
auth_hash_parts = '{}:{}'.format(
request.headers.get('Authorization', ''),
request.COOKIES.get('__Host-' + settings.SESSION_COOKIE_NAME, request.COOKIES.get(settings.SESSION_COOKIE_NAME, ''))
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
)
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
if not idempotency_key:
+3 -4
View File
@@ -176,7 +176,7 @@ class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent):
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
d = super().build_payload(logentry)
@@ -185,7 +185,6 @@ class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent
d['orderposition_id'] = logentry.parsed_data.get('position')
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
d['checkin_list'] = logentry.parsed_data.get('list')
d['type'] = logentry.parsed_data.get('type')
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
return d
@@ -297,11 +296,11 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.order.denied',
_('Order denied'),
),
ParametrizedOrderPositionCheckinWebhookEvent(
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin',
_('Ticket checked in'),
),
ParametrizedOrderPositionCheckinWebhookEvent(
ParametrizedOrderPositionWebhookEvent(
'pretix.event.checkin.reverted',
_('Ticket check-in reverted'),
),
+505 -12
View File
@@ -19,7 +19,10 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
@@ -30,21 +33,21 @@ from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import Event
from pretix.base.signals import register_html_mail_renderers
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_html_mail_renderers, register_mail_placeholders,
)
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
)
from pretix.base.services.placeholders import ( # noqa
BaseTextPlaceholder as BaseMailTextPlaceholder,
SimpleFunctionalTextPlaceholder as SimpleFunctionalMailTextPlaceholder,
)
from pretix.base.settings import get_name_parts_localized # noqa
logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend)
@@ -189,7 +192,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
tpl = get_template(self.template_name)
body_html = tpl.render(htmlctx)
inliner = css_inline.CSSInliner(keep_style_tags=False)
inliner = css_inline.CSSInliner(remove_style_tags=True)
body_html = inliner.inline(body_html)
return body_html
@@ -214,5 +217,495 @@ def base_renderers(sender, **kwargs):
return [ClassicMailRenderer, UnembellishedMailRenderer]
class BaseMailTextPlaceholder:
"""
This is the base class for for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
params = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
def get_email_context(**kwargs):
return PlaceholderContext(**kwargs).render_all()
from pretix.base.models import InvoiceAddress
event = kwargs['event']
if 'position' in kwargs:
kwargs.setdefault("position_or_address", kwargs['position'])
if 'order' in kwargs:
try:
if not kwargs.get('invoice_address'):
kwargs['invoice_address'] = kwargs['order'].invoice_address
except InvoiceAddress.DoesNotExist:
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
finally:
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
ctx = {}
for r, val in register_mail_placeholders.send(sender=event):
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
try:
ctx[v.identifier] = v.render(kwargs)
except:
ctx[v.identifier] = '(error)'
logger.exception(f'Failed to process email placeholder {v.identifier}.')
return ctx
def _placeholder_payments(order, payments):
d = []
for payment in payments:
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
else:
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
d = [line for line in d if line.strip()]
if d:
return '\n\n'.join(d)
else:
return ''
def get_best_name(position_or_address, parts=False):
"""
Return the best name we got for either an invoice address or an order position, falling back to the respective other
"""
from pretix.base.models import InvoiceAddress, OrderPosition
if isinstance(position_or_address, InvoiceAddress):
if position_or_address.name:
return position_or_address.name_parts if parts else position_or_address.name
elif position_or_address.order:
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
if isinstance(position_or_address, OrderPosition):
if position_or_address.attendee_name:
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
elif position_or_address.order:
try:
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return {} if parts else ""
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalMailTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalMailTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
lambda event_or_subevent: event_or_subevent.name
),
SimpleFunctionalMailTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
SimpleFunctionalMailTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalMailTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalMailTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalMailTextPlaceholder(
'order_email', ['order'], lambda order: order.email, 'john@example.org'
),
SimpleFunctionalMailTextPlaceholder(
'invoice_number', ['invoice'],
lambda invoice: invoice.full_invoice_no,
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
),
SimpleFunctionalMailTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
),
SimpleFunctionalMailTextPlaceholder(
'pending_sum', ['event', 'pending_sum'],
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalMailTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
),
SimpleFunctionalMailTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalMailTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event:
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
if order.modify_deadline
else '',
lambda event: date_format(
event.settings.get(
'last_order_modification_date', as_type=RelativeDateWrapper
).datetime(event).astimezone(event.timezone),
'SHORT_DATETIME_FORMAT'
) if event.settings.get('last_order_modification_date') else '',
),
SimpleFunctionalMailTextPlaceholder(
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''),
),
SimpleFunctionalMailTextPlaceholder(
'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_admission
else '',
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
),
SimpleFunctionalMailTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
),
SimpleFunctionalMailTextPlaceholder(
'subevent_date_from', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalMailTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalMailTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalMailTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret'],
'hash': order['hash'],
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
]
),
),
SimpleFunctionalMailTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalMailTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalMailTextPlaceholder(
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
),
SimpleFunctionalMailTextPlaceholder(
'name', ['name'], lambda name: name,
_('John Doe')
),
SimpleFunctionalMailTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['order', 'payments'], _placeholder_payments,
_('The amount has been charged to your card.'),
),
SimpleFunctionalMailTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalMailTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalMailTextPlaceholder(
'positionid', ['position'], lambda position: str(position.positionid),
'1'
),
SimpleFunctionalMailTextPlaceholder(
'name', ['position_or_address'],
get_best_name,
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
if "concatenation_for_salutation" in name_scheme:
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
else:
concatenation_for_salutation = name_scheme["concatenation"]
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name", ["waiting_list_entry"],
lambda waiting_list_entry: waiting_list_entry.name or "",
_("Mr Doe"),
))
ph.append(SimpleFunctionalMailTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
_("Mr Doe"),
))
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
ph.append(SimpleFunctionalMailTextPlaceholder(
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
v
))
return ph
-1
View File
@@ -86,7 +86,6 @@ class InvoiceExporterMixin:
('', _('All payment providers')),
] + [
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
if not v.is_meta
],
required=False,
help_text=_('Only include invoices for orders that have at least one payment attempt '
+11 -29
View File
@@ -209,7 +209,7 @@ class OrderListExporter(MultiSheetListExporter):
return qs.annotate(**annotations).filter(**filters)
return qs
def orders_qs(self, form_data):
def iterate_orders(self, form_data: dict):
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
@@ -250,15 +250,11 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
return qs
def iterate_orders(self, form_data: dict):
qs = self.orders_qs(form_data)
tax_rates = self._get_all_tax_rates(qs)
headers = [
_('Event slug'), _('Event name'), _('Order code'), _('Order total'), _('Status'), _('Email'),
_('Phone number'), _('Order date'), _('Order time'), _('Company'), _('Name'),
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'),
_('Order date'), _('Order time'), _('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
@@ -335,7 +331,6 @@ class OrderListExporter(MultiSheetListExporter):
row = [
self.event_object_cache[order.event_id].slug,
str(self.event_object_cache[order.event_id].name),
order.code,
order.total,
order.get_extended_status_display(),
@@ -411,7 +406,7 @@ class OrderListExporter(MultiSheetListExporter):
row += self.event_object_cache[order.event_id].meta_data.values()
yield row
def fees_qs(self, form_data):
def iterate_fees(self, form_data: dict):
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -430,14 +425,9 @@ class OrderListExporter(MultiSheetListExporter):
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
def iterate_fees(self, form_data: dict):
qs = self.fees_qs(form_data)
headers = [
_('Event slug'),
_('Event name'),
_('Order code'),
_('Status'),
_('Email'),
@@ -474,7 +464,6 @@ class OrderListExporter(MultiSheetListExporter):
tz = ZoneInfo(order.event.settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
str(self.event_object_cache[order.event_id].name),
order.code,
_("canceled") if op.canceled else order.get_extended_status_display(),
order.email,
@@ -517,19 +506,7 @@ class OrderListExporter(MultiSheetListExporter):
row += self.event_object_cache[order.event_id].meta_data.values()
yield row
def positions_qs(self, form_data: dict):
qs = OrderPosition.all.filter(
order__event__in=self.events,
)
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
def iterate_positions(self, form_data: dict):
base_qs = self.positions_qs(form_data)
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -539,6 +516,9 @@ class OrderListExporter(MultiSheetListExporter):
).values(
'm'
).order_by()
base_qs = OrderPosition.all.filter(
order__event__in=self.events,
)
qs = base_qs.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).select_related(
@@ -548,12 +528,15 @@ class OrderListExporter(MultiSheetListExporter):
'subevent', 'subevent__meta_values',
'answers', 'answers__question', 'answers__options'
)
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
qs = self._date_filter(qs, form_data, rel='order__')
has_subevents = self.events.filter(has_subevents=True).exists()
headers = [
_('Event slug'),
_('Event name'),
_('Order code'),
_('Position ID'),
_('Status'),
@@ -655,7 +638,6 @@ class OrderListExporter(MultiSheetListExporter):
tz = ZoneInfo(self.event_object_cache[order.event_id].settings.timezone)
row = [
self.event_object_cache[order.event_id].slug,
str(self.event_object_cache[order.event_id].name),
order.code,
op.positionid,
_("canceled") if op.canceled else order.get_extended_status_display(),
+6 -17
View File
@@ -609,38 +609,27 @@ class BaseQuestionsForm(forms.Form):
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
else:
max_date = None
min_date = now()
initial = None
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
if pos.used_membership.date_start >= now():
initial = min_date = pos.used_membership.date_start
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
attrs = {}
if max_date:
attrs['data-max'] = max_date.date().isoformat()
if min_date:
attrs['data-min'] = min_date.date().isoformat()
self.fields['requested_valid_from'] = forms.DateField(
label=_('Start date'),
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=bool(initial),
initial=pos.requested_valid_from or initial,
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=False,
widget=DatePickerWidget(attrs),
validators=([MaxDateValidator(max_date.date())] if max_date else []) + [MinDateValidator(min_date.date())]
validators=[MaxDateValidator(max_date.date())] if max_date else []
)
else:
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
label=_('Start date'),
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=bool(initial),
initial=pos.requested_valid_from or initial,
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
required=False,
widget=SplitDateTimePickerWidget(
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
min_date=min_date,
max_date=max_date
),
validators=([MaxDateTimeValidator(max_date)] if max_date else []) + [MinDateTimeValidator(min_date)]
validators=[MaxDateTimeValidator(max_date)] if max_date else []
)
add_fields = {}
+63
View File
@@ -0,0 +1,63 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from bootstrap3.renderers import (
FieldRenderer as BaseFieldRenderer,
InlineFieldRenderer as BaseInlineFieldRenderer,
)
from django.forms import (
CheckboxInput, CheckboxSelectMultiple, ClearableFileInput, RadioSelect,
SelectDateWidget,
)
class FieldRenderer(BaseFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html
class InlineFieldRenderer(BaseInlineFieldRenderer):
# Local application of https://github.com/zostera/django-bootstrap3/pull/859
def post_widget_render(self, html):
if isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, "checkbox")
elif isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, "radio")
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html
+3 -3
View File
@@ -33,7 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import os
from datetime import datetime
from datetime import date
from django import forms
from django.utils.formats import get_format
@@ -188,11 +188,11 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs['autocomplete'] = 'off'
if min_date:
date_attrs['data-min'] = (
min_date if not isinstance(min_date, datetime) else min_date.astimezone(get_current_timezone()).date()
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
).isoformat()
if max_date:
date_attrs['data-max'] = (
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
max_date if isinstance(max_date, date) else max_date.astimezone(get_current_timezone()).date()
).isoformat()
def date_placeholder():
+4 -35
View File
@@ -182,7 +182,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI')
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
for family, styles in get_fonts().items():
if family == self.event.settings.invoice_renderer_font:
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
self.font_regular = family
@@ -625,7 +625,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
)]
else:
tdata = [(
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
)]
@@ -855,7 +855,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
class Modern1Renderer(ClassicInvoiceRenderer):
identifier = 'modern1'
verbose_name = gettext_lazy('Default invoice renderer (European-style letter)')
verbose_name = gettext_lazy('Modern Invoice Renderer (pretix 2.7)')
bottom_margin = 16.9 * mm
top_margin = 16.9 * mm
right_margin = 20 * mm
@@ -989,37 +989,6 @@ class Modern1Renderer(ClassicInvoiceRenderer):
canvas.drawText(textobject)
class Modern1SimplifiedRenderer(Modern1Renderer):
identifier = 'modern1simplified'
verbose_name = gettext_lazy('Simplified invoice renderer')
logo_left = Modern1Renderer.left_margin
logo_width = pagesizes.A4[0] - Modern1Renderer.right_margin - logo_left
logo_height = 25 * mm
logo_top = 13 * mm
logo_anchor = 'nw'
def _draw_invoice_from(self, canvas):
super(Modern1Renderer, self)._draw_invoice_from(canvas)
def _draw_event(self, canvas):
pass
def _get_intro(self):
i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(Paragraph(
pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(),
),
self.stylesheet['Normal'],
))
i.append(Spacer(2 * mm, 2 * mm))
return i + super()._get_intro()
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
def recv_classic(sender, **kwargs):
return [ClassicInvoiceRenderer, Modern1Renderer, Modern1SimplifiedRenderer]
return [ClassicInvoiceRenderer, Modern1Renderer]
+16 -43
View File
@@ -20,7 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
from collections import OrderedDict
from urllib.parse import urlparse, urlsplit
from urllib.parse import urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings
@@ -40,30 +40,10 @@ from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
from pretix.presale.style import get_fonts
_supported = None
def get_supported_language(requested_language, allowed_languages, default_language):
language = requested_language
if language not in allowed_languages:
firstpart = language.split('-')[0]
if firstpart in allowed_languages:
language = firstpart
else:
language = default_language
for lang in allowed_languages:
if lang.startswith(firstpart + '-'):
language = lang
break
if language not in allowed_languages:
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
# not part of settings.locales
language = allowed_languages[0]
return language
class LocaleMiddleware(MiddlewareMixin):
"""
@@ -85,11 +65,20 @@ class LocaleMiddleware(MiddlewareMixin):
settings_holder = None
if settings_holder:
language = get_supported_language(
language,
settings_holder.settings.locales,
settings_holder.settings.locale,
)
if language not in settings_holder.settings.locales:
firstpart = language.split('-')[0]
if firstpart in settings_holder.settings.locales:
language = firstpart
else:
language = settings_holder.settings.locale
for lang in settings_holder.settings.locales:
if lang.startswith(firstpart + '-'):
language = lang
break
if language not in settings_holder.settings.locales:
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
# not part of settings.locales
language = settings_holder.settings.locales[0]
if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region
else:
@@ -241,14 +230,6 @@ class SecurityMiddleware(MiddlewareMixin):
)
def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
@@ -268,14 +249,6 @@ class SecurityMiddleware(MiddlewareMixin):
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = {
'default-src': ["{static}"],
'script-src': ['{static}'],
@@ -284,7 +257,7 @@ class SecurityMiddleware(MiddlewareMixin):
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"] + list(font_src),
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
@@ -1,22 +0,0 @@
# Generated by Django 4.2.4 on 2023-11-22 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0254_alter_logentry_organizer_link_and_more"),
]
operations = [
migrations.AddField(
model_name="item",
name="available_from_mode",
field=models.CharField(default="hide", max_length=16),
),
migrations.AddField(
model_name="item",
name="available_until_mode",
field=models.CharField(default="hide", max_length=16),
)
]
@@ -1,22 +0,0 @@
# Generated by Django 4.2.4 on 2024-01-11 15:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0255_item_unavail_modes"),
]
operations = [
migrations.AddField(
model_name="itemvariation",
name="available_from_mode",
field=models.CharField(default="hide", max_length=16),
),
migrations.AddField(
model_name="itemvariation",
name="available_until_mode",
field=models.CharField(default="hide", max_length=16),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 4.2.9 on 2024-01-30 11:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0256_itemvariation_unavail_modes"),
]
operations = [
migrations.AlterField(
model_name="item",
name="default_price",
field=models.DecimalField(decimal_places=2, default=0, max_digits=13),
preserve_default=False,
),
]
+27 -15
View File
@@ -37,7 +37,9 @@ import json
import operator
from datetime import timedelta
from functools import reduce
from urllib.parse import urlparse
import webauthn
from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin,
@@ -51,12 +53,13 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_otp.models import Device
from django_scopes import scopes_disabled
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
from u2flib_server.utils import (
pub_key_from_der, websafe_decode, websafe_encode,
)
from pretix.base.i18n import language
from pretix.helpers.urls import build_absolute_uri
from ...helpers.u2f import pub_key_from_der, websafe_decode
from .base import LoggingMixin
@@ -605,12 +608,7 @@ class U2FDevice(Device):
json_data = models.TextField()
@property
def webauthndevice(self):
d = json.loads(self.json_data)
return PublicKeyCredentialDescriptor(websafe_decode(d['keyHandle']))
@property
def webauthnpubkey(self):
def webauthnuser(self):
d = json.loads(self.json_data)
# We manually need to convert the pubkey from DER format (used in our
# former U2F implementation) to the format required by webauthn. This
@@ -622,7 +620,16 @@ class U2FDevice(Device):
pub_key.public_numbers().x, pub_key.public_numbers().y
)
)
return pub_key
return webauthn.WebAuthnUser(
d['keyHandle'],
self.user.email,
str(self.user),
settings.SITE_URL,
d['keyHandle'],
websafe_encode(pub_key),
1,
urlparse(settings.SITE_URL).netloc
)
class WebAuthnDevice(Device):
@@ -634,9 +641,14 @@ class WebAuthnDevice(Device):
sign_count = models.IntegerField(default=0)
@property
def webauthndevice(self):
return PublicKeyCredentialDescriptor(websafe_decode(self.credential_id))
@property
def webauthnpubkey(self):
return websafe_decode(self.pub_key)
def webauthnuser(self):
return webauthn.WebAuthnUser(
self.ukey,
self.user.email,
str(self.user),
settings.SITE_URL,
self.credential_id,
self.pub_key,
self.sign_count,
urlparse(settings.SITE_URL).netloc
)
-11
View File
@@ -229,14 +229,6 @@ class EventMixin:
else:
return self.presale_end
@property
def waiting_list_active(self):
if not self.settings.waiting_list_enabled:
return False
if self.settings.waiting_list_auto_disable:
return self.settings.waiting_list_auto_disable.datetime(self) > now()
return True
@property
def presale_has_ended(self):
"""
@@ -273,9 +265,6 @@ class EventMixin:
@property
def event_microdata(self):
if self.settings.event_microdata:
return self.settings.event_microdata
import json
eventdict = {
+3 -66
View File
@@ -263,8 +263,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
)
if not allow_addons:
@@ -374,13 +374,6 @@ class Item(LoggedModel):
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
)
UNAVAIL_MODE_HIDDEN = "hide"
UNAVAIL_MODE_INFO = "info"
UNAVAIL_MODES = (
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
)
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
@@ -430,7 +423,7 @@ class Item(LoggedModel):
help_text=_("If this product has multiple variations, you can set different prices for each of the "
"variations. If a variation does not have a special price or if you do not have variations, "
"this price will be used."),
max_digits=13, decimal_places=2,
max_digits=13, decimal_places=2, null=True
)
free_price = models.BooleanField(
default=False,
@@ -494,21 +487,11 @@ class Item(LoggedModel):
null=True, blank=True,
help_text=_('This product will not be sold before the given date.')
)
available_from_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
available_until_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
hidden_if_available = models.ForeignKey(
'Quota',
null=True, blank=True,
@@ -720,8 +703,6 @@ class Item(LoggedModel):
return str(self.internal_name or self.name)
def save(self, *args, **kwargs):
if self.hide_without_voucher:
self.require_voucher = True
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()
@@ -799,24 +780,6 @@ class Item(LoggedModel):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or now()
subevent_item = subevent and subevent.item_overrides.get(self.pk)
if not self.active:
return 'active'
elif self.available_from and self.available_from > now_dt:
return 'available_from'
elif self.available_until and self.available_until < now_dt:
return 'available_until'
elif (self.require_voucher or self.hide_without_voucher) and not has_voucher:
return 'require_voucher'
elif subevent_item and subevent_item.available_from and subevent_item.available_from > now_dt:
return 'available_from'
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
return 'available_until'
else:
return None
def _get_quotas(self, ignored_quotas=None, subevent=None):
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
@@ -1115,21 +1078,11 @@ class ItemVariation(models.Model):
null=True, blank=True,
help_text=_('This variation will not be sold before the given date.')
)
available_from_mode = models.CharField(
choices=Item.UNAVAIL_MODES,
default=Item.UNAVAIL_MODE_HIDDEN,
max_length=16,
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This variation will not be sold after the given date.')
)
available_until_mode = models.CharField(
choices=Item.UNAVAIL_MODES,
default=Item.UNAVAIL_MODE_HIDDEN,
max_length=16,
)
sales_channels = fields.MultiStringField(
verbose_name=_('Sales channels'),
default=_all_sales_channels_identifiers,
@@ -1307,22 +1260,6 @@ class ItemVariation(models.Model):
return False
return True
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
now_dt = now_dt or now()
subevent_var = subevent and subevent.var_overrides.get(self.pk)
if not self.active:
return 'active'
elif self.available_from and self.available_from > now_dt:
return 'available_from'
elif self.available_until and self.available_until < now_dt:
return 'available_until'
elif subevent_var and subevent_var.available_from and subevent_var.available_from > now_dt:
return 'available_from'
elif subevent_var and subevent_var.available_until and subevent_var.available_until < now_dt:
return 'available_until'
else:
return None
@property
def meta_data(self):
data = self.item.meta_data
+3 -8
View File
@@ -49,8 +49,7 @@ class MembershipType(LoggedModel):
allow_parallel_usage = models.BooleanField(
verbose_name=_('Parallel usage is allowed'),
help_text=_('If this is selected, the membership can be used to purchase tickets for events happening at the same time. Note '
'that this will only check for an identical start time of the events, not for any overlap between events. An overlap '
'check will be performed if there is a product-level validity of the ticket.'),
'that this will only check for an identical start time of the events, not for any overlap between events.'),
default=False
)
max_usages = models.PositiveIntegerField(
@@ -163,12 +162,8 @@ class Membership(models.Model):
def attendee_name(self):
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
if valid_from_not_chosen:
return not self.canceled and self.date_end >= now()
elif ticket_valid_from:
dt = ticket_valid_from
elif ev:
def is_valid(self, ev=None):
if ev:
dt = ev.date_from
else:
dt = now()
+15 -16
View File
@@ -44,7 +44,7 @@ from datetime import datetime, time, timedelta
from decimal import Decimal
from functools import reduce
from time import sleep
from typing import Any, Dict, Iterable, List, Union
from typing import Any, Dict, List, Union
from zoneinfo import ZoneInfo
import dateutil
@@ -79,7 +79,7 @@ from pretix.base.i18n import language
from pretix.base.models import Customer, User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
from pretix.base.signals import order_gracefully_delete
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
@@ -451,9 +451,9 @@ class Order(LockModel, LoggedModel):
if results:
qs = qs.annotate(
is_overpaid=Case(
When(~Q(status__in=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)) & Q(pending_sum_t__lt=-1e-8),
When(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=-1e-8),
then=Value(1)),
When(Q(status__in=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)) & Q(pending_sum_rc__lt=-1e-8),
When(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=-1e-8),
then=Value(1)),
default=Value(0),
output_field=models.IntegerField()
@@ -468,7 +468,7 @@ class Order(LockModel, LoggedModel):
is_underpaid=Case(
When(Q(status=Order.STATUS_PAID) & Q(pending_sum_t__gt=1e-8),
then=Value(1)),
When(Q(status__in=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)) & Q(pending_sum_rc__gt=1e-8),
When(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__gt=1e-8),
then=Value(1)),
default=Value(0),
output_field=models.IntegerField()
@@ -1090,6 +1090,9 @@ class Order(LockModel, LoggedModel):
if not self.email and not (position and position.attendee_email):
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale, self.event.settings.region):
recipient = self.email
if position and position.attendee_email:
@@ -1134,19 +1137,12 @@ class Order(LockModel, LoggedModel):
attach_tickets=True,
)
@property
def positions_with_tickets_ignoring_plugins(self):
return (op for op in self.positions.select_related('item') if op.generate_ticket)
@property
def positions_with_tickets(self):
signal_response = allow_ticket_download.send(self.event, order=self)
if all([r is True for rr, r in signal_response]):
return self.positions_with_tickets_ignoring_plugins
elif any([r is False for rr, r in signal_response]):
return []
else:
return set.intersection(set(self.positions_with_tickets_ignoring_plugins), *[set(r) for rr, r in signal_response if isinstance(r, Iterable)])
for op in self.positions.select_related('item'):
if not op.generate_ticket:
continue
yield op
def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False,
_backfill_before_cancellation=False, save=True):
@@ -2654,6 +2650,9 @@ class OrderPosition(AbstractPosition):
if not self.attendee_email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
try:
+8 -20
View File
@@ -251,8 +251,7 @@ class Voucher(LoggedModel):
null=True, blank=True,
on_delete=models.PROTECT, # We use a fake version of SET_NULL in Item.delete()
help_text=_(
"This product is added to the user's cart if the voucher is redeemed. Instead of a specific product, you "
"can also select a quota. In this case, all products assigned to this quota can be selected."
"This product is added to the user's cart if the voucher is redeemed."
)
)
variation = models.ForeignKey(
@@ -351,6 +350,9 @@ class Voucher(LoggedModel):
'variations.'))
if variation and not item.variations.filter(pk=variation.pk).exists():
raise ValidationError(_('This variation does not belong to this product.'))
if item.has_variations and not variation and data.get('block_quota'):
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
'Otherwise it might be unclear which quotas to block.'))
if item.category and item.category.is_addon:
raise ValidationError(_('It is currently not possible to create vouchers for add-on products.'))
elif block_quota:
@@ -428,15 +430,7 @@ class Voucher(LoggedModel):
elif old_instance.variation:
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
elif old_instance.item:
if old_instance.item.has_variations:
quotas |= set(
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
itemvariation__item=old_instance.item,
quota__subevent=old_instance.subevent,
).values('quota_id'))
)
else:
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
return quotas
@staticmethod
@@ -451,19 +445,13 @@ class Voucher(LoggedModel):
if quota:
new_quotas = {quota}
elif item and item.has_variations and not variation:
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
'Otherwise it might be unclear which quotas to block.'))
elif item and variation:
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
elif item and not item.has_variations:
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
elif item and item.has_variations:
new_quotas = set(
Quota.objects.filter(
pk__in=Quota.variations.through.objects.filter(
itemvariation__item=old_instance.item,
quota__subevent=data.get('subevent'),
).values('quota_id')
)
)
else:
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
'tickets.'))
+3
View File
@@ -259,6 +259,9 @@ class WaitingListEntry(LoggedModel):
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale, self.event.settings.region):
recipient = self.email
+4 -2
View File
@@ -849,7 +849,7 @@ class BasePaymentProvider:
except InvoiceAddress.DoesNotExist:
pass
else:
if str(ia.country) != '' and str(ia.country) not in restricted_countries:
if str(ia.country) not in restricted_countries:
return False
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
@@ -1311,7 +1311,9 @@ class GiftCardPayment(BasePaymentProvider):
@property
def public_name(self) -> str:
return str(self.settings.get("public_name", as_type=LazyI18nString) or _("Gift card"))
return str(self.settings.get("public_name", as_type=LazyI18nString)) or _(
"Gift card"
)
@property
def settings_form_fields(self):
+3 -10
View File
@@ -78,7 +78,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.models import Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -738,10 +738,9 @@ class Renderer:
else:
self.bg_bytes = None
self.bg_pdf = None
self.event_fonts = list(get_fonts(event, pdf_support_required=True).keys()) + ['Open Sans']
@classmethod
def _register_fonts(cls, event: Event = None):
def _register_fonts(cls):
if hasattr(cls, '_fonts_registered'):
return
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
@@ -749,7 +748,7 @@ class Renderer:
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
for family, styles in get_fonts(event, pdf_support_required=True).items():
for family, styles in get_fonts().items():
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
if 'italic' in styles:
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
@@ -940,12 +939,6 @@ class Renderer:
if o['italic']:
font += ' I'
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
# should not have access to.
if font not in self.event_fonts:
logger.warning(f'Unauthorized use of font "{font}"')
font = 'Open Sans'
try:
ad = getAscentDescent(font, float(o['fontsize']))
except KeyError: # font not known, fall back
+7 -50
View File
@@ -113,15 +113,6 @@ error_messages = {
'Some of the products you selected are no longer available in '
'the quantity you selected. Please see below for details.'
),
'unavailable_listed': gettext_lazy(
'Some of the products you selected are no longer available. '
'The following products are affected and have not been added to your cart: %s'
),
'in_part_listed': gettext_lazy(
'Some of the products you selected are no longer available in '
'the quantity you selected. The following products are affected and have not '
'been added to your cart: %s'
),
'max_items': ngettext_lazy(
"You cannot select more than %s item per order.",
"You cannot select more than %s items per order."
@@ -203,7 +194,7 @@ error_messages = {
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'),
'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'),
'seat_required': gettext_lazy('You need to select a specific seat.'),
@@ -1114,8 +1105,6 @@ class CartManager:
if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2)
err_unavailable_products = []
for iop, op in enumerate(self._operations):
if isinstance(op, self.RemoveOperation):
if op.position.expires > self.now_dt:
@@ -1143,15 +1132,9 @@ class CartManager:
voucher_available_count = min(voucher_available_count, vouchers_ok[op.voucher])
if quota_available_count < 1:
err = err or error_messages['unavailable_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
err = err or error_messages['unavailable']
elif quota_available_count < requested_count:
err = err or error_messages['in_part_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
err = err or error_messages['in_part']
if voucher_available_count < 1:
if op.voucher in self._voucher_depend_on_cart:
@@ -1168,25 +1151,16 @@ class CartManager:
b_quotas = list(b.quotas)
if not b_quotas:
if not op.voucher or not op.voucher.allow_ignore_quota:
err = err or error_messages['unavailable_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
err = err or error_messages['unavailable']
available_count = 0
continue
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b_quotas))
if b_quota_available_count < b.count:
err = err or error_messages['unavailable_listed']
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
err = err or error_messages['unavailable']
available_count = 0
elif b_quota_available_count < available_count * b.count:
err = err or error_messages['in_part_listed']
err = err or error_messages['in_part']
available_count = b_quota_available_count // b.count
err_unavailable_products.append(
f'{op.item.name} {op.variation}' if op.variation else op.item.name
)
for q in b_quotas:
quotas_ok[q] -= available_count * b.count
# TODO: is this correct?
@@ -1325,23 +1299,10 @@ class CartManager:
op.position.price_after_voucher = op.price_after_voucher
op.position.voucher = op.voucher
# op.position.price will be set in recompute_final_prices_and_taxes
# op.posiiton.price will be set in recompute_final_prices_and_taxes
op.position.save(update_fields=['price_after_voucher', 'voucher'])
vouchers_ok[op.voucher] -= 1
if op.voucher.all_bundles_included or op.voucher.all_addons_included:
for a in op.position.addons.all():
if a.is_bundled and op.voucher.all_bundles_included and a.price:
a.listed_price = Decimal("0.00")
a.price_after_voucher = Decimal("0.00")
# a.price will be set in recompute_final_prices_and_taxes
a.save(update_fields=['listed_price', 'price_after_voucher'])
elif not a.is_bundled and op.voucher.all_addons_included and a.price and not a.custom_price_input:
a.listed_price = Decimal("0.00")
a.price_after_voucher = Decimal("0.00")
# op.positon.price will be set in recompute_final_prices_and_taxes
a.save(update_fields=['listed_price', 'price_after_voucher'])
for p in new_cart_positions:
if getattr(p, '_answers', None):
if not p.pk: # We stored some to the database already before
@@ -1351,10 +1312,6 @@ class CartManager:
if 'sleep-before-commit' in debugflags_var.get():
sleep(2)
if err in (error_messages['unavailable_listed'], error_messages['in_part_listed']):
err = err % ', '.join(str(p) for p in err_unavailable_products)
return err
def recompute_final_prices_and_taxes(self):
+7 -7
View File
@@ -104,10 +104,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
expire_date=date_format(invoice.order.expires, "SHORT_DATE_FORMAT")
)
invoice.introductory_text = str(introductory).replace('\n', '<br />').replace('\r', '')
invoice.additional_text = str(additional).replace('\n', '<br />').replace('\r', '')
invoice.introductory_text = str(introductory).replace('\n', '<br />')
invoice.additional_text = str(additional).replace('\n', '<br />')
invoice.footer_text = str(footer)
invoice.payment_provider_text = str(payment).replace('\n', '<br />').replace('\r', '')
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
invoice.payment_provider_stamp = str(payment_stamp) if payment_stamp else None
try:
@@ -462,10 +462,10 @@ def build_preview_invoice_pdf(event):
footer = event.settings.get('invoice_footer_text', as_type=LazyI18nString)
payment = _("A payment provider specific text might appear here.")
invoice.introductory_text = str(introductory).replace('\n', '<br />').replace('\r', '')
invoice.additional_text = str(additional).replace('\n', '<br />').replace('\r', '')
invoice.introductory_text = str(introductory).replace('\n', '<br />')
invoice.additional_text = str(additional).replace('\n', '<br />')
invoice.footer_text = str(footer)
invoice.payment_provider_text = str(payment).replace('\n', '<br />').replace('\r', '')
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
invoice.payment_provider_stamp = _('paid')
invoice.invoice_to_name = _("John Doe")
invoice.invoice_to_street = _("214th Example Street")
@@ -488,7 +488,7 @@ def build_preview_invoice_pdf(event):
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product {}").format(i + 1),
gross_value=tax.gross, tax_value=tax.tax,
tax_rate=tax.rate, tax_name=tax.name
tax_rate=tax.rate
)
else:
for i in range(5):
+4 -1
View File
@@ -186,6 +186,10 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
headers.setdefault('X-Mailer', 'pretix')
with language(locale):
if isinstance(context, dict) and event:
for k, v in event.meta_data.items():
context['meta_' + k] = v
if isinstance(context, dict) and order:
try:
context.update({
@@ -383,7 +387,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
if event:
with scopes_disabled():
event = Event.objects.get(id=event)
organizer = event.organizer
backend = event.get_mail_backend()
cm = lambda: scope(organizer=event.organizer) # noqa
elif organizer:
+23 -73
View File
@@ -29,8 +29,8 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.models import (
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
OrderPosition, SubEvent,
AbstractPosition, Customer, Event, Item, Membership, Order, OrderPosition,
SubEvent,
)
from pretix.helpers import OF_SELF
@@ -82,8 +82,7 @@ def create_membership(customer: Customer, position: OrderPosition):
)
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False,
valid_from_not_chosen=False):
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False):
"""
Validate that a set of cart or order positions. This currently does not validate
@@ -93,8 +92,6 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
:param lock: Whether to place a SELECT FOR UPDATE lock on the selected memberships
:param ignored_order: An order that should be ignored for usage counting
:param testmode: If ``True``, only test mode memberships are allowed. If ``False``, test mode memberships are not allowed.
:param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
"""
tz = event.timezone
applicable_positions = [
@@ -135,11 +132,7 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
qs = qs.exclude(order_id=ignored_order.pk)
m._used_at_dates = [
(op.subevent or op.order.event).date_from
for op in qs if not op.valid_from or not op.valid_until
]
m._used_for_ranges = [
(op.valid_from, op.valid_until)
for op in qs if op.valid_from or op.valid_until
for op in qs
]
for p in applicable_positions:
@@ -154,44 +147,22 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
_('You selected membership that has been canceled.')
)
if m.testmode and not testmode:
if m.testmode != testmode:
raise ValidationError(
_('You can not use a test mode membership for tickets that are not in test mode.')
)
elif not m.testmode and testmode:
raise ValidationError(
_('You need to add a test mode membership to the customer account to use it in test mode.')
_('You can only use a test mode membership for test mode tickets.')
)
ev = p.subevent or event
if isinstance(p, (OrderPosition, CartPosition)):
# override_ variants are for usage of fake cart in OrderChangeManager
valid_from = getattr(p, 'override_valid_from', p.valid_from)
valid_until = getattr(p, 'override_valid_until', p.valid_until)
else: # future safety, not technically defined on AbstractPosition
valid_from = None
valid_until = None
if not m.is_valid(ev, valid_from, valid_from_not_chosen=p.item.validity_dynamic_start_choice and valid_from_not_chosen):
if valid_from:
raise ValidationError(
_('You selected a membership that is valid from {start} to {end}, but selected a ticket that '
'starts to be valid on {date}.').format(
start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
)
else:
raise ValidationError(
_('You selected a membership that is valid from {start} to {end}, but selected an event '
'taking place at {date}.').format(
start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
if not m.is_valid(ev):
raise ValidationError(
_('You selected a membership that is valid from {start} to {end}, but selected an event '
'taking place at {date}.').format(
start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
)
if p.variation and p.variation.require_membership:
types = p.variation.require_membership_types.all()
@@ -217,34 +188,13 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
m.usages += 1
if not m.membership_type.allow_parallel_usage:
if (valid_from or valid_until) and not (p.item.validity_dynamic_start_choice and valid_from_not_chosen):
for used_range in m._used_for_ranges:
if valid_from and valid_from > used_range[1]:
continue
if valid_until and valid_until < used_range[0]:
continue
raise ValidationError(
_('You are trying to use a membership of type "{type}" for a ticket valid from {valid_from} '
'until {valid_until}, however you already used the same membership for a different ticket '
'that overlaps with this time frame ({conflict_from} {conflict_until}).').format(
type=m.membership_type.name,
valid_from=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_from else _('start'),
valid_until=date_format(valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_until else _('open end'),
conflict_from=date_format(used_range[0].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[0] else _('start'),
conflict_until=date_format(used_range[1].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[1] else _('open end'),
)
df = ev.date_from
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
raise ValidationError(
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
'however you already used the same membership for a different ticket at the same time.').format(
type=m.membership_type.name,
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
m._used_for_ranges.append((p.valid_from, p.valid_until))
if not valid_from or not valid_until:
df = ev.date_from
if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
raise ValidationError(
_('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
'however you already used the same membership for a different ticket at the same time.').format(
type=m.membership_type.name,
date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
)
)
m._used_at_dates.append(ev.date_from)
)
m._used_at_dates.append(ev.date_from)
+1 -1
View File
@@ -136,7 +136,7 @@ def send_notification_mail(notification: Notification, user: User):
tpl_html = get_template('pretixbase/email/notification.html')
body_html = tpl_html.render(ctx)
inliner = css_inline.CSSInliner(keep_style_tags=False)
inliner = css_inline.CSSInliner(remove_style_tags=True)
body_html = inliner.inline(body_html)
tpl_plain = get_template('pretixbase/email/notification.txt')
+20 -17
View File
@@ -98,9 +98,10 @@ from pretix.base.services.pricing import (
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
from pretix.base.signals import (
order_approved, order_canceled, order_changed, order_denied, order_expired,
order_fee_calculation, order_paid, order_placed, order_split,
order_valid_if_pending, periodic_task, validate_order,
allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_paid,
order_placed, order_split, order_valid_if_pending, periodic_task,
validate_order,
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF
@@ -197,7 +198,7 @@ error_messages = {
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'),
}
@@ -220,7 +221,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
is_available = order._is_still_available(now(), count_waitinglist=False, check_voucher_usage=True,
check_memberships=True, lock=True, force=force)
if is_available is True:
if order.payment_refund_sum >= order.total and not order.require_approval:
if order.payment_refund_sum >= order.total:
order.status = Order.STATUS_PAID
else:
order.status = Order.STATUS_PENDING
@@ -1407,16 +1408,23 @@ def send_download_reminders(sender, **kwargs):
if o.download_reminder_sent:
# Race condition
continue
positions = list(o.positions_with_tickets)
if not positions:
if not all([r for rr, r in allow_ticket_download.send(event, order=o)]):
continue
if not o.ticket_download_available:
continue
positions = o.positions.select_related('item')
if o.status != Order.STATUS_PAID:
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
continue
send = False
for p in positions:
if p.generate_ticket:
send = True
break
if not send:
continue
with language(o.locale, o.event.settings.region):
o.download_reminder_sent = True
@@ -1434,7 +1442,10 @@ def send_download_reminders(sender, **kwargs):
logger.exception('Reminder email could not be sent')
if event.settings.mail_send_download_reminder_attendee:
for p in positions:
for p in o.positions.all():
if not p.generate_ticket:
continue
if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
hour=0, minute=0, second=0, microsecond=0
@@ -2500,7 +2511,7 @@ class OrderChangeManager:
remaining_total = sum([p.price for p in self.order.positions.all()]) + sum([f.value for f in self.order.fees.all()])
offset_amount = min(max(0, self.completed_payment_sum - remaining_total), split_order.total)
if offset_amount >= split_order.total and not split_order.require_approval:
if offset_amount >= split_order.total:
split_order.status = Order.STATUS_PAID
else:
split_order.status = Order.STATUS_PENDING
@@ -2671,7 +2682,6 @@ class OrderChangeManager:
for p in self.order.positions.all():
cp = CartPosition(
event=self.event,
item=p.item,
variation=p.variation,
attendee_name_parts=p.attendee_name_parts,
@@ -2692,23 +2702,16 @@ class OrderChangeManager:
positions_to_fake_cart[op.position].seat = op.seat
elif isinstance(op, self.MembershipOperation):
positions_to_fake_cart[op.position].used_membership = op.membership
elif isinstance(op, self.ChangeValidFromOperation):
positions_to_fake_cart[op.position].override_valid_from = op.valid_from
elif isinstance(op, self.ChangeValidUntilOperation):
positions_to_fake_cart[op.position].override_valid_until = op.valid_until
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
fake_cart.remove(positions_to_fake_cart[op.position])
elif isinstance(op, self.AddOperation):
cp = CartPosition(
event=self.event,
item=op.item,
variation=op.variation,
used_membership=op.membership,
subevent=op.subevent,
seat=op.seat,
)
cp.override_valid_from = op.valid_from
cp.override_valid_until = op.valid_until
fake_cart.append(cp)
try:
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)
-584
View File
@@ -1,584 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import inspect
import logging
from datetime import timedelta
from decimal import Decimal
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from pretix.base.forms import PlaceholderValidator
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
register_mail_placeholders, register_text_placeholders,
)
from pretix.helpers.format import SafeFormatter
logger = logging.getLogger('pretix.base.services.placeholders')
class BaseTextPlaceholder:
"""
This is the base class for all email text placeholders.
"""
@property
def required_context(self):
"""
This property should return a list of all attribute names that need to be
contained in the base context so that this placeholder is available. By default,
it returns a list containing the string "event".
"""
return ["event"]
@property
def identifier(self):
"""
This should return the identifier of this placeholder in the email.
"""
raise NotImplementedError()
def render(self, context):
"""
This method is called to generate the actual text that is being
used in the email. You will be passed a context dictionary with the
base context attributes specified in ``required_context``. You are
expected to return a plain-text string.
"""
raise NotImplementedError()
def render_sample(self, event):
"""
This method is called to generate a text to be used in email previews.
This may only depend on the event.
"""
raise NotImplementedError()
class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
def __init__(self, identifier, args, func, sample):
self._identifier = identifier
self._args = args
self._func = func
self._sample = sample
@property
def identifier(self):
return self._identifier
@property
def required_context(self):
return self._args
def render(self, context):
return self._func(**{k: context[k] for k in self._args})
def render_sample(self, event):
if callable(self._sample):
return self._sample(event)
else:
return self._sample
class PlaceholderContext(SafeFormatter):
"""
Holds the contextual arguments and corresponding list of available placeholders for formatting
an email or other templated text.
Example:
context = PlaceholderContext(event=my_event, order=my_order)
formatted_doc = context.format(input_doc)
"""
def __init__(self, **kwargs):
super().__init__({})
self.context_args = kwargs
self._extend_context_args()
self.placeholders = {}
self.cache = {}
event = kwargs['event']
for r, val in [
*register_mail_placeholders.send(sender=event),
*register_text_placeholders.send(sender=event)
]:
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in kwargs for rp in v.required_context):
self.placeholders[v.identifier] = v
def _extend_context_args(self):
from pretix.base.models import InvoiceAddress
if 'position' in self.context_args:
self.context_args.setdefault("position_or_address", self.context_args['position'])
if 'order' in self.context_args:
try:
if not self.context_args.get('invoice_address'):
self.context_args['invoice_address'] = self.context_args['order'].invoice_address
except InvoiceAddress.DoesNotExist:
self.context_args['invoice_address'] = InvoiceAddress(order=self.context_args['order'])
finally:
self.context_args.setdefault("position_or_address", self.context_args['invoice_address'])
def render_placeholder(self, placeholder):
try:
return self.cache[placeholder.identifier]
except KeyError:
try:
value = self.cache[placeholder.identifier] = placeholder.render(self.context_args)
return value
except:
logger.exception(f'Failed to process template placeholder {placeholder.identifier}.')
return '(error)'
def render_all(self):
return {identifier: self.render_placeholder(placeholder)
for (identifier, placeholder) in self.placeholders.items()}
def get_value(self, key, args, kwargs):
if key not in self.placeholders:
return '{' + str(key) + '}'
return self.render_placeholder(self.placeholders[key])
def _placeholder_payments(order, payments):
d = []
for payment in payments:
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
else:
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
d = [line for line in d if line.strip()]
if d:
return '\n\n'.join(d)
else:
return ''
def get_best_name(position_or_address, parts=False):
"""
Return the best name we got for either an invoice address or an order position, falling back to the respective other
"""
from pretix.base.models import InvoiceAddress, OrderPosition
if isinstance(position_or_address, InvoiceAddress):
if position_or_address.name:
return position_or_address.name_parts if parts else position_or_address.name
elif position_or_address.order:
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
if isinstance(position_or_address, OrderPosition):
if position_or_address.attendee_name:
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
elif position_or_address.order:
try:
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
except InvoiceAddress.DoesNotExist:
pass
return {} if parts else ""
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
def base_placeholders(sender, **kwargs):
from pretix.multidomain.urlreverse import build_absolute_uri
ph = [
SimpleFunctionalTextPlaceholder(
'event', ['event'], lambda event: event.name, lambda event: event.name
),
SimpleFunctionalTextPlaceholder(
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
lambda event_or_subevent: event_or_subevent.name
),
SimpleFunctionalTextPlaceholder(
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
),
SimpleFunctionalTextPlaceholder(
'code', ['order'], lambda order: order.code, 'F8VVL'
),
SimpleFunctionalTextPlaceholder(
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
),
SimpleFunctionalTextPlaceholder(
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
),
SimpleFunctionalTextPlaceholder(
'order_email', ['order'], lambda order: order.email, 'john@example.org'
),
SimpleFunctionalTextPlaceholder(
'invoice_number', ['invoice'],
lambda invoice: invoice.full_invoice_no,
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
),
SimpleFunctionalTextPlaceholder(
'refund_amount', ['event_or_subevent', 'refund_amount'],
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
),
SimpleFunctionalTextPlaceholder(
'pending_sum', ['event', 'pending_sum'],
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalTextPlaceholder(
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
event.currency),
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
),
SimpleFunctionalTextPlaceholder(
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
lambda event: LazyDate(now() + timedelta(days=15))
),
SimpleFunctionalTextPlaceholder(
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.open', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'hash': '98kusd8ofsj8dnkd'
}
),
),
SimpleFunctionalTextPlaceholder(
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.modify', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalTextPlaceholder(
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.change', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalTextPlaceholder(
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': order.code,
'secret': order.secret,
}
), lambda event: build_absolute_uri(
event,
'presale:event.order.cancel', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
}
),
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
event,
'presale:event.order.position',
kwargs={
'order': position.order.code,
'secret': position.web_secret,
'position': position.positionid
}
),
lambda event: build_absolute_uri(
event,
'presale:event.order.position', kwargs={
'order': 'F8VVL',
'secret': '6zzjnumtsx136ddy',
'position': '123'
}
),
),
SimpleFunctionalTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event:
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
if order.modify_deadline
else '',
lambda event: date_format(
event.settings.get(
'last_order_modification_date', as_type=RelativeDateWrapper
).datetime(event).astimezone(event.timezone),
'SHORT_DATETIME_FORMAT'
) if event.settings.get('last_order_modification_date') else '',
),
SimpleFunctionalTextPlaceholder(
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
lambda event: str(event.location or ''),
),
SimpleFunctionalTextPlaceholder(
'event_admission_time', ['event_or_subevent'],
lambda event_or_subevent:
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
if event_or_subevent.date_admission
else '',
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
),
SimpleFunctionalTextPlaceholder(
'subevent', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
),
SimpleFunctionalTextPlaceholder(
'subevent_date_from', ['waiting_list_entry', 'event'],
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
),
SimpleFunctionalTextPlaceholder(
'url_remove', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.waitinglist.remove'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.waitinglist.remove',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalTextPlaceholder(
'url', ['waiting_list_voucher', 'event'],
lambda waiting_list_voucher, event: build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + waiting_list_voucher.code,
lambda event: build_absolute_uri(
event,
'presale:event.redeem',
) + '?voucher=68CYU2H6ZTP3WLK5',
),
SimpleFunctionalTextPlaceholder(
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
_('John Doe')
),
SimpleFunctionalTextPlaceholder(
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
_('Sample Corporation')
),
SimpleFunctionalTextPlaceholder(
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
'* {} - {}'.format(
order.full_code,
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash(),
}),
)
for order in orders
), lambda event: '\n' + '\n\n'.join(
'* {} - {}'.format(
'{}-{}'.format(event.slug.upper(), order['code']),
build_absolute_uri(event, 'presale:event.order.open', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order['code'],
'secret': order['secret'],
'hash': order['hash'],
}),
)
for order in [
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
]
),
),
SimpleFunctionalTextPlaceholder(
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
event.settings.waiting_list_hours,
lambda event: event.settings.waiting_list_hours
),
SimpleFunctionalTextPlaceholder(
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
_('Sample Admission Ticket')
),
SimpleFunctionalTextPlaceholder(
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
'68CYU2H6ZTP3WLK5'
),
SimpleFunctionalTextPlaceholder(
# 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'
),
SimpleFunctionalTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
),
SimpleFunctionalTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
})
),
SimpleFunctionalTextPlaceholder(
'name', ['name'], lambda name: name,
_('John Doe')
),
SimpleFunctionalTextPlaceholder(
'comment', ['comment'], lambda comment: comment,
_('An individual text with a reason can be inserted here.'),
),
SimpleFunctionalTextPlaceholder(
'payment_info', ['order', 'payments'], _placeholder_payments,
_('The amount has been charged to your card.'),
),
SimpleFunctionalTextPlaceholder(
'payment_info', ['payment_info'], lambda payment_info: payment_info,
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
),
SimpleFunctionalTextPlaceholder(
'attendee_name', ['position'], lambda position: position.attendee_name,
_('John Doe'),
),
SimpleFunctionalTextPlaceholder(
'positionid', ['position'], lambda position: str(position.positionid),
'1'
),
SimpleFunctionalTextPlaceholder(
'name', ['position_or_address'],
get_best_name,
_('John Doe'),
),
]
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
if "concatenation_for_salutation" in name_scheme:
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
else:
concatenation_for_salutation = name_scheme["concatenation"]
ph.append(SimpleFunctionalTextPlaceholder(
"name_for_salutation", ["waiting_list_entry"],
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
_("Mr Doe"),
))
ph.append(SimpleFunctionalTextPlaceholder(
"name", ["waiting_list_entry"],
lambda waiting_list_entry: waiting_list_entry.name or "",
_("Mr Doe"),
))
ph.append(SimpleFunctionalTextPlaceholder(
"name_for_salutation", ["position_or_address"],
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
_("Mr Doe"),
))
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
ph.append(SimpleFunctionalTextPlaceholder(
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalTextPlaceholder(
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
name_scheme['sample'][f]
))
ph.append(SimpleFunctionalTextPlaceholder(
'name_%s' % f, ['position_or_address'],
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
name_scheme['sample'][f]
))
for k, v in sender.meta_data.items():
ph.append(SimpleFunctionalTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
v
))
ph.append(SimpleFunctionalTextPlaceholder(
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
v
))
return ph
class FormPlaceholderMixin:
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
)
def get_available_placeholders(event, base_parameters):
if 'order' in base_parameters:
base_parameters.append('invoice_address')
base_parameters.append('position_or_address')
params = {}
for r, val in [*register_mail_placeholders.send(sender=event), *register_text_placeholders.send(sender=event)]:
if not isinstance(val, (list, tuple)):
val = [val]
for v in val:
if all(rp in base_parameters for rp in v.required_context):
params[v.identifier] = v
return params
+6 -17
View File
@@ -90,8 +90,8 @@ class QuotaAvailability:
self._count_waitinglist = count_waitinglist
self._ignore_closed = ignore_closed
self._full_results = full_results
self._item_to_quotas = defaultdict(set)
self._var_to_quotas = defaultdict(set)
self._item_to_quotas = defaultdict(list)
self._var_to_quotas = defaultdict(list)
self._early_out = early_out
self._quota_objects = {}
self.results = {}
@@ -243,16 +243,13 @@ class QuotaAvailability:
quota_id__in=[q.pk for q in quotas]
).values('quota_id', 'item_id')
for m in q_items:
self._item_to_quotas[m['item_id']].add(self._quota_objects[m['quota_id']])
self._item_to_quotas[m['item_id']].append(self._quota_objects[m['quota_id']])
q_vars = Quota.variations.through.objects.filter(
quota_id__in=[q.pk for q in quotas]
).values('quota_id', 'itemvariation_id', 'itemvariation__item_id')
).values('quota_id', 'itemvariation_id')
for m in q_vars:
self._var_to_quotas[m['itemvariation_id']].add(self._quota_objects[m['quota_id']])
# We can't be 100% certain that a quota, when it is connected to a variation, is also always connected to
# the parent item, so we double-check here just to be sure.
self._item_to_quotas[m['itemvariation__item_id']].add(self._quota_objects[m['quota_id']])
self._var_to_quotas[m['itemvariation_id']].append(self._quota_objects[m['quota_id']])
self._compute_orders(quotas, q_items, q_vars, size_left)
@@ -381,10 +378,7 @@ class QuotaAvailability:
Q(
Q(
Q(variation_id__isnull=True) &
Q(item_id__in=(
{i['item_id'] for i in q_items if i['quota_id'] in quota_ids} |
{i['itemvariation__item_id'] for i in q_vars if i['quota_id'] in quota_ids}
))
Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids})
) | Q(
variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}
) | Q(
@@ -446,11 +440,6 @@ class QuotaAvailability:
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
quotas = [
q for q in quotas
if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now()
]
events = {q.event_id for q in quotas}
subevents = {q.subevent_id for q in quotas}
quota_ids = {q.pk for q in quotas}
+6 -3
View File
@@ -34,7 +34,7 @@ from pretix.base.models import (
)
from pretix.base.services.tasks import EventTask, ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import register_ticket_outputs
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.celery_app import app
from pretix.helpers.database import rolledback_transaction
@@ -124,8 +124,8 @@ def preview(event: int, provider: str):
def get_tickets_for_order(order, base_position=None):
positions = list(order.positions_with_tickets)
if not positions:
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
return []
if not order.ticket_download_available:
return []
@@ -135,8 +135,10 @@ def get_tickets_for_order(order, base_position=None):
for receiver, response
in register_ticket_outputs.send(order.event)
]
tickets = []
positions = list(order.positions_with_tickets)
if base_position:
# Only the given position and its children
positions = [
@@ -200,6 +202,7 @@ def get_tickets_for_order(order, base_position=None):
))
except:
logger.exception('Failed to generate ticket.')
return tickets
-3
View File
@@ -110,9 +110,6 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
continue
if wle.subevent and not wle.subevent.presale_is_running:
continue
if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now():
gone.add((wle.item, wle.variation, wle.subevent))
continue
if not wle.item.is_available():
gone.add((wle.item, wle.variation, wle.subevent))
continue
+2 -21
View File
@@ -89,7 +89,7 @@ def primary_font_kwargs():
choices = [('Open Sans', 'Open Sans')]
choices += sorted([
(a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items()
(a, {"title": a, "data": v}) for a, v in get_fonts().items() if not v.get('pdf_only', False)
], key=lambda a: a[0])
return {
'choices': choices,
@@ -1397,19 +1397,6 @@ DEFAULTS = {
widget=forms.NumberInput(),
)
},
'waiting_list_auto_disable': {
'default': None,
'type': RelativeDateWrapper,
'form_class': RelativeDateTimeField,
'serializer_class': SerializerRelativeDateTimeField,
'form_kwargs': dict(
label=_("Disable waiting list"),
help_text=_("The waiting list will be fully disabled after this date. This means that nobody can add "
"themselves to the waiting list any more, but also that tickets will be available for sale "
"again if quota permits, even if there are still people on the waiting list. Vouchers that "
"have already been sent remain active."),
)
},
'waiting_list_names_asked': {
'default': 'False',
'type': bool,
@@ -3242,12 +3229,6 @@ Your {organizer} team""")) # noqa: W291
label=_('Length of gift card codes'),
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
min_value=6,
max_value=64,
),
'serializer_kwargs': dict(
min_value=6,
max_value=64,
)
},
'giftcard_expiry_years': {
@@ -3676,7 +3657,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
'BR': (['State'], 'short'),
'CA': (['Province', 'Territory'], 'short'),
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'MY': (['State', 'Federal territory'], 'long'),
'MY': (['State'], 'long'),
'MX': (['State', 'Federal district'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
}
+5 -11
View File
@@ -220,18 +220,12 @@ subclass of pretix.base.payment.BasePaymentProvider or a list of these
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_text_placeholders = EventPluginSignal()
"""
This signal is sent out to get all known text placeholders. Receivers should return
an instance of a subclass of pretix.base.services.placeholders.BaseTextPlaceholder or a
list of these.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_mail_placeholders = EventPluginSignal()
"""
**DEPRECATED**: This signal has a new name, please use ``register_text_placeholders`` instead.
This signal is sent out to get all known email text placeholders. Receivers should return
an instance of a subclass of pretix.base.email.BaseMailTextPlaceholder or a list of these.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_html_mail_renderers = EventPluginSignal()
@@ -652,7 +646,7 @@ allow_ticket_download = EventPluginSignal()
Arguments: ``order``
This signal is sent out to check if tickets for an order can be downloaded. If any receiver returns false,
a download will not be offered. If a receiver returns a list of OrderPositions, only those will be downloadable.
a download will not be offered.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -0,0 +1,6 @@
{# this is the version from django 3.x, prior to https://github.com/django/django/commit/5942ab5eb165ee2e759174e297148a40dd855920 so that django-bootstrap3 can keep doing its magic #}
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}"{% endif %}>{% endif %}{% for option in options %}
<li>{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
</ul></li>{% endif %}{% endfor %}
</ul>{% endwith %}
+1 -4
View File
@@ -96,9 +96,6 @@ class BaseTicketOutput:
"""
raise NotImplementedError()
def get_tickets_to_print(self, order):
return order.positions_with_tickets
def generate_order(self, order: Order) -> Tuple[str, str, str]:
"""
This method is the same as order() but should not generate one file per order position
@@ -119,7 +116,7 @@ class BaseTicketOutput:
"""
with tempfile.TemporaryDirectory() as d:
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
for pos in self.get_tickets_to_print(order):
for pos in order.positions_with_tickets:
fname, __, content = self.generate(pos)
zipf.writestr('{}-{}{}'.format(
order.code, pos.positionid, os.path.splitext(fname)[1]
+2 -4
View File
@@ -24,7 +24,6 @@ from datetime import datetime, time, timedelta
from django.db.models import Q
from django.urls import reverse
from django.utils.text import format_lazy
from django.utils.timezone import make_aware
from django.utils.translation import pgettext_lazy
@@ -90,10 +89,9 @@ def timeline_for_event(event, subevent=None):
datetime=(
ev.presale_end or ev.date_to or ev.date_from.astimezone(ev.timezone).replace(hour=23, minute=59, second=59)
),
description=format_lazy(
'{} ({})',
description='{}{}'.format(
pgettext_lazy('timeline', 'End of ticket sales'),
pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured') if not ev.presale_end else ""
f" ({pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured')})" if not ev.presale_end else ""
),
edit_url=ev_edit_url
))
-2
View File
@@ -72,7 +72,6 @@ class EventSlugBanlistValidator(BanlistValidator):
'widget',
'customer',
'account',
'lead',
]
@@ -93,7 +92,6 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
'api',
'csp_report',
'widget',
'lead',
]
+5 -28
View File
@@ -123,6 +123,8 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
@property
def name(self):
if hasattr(self.file, 'display_name'):
return self.file.display_name
return self.file.name
@property
@@ -217,17 +219,15 @@ class ExtValidationMixin:
def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs)
from ...base.models import CachedFile
if isinstance(data, (UploadedFile, CachedFile)):
filename = data.name if isinstance(data, UploadedFile) else data.filename
if isinstance(data, UploadedFile):
filename = data.name
ext = os.path.splitext(filename)[1]
ext = ext.lower()
if ext not in self.ext_whitelist:
raise forms.ValidationError(_("Filetype not allowed!"))
if ext in IMAGE_EXTS:
validate_uploaded_file_for_valid_image(data if isinstance(data, UploadedFile) else data.file)
validate_uploaded_file_for_valid_image(data)
return data
@@ -257,12 +257,6 @@ class CachedFileField(ExtFileField):
if isinstance(data, File):
if hasattr(data, '_uploaded_to'):
return data._uploaded_to
try:
self.clean(data)
except ValidationError:
return None
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
@@ -274,9 +268,6 @@ class CachedFileField(ExtFileField):
cf.save()
data._uploaded_to = cf
return cf
if isinstance(data, CachedFile):
return data
return super().bound_data(data, initial)
def clean(self, *args, **kwargs):
@@ -424,17 +415,3 @@ class FontSelect(forms.RadioSelect):
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
def label_from_instance(self, obj):
return str(obj) if obj.active else mark_safe(f'<strike class="text-muted">{escape(obj)}</strike>')
class ButtonGroupRadioSelect(forms.RadioSelect):
template_name = 'pretixcontrol/button_group_radio.html'
option_template_name = 'pretixcontrol/button_group_radio_option.html'
def __init__(self, *args, **kwargs):
self.option_icons = kwargs.pop('option_icons')
super().__init__(*args, **kwargs)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
attrs['icon'] = self.option_icons[value]
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
return opt
+15 -18
View File
@@ -60,11 +60,12 @@ from i18nfield.forms import (
from pytz import common_timezones
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
@@ -79,7 +80,6 @@ from pretix.helpers.countries import CachedCountries
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.presale.style import get_fonts
class EventWizardFoundationForm(forms.Form):
@@ -503,7 +503,7 @@ class EventSettingsValidationMixin:
del self.cleaned_data[field]
class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, SettingsForm):
class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Event timezone"),
@@ -539,7 +539,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'region',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_auto_disable',
'waiting_list_hours',
'waiting_list_auto',
'waiting_list_names_asked',
@@ -594,10 +593,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'og_image',
]
base_context = {
'frontpage_text': ['event'],
}
def _resolve_virtual_keys_input(self, data, prefix=''):
# set all dependants of virtual_keys and
# delete all virtual_fields to prevent them from being saved
@@ -653,9 +648,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_available_only']
del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices += [
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
]
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
self.virtual_keys = []
@@ -690,9 +682,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
else:
self.initial[virtual_key] = 'do_not_ask'
for k, v in self.base_context.items():
self._set_field_placeholders(k, v)
@cached_property
def changed_data(self):
data = []
@@ -936,9 +925,6 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
)
)
self.fields['invoice_generate'].choices = generate_choices
self.fields['invoice_renderer_font'].choices += [
(a, a) for a in get_fonts(event, pdf_support_required=True).keys()
]
def contains_web_channel_validate(val):
@@ -946,7 +932,7 @@ def contains_web_channel_validate(val):
raise ValidationError(_("The online shop must be selected to receive these emails."))
class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
class MailSettingsForm(SettingsForm):
auto_fields = [
'mail_prefix',
'mail_from_name',
@@ -1358,6 +1344,17 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
'mail_attach_ical_description': ['event', 'event_or_subevent'],
}
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
)
def __init__(self, *args, **kwargs):
self.event = event = kwargs.get('obj')
super().__init__(*args, **kwargs)
+5 -2
View File
@@ -309,8 +309,11 @@ class OrderFilterForm(FilterForm):
elif s in ('p', 'n', 'e', 'c', 'r'):
qs = qs.filter(status=s)
elif s == 'overpaid':
qs = Order.annotate_overpayments(qs, refunds=False, results=True, sums=False)
qs = qs.filter(is_overpaid=True)
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
qs = qs.filter(
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
)
elif s == 'rc':
qs = qs.filter(
cancellation_requests__isnull=False
+10 -79
View File
@@ -64,10 +64,10 @@ from pretix.base.models import (
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
from pretix.control.forms import (
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
SplitDateTimeField, SplitDateTimePickerWidget,
ItemMultipleChoiceField, SizeValidationMixin, SplitDateTimeField,
SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
from pretix.control.forms.widgets import Select2
from pretix.helpers.models import modelcopy
from pretix.helpers.money import change_decimal_field
@@ -207,20 +207,14 @@ class QuestionOptionForm(I18nModelForm):
class QuotaForm(I18nModelForm):
itemvars = forms.MultipleChoiceField(
label=_("Products"),
required=True,
)
def __init__(self, **kwargs):
self.instance = kwargs.get('instance', None)
self.event = kwargs.get('event')
items = kwargs.pop('items', None) or self.event.items.prefetch_related('variations')
searchable_selection = kwargs.pop('searchable_selection', None)
self.original_instance = modelcopy(self.instance) if self.instance else None
initial = kwargs.get('initial', {})
if self.instance and self.instance.pk and 'itemvars' not in initial:
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all() if (len(i.variations.all()) == 0)] + [
initial['itemvars'] = [str(i.pk) for i in self.instance.items.all()] + [
'{}-{}'.format(v.item_id, v.pk) for v in self.instance.variations.all()
]
kwargs['initial'] = initial
@@ -237,22 +231,12 @@ class QuotaForm(I18nModelForm):
else:
choices.append(('{}'.format(item.pk), str(item) if item.active else mark_safe(f'<strike class="text-muted">{escape(item)}</strike>')))
if searchable_selection:
self.fields['itemvars'].widget = Select2ItemVarMulti(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.items.itemvars.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': _('No products')
},
choices=choices,
)
else:
self.fields['itemvars'].widget = forms.CheckboxSelectMultiple()
self.fields['itemvars'].choices = choices
self.fields['itemvars'] = forms.MultipleChoiceField(
label=_('Products'),
required=True,
choices=choices,
widget=forms.CheckboxSelectMultiple
)
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
@@ -396,9 +380,7 @@ class ItemCreateForm(I18nModelForm):
'description',
'active',
'available_from',
'available_from_mode',
'available_until',
'available_until_mode',
'require_voucher',
'hide_without_voucher',
'allow_cancel',
@@ -580,34 +562,6 @@ class ItemUpdateForm(I18nModelForm):
)
change_decimal_field(self.fields['default_price'], self.event.currency)
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect(
choices=(
(True, _("Hide product if unavailable")),
(False, _("Show product with info on why its unavailable")),
),
option_icons={
True: 'eye-slash',
False: 'info'
},
attrs={'data-checkbox-dependency': '#id_require_voucher'}
)
if self.instance.hidden_if_available_id:
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
self.fields['hidden_if_available'].help_text = format_html(
@@ -702,9 +656,6 @@ class ItemUpdateForm(I18nModelForm):
)
)
if not d.get('require_voucher'):
d['hide_without_voucher'] = False
if d.get('require_membership') and not d.get('require_membership_types'):
self.add_error(
'require_membership_types',
@@ -762,9 +713,7 @@ class ItemUpdateForm(I18nModelForm):
'free_price_suggestion',
'tax_rule',
'available_from',
'available_from_mode',
'available_until',
'available_until_mode',
'require_voucher',
'require_approval',
'hide_without_voucher',
@@ -901,22 +850,6 @@ class ItemVariationForm(I18nModelForm):
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.meta_fields = []
meta_defaults = {}
if self.instance.pk:
@@ -959,9 +892,7 @@ class ItemVariationForm(I18nModelForm):
'checkin_attention',
'checkin_text',
'available_from',
'available_from_mode',
'available_until',
'available_until_mode',
'sales_channels',
'hide_without_voucher',
]
+18 -6
View File
@@ -61,7 +61,6 @@ from pretix.base.models import (
TaxRule,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.services.pricing import get_price
from pretix.control.forms import SplitDateTimeField
from pretix.control.forms.widgets import Select2
@@ -152,6 +151,10 @@ class ForceQuotaConfirmationForm(forms.Form):
del self.fields['force']
class ConfirmPaymentForm(ForceQuotaConfirmationForm):
pass
class ReactivateOrderForm(ForceQuotaConfirmationForm):
pass
@@ -217,11 +220,10 @@ class DenyForm(forms.Form):
)
class MarkPaidForm(ForceQuotaConfirmationForm):
class MarkPaidForm(ConfirmPaymentForm):
send_email = forms.BooleanField(
required=False,
label=_('Notify customer by email'),
help_text=_('A mail will only be sent if the order is fully paid after this.'),
initial=True
)
amount = forms.DecimalField(
@@ -238,10 +240,9 @@ class MarkPaidForm(ForceQuotaConfirmationForm):
)
def __init__(self, *args, **kwargs):
payment = kwargs.pop('payment', None)
super().__init__(*args, **kwargs)
change_decimal_field(self.fields['amount'], self.instance.event.currency)
self.fields['amount'].initial = max(Decimal('0.00'), payment.amount if payment else self.instance.pending_sum)
self.fields['amount'].initial = max(Decimal('0.00'), self.instance.pending_sum)
class ExporterForm(forms.Form):
@@ -768,7 +769,7 @@ class OrderRefundForm(forms.Form):
return data
class EventCancelForm(FormPlaceholderMixin, forms.Form):
class EventCancelForm(forms.Form):
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'Date'),
@@ -868,6 +869,17 @@ class EventCancelForm(FormPlaceholderMixin, forms.Form):
send_waitinglist_subject = forms.CharField()
send_waitinglist_message = forms.CharField()
def _set_field_placeholders(self, fn, base_parameters):
placeholders = get_available_placeholders(self.event, base_parameters)
ht = format_placeholders_help_text(placeholders, self.event)
if self.fields[fn].help_text:
self.fields[fn].help_text += ' ' + str(ht)
else:
self.fields[fn].help_text = ht
self.fields[fn].validators.append(
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
kwargs.setdefault('initial', {})
+2 -17
View File
@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
from bootstrap3.text import text_value
from django.forms import CheckboxInput, CheckboxSelectMultiple, RadioSelect
from django.forms.utils import flatatt
@@ -28,6 +27,8 @@ from django.utils.safestring import mark_safe
from django.utils.translation import pgettext
from i18nfield.forms import I18nFormField
from pretix.base.forms.renderers import FieldRenderer, InlineFieldRenderer
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False):
"""
@@ -99,22 +100,6 @@ class ControlFieldRenderer(FieldRenderer):
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
def __init__(self, *args, **kwargs):
kwargs['layout'] = 'horizontal'
kwargs['horizontal_field_class'] = 'col-md-7'
self.visibility_field = kwargs['visibility_field']
super().__init__(*args, **kwargs)
def render_visibility_field(self):
return self.visibility_field.as_widget(attrs=self.visibility_field.field.widget.attrs)
def wrap_field(self, html):
html = super().wrap_field(html)
html += '<div class="col-md-2 text-right">' + self.render_visibility_field() + '</div>'
return html
class BulkEditMixin:
def __init__(self, *args, **kwargs):
+1 -6
View File
@@ -260,8 +260,6 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
super().__init__(*args, **kwargs)
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
self.fields['price'].label = str(self.item)
self.available_from_mode = self.item.available_from_mode
self.available_until_mode = self.item.available_until_mode
class Meta:
model = SubEventItem
@@ -289,8 +287,6 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
super().__init__(*args, **kwargs)
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
self.fields['price'].label = '{} {}'.format(str(self.item), self.variation.value)
self.available_from_mode = self.variation.available_from_mode
self.available_until_mode = self.variation.available_until_mode
class Meta:
model = SubEventItemVariation
@@ -360,7 +356,6 @@ class BulkSubEventItemVariationForm(SubEventItemVariationForm):
class QuotaFormSet(I18nInlineFormSet):
def __init__(self, *args, **kwargs):
self.searchable_selection = kwargs.pop('searchable_selection', None)
self.event = kwargs.pop('event', None)
self.locales = self.event.settings.get('locales')
super().__init__(*args, **kwargs)
@@ -373,7 +368,7 @@ class QuotaFormSet(I18nInlineFormSet):
kwargs['locales'] = self.locales
kwargs['event'] = self.event
kwargs['items'] = self.items
kwargs['searchable_selection'] = self.searchable_selection
kwargs['items'] = self.items
return super()._construct_form(i, **kwargs)
@property
+1 -2
View File
@@ -63,8 +63,7 @@ class VoucherForm(I18nModelForm):
itemvar = FakeChoiceField(
label=_("Product"),
help_text=_(
"This product is added to the user's cart if the voucher is redeemed. Instead of a specific product, you "
"can also select a quota. In this case, all products assigned to this quota can be selected."
"This product is added to the user's cart if the voucher is redeemed."
),
required=True
)
-16
View File
@@ -77,19 +77,3 @@ class Select2ItemVarQuotaMixin(Select2Mixin):
class Select2ItemVarQuota(Select2ItemVarQuotaMixin, forms.Select):
pass
class Select2ItemVarMulti(Select2Mixin, forms.SelectMultiple):
def options(self, name, value, attrs=None):
# we need this for multi-selection without a queryset for the selection of items and variations
for i, v in enumerate(value):
yield self.create_option(
None,
v,
dict(self.choices)[v],
True,
i,
subindex=None,
attrs=attrs
)
return
@@ -1,6 +0,0 @@
{% with id=widget.attrs.id %}<div data-toggle="buttons"{% if id %} id="{{ id }}"{% endif %} class="btn-group btn-group-toggle {{ widget.attrs.class }}">{% for group, options, index in widget.optgroups %}
{% for option in options %}
{% include option.template_name with widget=option %}
{% endfor %}
{% endfor %}
</div>{% endwith %}
@@ -1,2 +0,0 @@
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} class="btn btn-primary-if-active form-field-boundary {% if widget.attrs.checked %} active{% endif %}" title="{{ widget.label }}" data-toggle="tooltip">
{% include "django/forms/widgets/input.html" %} <span class="fa fa-{{ widget.attrs.icon }}"></span></label>
@@ -0,0 +1,393 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load eventurl %}
{% load bootstrap3 %}
{% load static %}
{% load escapejson %}
{% load eventsignal %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<div class="row">
<div id="header-area" class="col-lg-8">
<h1>
{{ request.event.name }}
<small>
{% if request.event.has_subevents %}
{% trans "Event series" %}
{% else %}
{{ request.event.get_date_range_display }}
{% endif %}
</small>
</h1>
<div class="helper-space-below">
{% trans "Shop URL:" %}
<span id="shop_url" class="text-muted">{% abseventurl request.event "presale:event.index" %}</span>
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
</button>
<div class="btn-group helper-display-inline-block">
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-qrcode" aria-hidden="true"></i>
</button>
{% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=0 %}
</div>
<br>
<span id="shop_state" class="shop_state">
{{ shop_state.content|safe }}
<a href="{{ shop_state.url }}">
<button type="button" class="btn btn-default btn-xs btn-wrench js-only" href="{{ shop_state.url }}">
<i class="fa fa-wrench" aria-hidden="true"></i>
<span class="sr-only">{% trans "Change shop state" %}</span>
</button>
</a>
</span>
<div class="clearfix"></div>
</div>
</div>
<!-- Comments ----------------------------------------------------------------------------------------------------->
<div class="event-comment col-lg-4">
{% trans "Internal comment" %}
<form class="form" method="post"
action="{% url "control:event.comment" event=request.event.slug organizer=request.event.organizer.slug %}">
{% csrf_token %}
<div class="row">
{% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
</div>
{% if not comment_form.readonly %}
<p class="text-right flip">
<button class="btn btn-default">
{% trans "Update comment" %}
</button>
</p>
{% endif %}
</form>
</div>
</div>
<!-- Comments ----------------------------------------------------------------------------------------------------->
<!-- Warnings ----------------------------------------------------------------------------------------------------->
<div class="row-form-errors">
{% if has_overpaid_orders %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>overpaid orders</strong>, for example due to duplicate payment attempts.
You should review the cases and consider refunding the overpaid amount to the user.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=overpaid"
class="btn btn-primary">{% trans "Show overpaid orders" %}</a>
</div>
{% endif %}
{% if has_pending_refunds %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>pending refunds</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders.refunds" event=request.event.slug organizer=request.event.organizer.slug %}"
class="btn btn-primary">{% trans "Show pending refunds" %}</a>
</div>
{% endif %}
{% if has_cancellation_requests %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>requested cancellations</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=rc"
class="btn btn-primary">{% trans "Show orders requesting cancellation" %}</a>
</div>
{% endif %}
{% if has_pending_approvals %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>pending approvals</strong> that you should take care of.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pa"
class="btn btn-primary">{% trans "Show orders pending approval" %}</a>
</div>
{% endif %}
{% if has_pending_orders_with_full_payment %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This event contains <strong>fully paid orders</strong> that are not marked as paid, probably
because no quota was left at the time their payment arrived. You should review the cases and consider
either refunding the customer or creating more space.
{% endblocktrans %}
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status=pendingpaid"
class="btn btn-primary">{% trans "Show affected orders" %}</a>
</div>
{% endif %}
</div>
<!-- Warnings ----------------------------------------------------------------------------------------------------->
{% eventsignal request.event "pretix.control.signals.event_dashboard_top" request=request %}
<!-- Timeline ----------------------------------------------------------------------------------------------------->
{% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</form>
{% endif %}
{% if not request.event.has_subevents or subevent %}
{% include "pretixcontrol/event/fragment_timeline.html" %}
{% endif %}
<!-- Timeline ----------------------------------------------------------------------------------------------------->
<!-- Big numbers ---------------------------------------------------------------------------------------------->
<div class="dashboard">
<div class="widget-container widget-small event-dashboard">
<a href="{{ attendees_paid_ordered.url }}" class="widget">
{{ attendees_paid_ordered.content|safe }}
</a>
</div>
<div class="widget-container widget-small event-dashboard">
<a href="{{ total_revenue.url }}" class="widget">
{{ total_revenue.content|safe }}
</a>
</div>
</div>
<!-- Big numbers -------------------------------------------------------------------------------------------------->
<!-- Diagram ------------------------------------------------------------------------------------------------------>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Revenue over time" %}</h3>
</div>
<div class="panel-body">
<div id="rev_chart" class="chart"></div>
{% if request.GET.subevent %}
<div class="alert alert-info">
{% blocktrans trimmed context "subevent" %}
If you select a single date, payment method fees will not be listed here as it might not be clear which
date they belong to.
{% endblocktrans %}
</div>
{% endif %}
<p class="help-block">
<small>
{% blocktrans trimmed %}
Only fully paid orders are counted.
Orders paid in multiple payments are shown with the date of their last payment.
{% endblocktrans %}
</small>
</p>
</div>
</div>
<!-- Diagram ------------------------------------------------------------------------------------------------------>
<!-- Check-Ins ---------------------------------------------------------------------------------------------------->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Check-In" %}</h3>
</div>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>
{% trans "Name" %}
</th>
<th>{% trans "Checked in" %}</th>
{% if request.event.has_subevents %}
<th>
{% trans "Date" context "subevent" %}
</th>
{% endif %}
<th class="iconcol">{% trans "Automated check-in" %}</th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for cl in checkinlists %}
<tr>
<td>
<strong><a
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
</td>
<td>
<div class="quotabox availability">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
</div>
</div>
<div class="numbers">
{{ cl.checkin_count|default_if_none:"0" }} /
{{ cl.position_count|default_if_none:"0" }}
</div>
</div>
</td>
{% if request.event.has_subevents %}
{% if cl.subevent %}
<td>
{{ cl.subevent.name }} {{ cl.subevent.get_date_range_display }}
{{ cl.subevent.date_from|date:"TIME_FORMAT" }}
</td>
{% else %}
<td>
<em>{% trans "All" %}</em>
</td>
{% endif %}
{% endif %}
<td>
{% for channel in cl.auto_checkin_sales_channels %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
{% endfor %}
</td>
<td>
{% if cl.all_products %}
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for item in cl.limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Check-Ins ---------------------------------------------------------------------------------------------------->
<!-- Quotas ------------------------------------------------------------------------------------------------------->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Quotas" %}</h3>
</div>
<div class="table-responsive table-quotas">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>
{% trans "Name" %}
</th>
<th>
{% trans "Begin" %}
<a href="? url_replace request 'filter-ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
<a href="? url_replace request 'filter-ordering' 'date_from' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>
{% trans "Paid tickets per quota" %}
</th>
<th>
{% trans "Capacity left" %}
</th>
</tr>
</thead>
<tbody>
{% if request.event.has_subevents and not subevent %}
{% for s in subevents %}
<tr>
<td>
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}">
{{ s.name }}</a></strong><br>
<small class="text-muted">
#{{ s.pk }}
</small>
</td>
<td>
{{ s.get_date_from_display }}<br>
<span class="text-muted">
{{ s.date_from|date:"l" }}
</span>
</td>
<td>
{% for q in s.first_quotas|slice:":3" %}
{% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %}
{% endfor %}
{% if s.first_quotas|length > 3 %}
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}"
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
data-placement="top">
&middot;&middot;&middot;
</a>
{% endif %}
</td>
<td>
{% for q in s.first_quotas %}
{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}
{% endfor %}
</td>
</tr>
{% endfor %}
{% endif %}
{% if subevent %}
<tr>
<td>
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=subevent.id %}?returnto={{ request.GET.urlencode|urlencode }}">
{{ subevent.name }}</a></strong><br>
<small class="text-muted">
#{{ subevent.pk }}
</small>
</td>
<td>
{{ subevent.get_date_from_display }}<br>
<span class="text-muted">
{{ subevent.date_from|date:"l" }}
</span>
</td>
<td>
{% for q in subevent.first_quotas|slice:":3" %}
{% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %}
{% endfor %}
{% if subevent.first_quotas|length > 3 %}
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ subevent.id }}"
class="quotabox-more" data-toggle="tooltip" title="{% trans "More quotas" %}"
data-placement="top">
&middot;&middot;&middot;
</a>
{% endif %}
</td>
<td>
{% for q in subevent.first_quotas %}
{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}
{% endfor %}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Quotas ------------------------------------------------------------------------------------------------------->
<!-- Logs --------------------------------------------------------------------------------------------------------->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Event logs" %}
</h3>
</div>
<ul class="list-group" id="logs_target">
<div class="logs-lazy-loading">
<span class="fa fa-cog fa-4x"></span>
</div>
</ul>
<div class="panel-footer">
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
{% trans "Show more logs" %}
</a>
</div>
</div>
<!-- Logs --------------------------------------------------------------------------------------------------------->
<script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
<script type="application/text" id="currency">{{ request.event.currency }}</script>
{% endblock %}
@@ -7,7 +7,7 @@
{% block title %}{% trans "General settings" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
{% endblock %}
{% block inside %}
<h1>{% trans "General settings" %}</h1>
@@ -361,7 +361,6 @@
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
{% bootstrap_field sform.waiting_list_auto layout="control" %}
{% bootstrap_field sform.waiting_list_hours layout="control" %}
{% bootstrap_field sform.waiting_list_auto_disable layout="control" %}
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
@@ -95,8 +95,8 @@
</div>
</div>
{% endif %}
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
{% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.sales_channels layout="control" %}
{% bootstrap_field form.hide_without_voucher layout="control" %}
{% bootstrap_field form.require_approval layout="control" %}
@@ -194,8 +194,7 @@
</div>
</div>
{% endif %}
{% bootstrap_field formset.empty_form.available_from visibility_field=formset.empty_form.available_from_mode layout="control_with_visibility" %}
{% bootstrap_field formset.empty_form.available_until visibility_field=formset.empty_form.available_until_mode layout="control_with_visibility" %}
{% bootstrap_field formset.empty_form.available_from layout="control" %}
{% bootstrap_field formset.empty_form.available_until layout="control" %}
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}
@@ -152,28 +152,27 @@
</fieldset>
<fieldset>
<legend>{% trans "Availability" %}</legend>
{% bootstrap_field form.sales_channels layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
{% bootstrap_field form.max_per_order layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.min_per_order layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.require_voucher visibility_field=form.hide_without_voucher layout="control_with_visibility" %}
{% bootstrap_field form.require_bundling layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.sales_channels layout="control" %}
{% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.max_per_order layout="control" %}
{% bootstrap_field form.min_per_order layout="control" %}
{% bootstrap_field form.require_voucher layout="control" %}
{% bootstrap_field form.hide_without_voucher layout="control" %}
{% bootstrap_field form.require_bundling layout="control" %}
{% if form.require_membership %}
{% bootstrap_field form.require_membership layout="control" %}
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
{% bootstrap_field form.require_membership_types layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.require_membership_hidden layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.require_membership_types layout="control" %}
{% bootstrap_field form.require_membership_hidden layout="control" %}
</div>
{% endif %}
{% bootstrap_field form.allow_cancel layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.allow_waitinglist layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.allow_cancel layout="control" %}
{% bootstrap_field form.allow_waitinglist layout="control" %}
{% if form.hidden_if_available %}
{% bootstrap_field form.hidden_if_available layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.hidden_if_available layout="control" %}
{% endif %}
{% bootstrap_field form.hidden_if_item_available layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.hidden_if_item_available layout="control" %}
</fieldset>
{% for v in formsets.values %}
<fieldset>
@@ -239,7 +238,7 @@
</div>
</div>
</fieldset>
<fieldset id="tab-item-additional-settings">
<fieldset>
<legend>{% trans "Additional settings" %}</legend>
{% bootstrap_field form.issue_giftcard layout="control" %}
{% if form.grant_membership_type %}
@@ -267,7 +266,6 @@
{% bootstrap_field form.show_quota_left layout="control" %}
{% for f in plugin_forms %}
{% if not f.is_layouts and not f.title %}
<hr />
{% if f.template and not "template" in f.fields %}
{% include f.template with form=f %}
{% else %}
@@ -70,10 +70,6 @@
{% endblocktrans %}
</em>
{% endif %}
{% if position.attendee_name %}
<span class="fa fa-user" aria-hidden="true"></span>
{{ position.attendee_name }}
{% endif %}
</h3>
</div>
<div class="panel-body">
@@ -124,13 +124,6 @@
</button>
</div>
</form>
{% elif order.status == "e" and order.payment_refund_sum != 0 %}
<div class="alert alert-warning">
{% blocktrans trimmed with amount=order.payment_refund_sum|money:request.event.currency %}
This order is expired even though it received payments of {{ amount }}. You can choose to refund
the money below or reactivate it by extending the payment deadline.
{% endblocktrans %}
</div>
{% endif %}
<div class="row">
@@ -21,13 +21,7 @@
Do you really want to mark this payment as complete?
{% endblocktrans %}</p>
<input type="hidden" name="status" value="p" />
{% bootstrap_form_errors form %}
{% bootstrap_field form.amount layout='horizontal' %}
{% bootstrap_field form.payment_date layout='horizontal' %}
{% bootstrap_field form.send_email layout='horizontal' %}
{% if form.force %}
{% bootstrap_field form.force layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
{% endif %}
{% bootstrap_form form layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
<div class="form-group submit-group">
<a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
@@ -157,7 +157,7 @@
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
<fieldset id="tab-organizer-privacy">
<fieldset>
<legend>{% trans "Privacy" %}</legend>
{% bootstrap_field sform.privacy_url layout="control" %}
<div class="alert alert-legal">
@@ -26,11 +26,7 @@
{% for u in team.members.all %}
<tr>
<td>
{% if request.user.is_staff and staff_session %}
<strong><a href="{% url "control:users.edit" id=u.pk %}">{{ u.email }}</a></strong>
{% else %}
{{ u.email }}
{% endif %}
{{ u.email }}
{% if u.require_2fa %}
<span class="fa fa-shield text-success" data-toggle="tooltip"
title="{% trans "Two-factor authentication enabled" %}">
@@ -8,7 +8,7 @@
{% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
{% endcompress %}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" %}">
{% endblock %}
{% block content %}
<h1>
@@ -183,7 +183,6 @@
</div>
{% endif %}
<div class="row control-group pdf-info">
<hr/>
<div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" id="pdf-info-width" class="input-block-level form-control">
@@ -225,7 +224,6 @@
</div>
</div>
<div class="row control-group pdf-info">
<hr/>
<div class="col-sm-12">
<label>{% trans "Preferred language" %}</label><br>
<select class="form-control" id="pdf-info-locale">
@@ -236,62 +234,7 @@
</select>
</div>
</div>
<div class="row control-group poweredby">
<div class="col-sm-12">
<label>{% trans "Style" %}</label><br>
<select class="input-block-level form-control" id="toolbox-poweredby-style">
<option value="dark">{% trans "Dark" %}</option>
<option value="white">{% trans "Light" %}</option>
</select>
</div>
</div>
<div class="row control-group imagecontent">
<div class="col-sm-12">
<label>{% trans "Image content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-imagecontent">
<option value="">{% trans "Empty" %}</option>
{% for varname, var in images.items %}
<option value="{{ varname }}">{{ var.label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row control-group text textcontent">
<div class="col-sm-12">
<label>{% trans "Content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-content">
{% for varname, var in variables.items %}
{% if not var.hidden %}
<option data-sample="{{ var.editor_sample }}" {% if var.migrate_from %}data-old-value="{{ var.migrate_from }}"{% endif %} value="{{ varname }}">{{ var.label }}</option>
{% endif %}
{% endfor %}
{% for p in request.organizer.meta_properties.all %}
<option value="meta:{{ p.name }}">
{% trans "Event attribute:" %} {{ p.name }}
</option>
{% endfor %}
{% for p in request.event.item_meta_properties.all %}
<option value="itemmeta:{{ p.name }}">
{% trans "Item attribute:" %} {{ p.name }}
</option>
{% endfor %}
<option value="other_i18n">{% trans "Other… (multilingual)" %}</option>
<option value="other">{% trans "Other…" %}</option>
</select>
<textarea type="text" value="" class="input-block-level form-control"
id="toolbox-content-other"></textarea>
<div class="i18n-form-group" id="toolbox-content-other-i18n">
{% for l in request.event.settings.locales %}
<textarea id="toolbox-content-other-{{ l }}" rows="3" class="input-block-level form-control" title="{{ l }}" lang="{{ l }}"></textarea>
{% endfor %}
</div>
<p class="help-block" id="toolbox-content-other-help">
<a href="?placeholders=true" target="_blank">{% trans "Show available placeholders" %}</a>
</p>
</div>
</div>
<div class="row control-group position">
<hr/>
<div class="col-sm-6">
<label>{% trans "x (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
@@ -334,30 +277,6 @@
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-textwidth">
</div>
<div class="col-sm-6">
<label>{% trans "Rotation (°)" %}</label><br>
<input type="number" value="0" class="input-block-level form-control" step="0.1"
id="toolbox-textrotation">
</div>
</div>
<div class="row control-group text">
<hr/>
<div class="col-sm-12">
<label>{% trans "Font" %}</label><br>
<select class="input-block-level form-control" id="toolbox-fontfamily">
<option>Open Sans</option>
{% for family in fonts.keys %}
<option>{{ family }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Font size (pt)" %}</label><br>
@@ -416,6 +335,83 @@
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-12">
<label>{% trans "Font" %}</label><br>
<select class="input-block-level form-control" id="toolbox-fontfamily">
<option>Open Sans</option>
{% for family in fonts.keys %}
<option>{{ family }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-textwidth">
</div>
<div class="col-sm-6">
<label>{% trans "Rotation (°)" %}</label><br>
<input type="number" value="0" class="input-block-level form-control" step="0.1"
id="toolbox-textrotation">
</div>
</div>
<div class="row control-group poweredby">
<div class="col-sm-12">
<label>{% trans "Style" %}</label><br>
<select class="input-block-level form-control" id="toolbox-poweredby-style">
<option value="dark">{% trans "Dark" %}</option>
<option value="white">{% trans "Light" %}</option>
</select>
</div>
</div>
<div class="row control-group imagecontent">
<div class="col-sm-12">
<label>{% trans "Image content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-imagecontent">
<option value="">{% trans "Empty" %}</option>
{% for varname, var in images.items %}
<option value="{{ varname }}">{{ var.label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row control-group text textcontent">
<div class="col-sm-12">
<label>{% trans "Content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-content">
{% for varname, var in variables.items %}
{% if not var.hidden %}
<option data-sample="{{ var.editor_sample }}" {% if var.migrate_from %}data-old-value="{{ var.migrate_from }}"{% endif %} value="{{ varname }}">{{ var.label }}</option>
{% endif %}
{% endfor %}
{% for p in request.organizer.meta_properties.all %}
<option value="meta:{{ p.name }}">
{% trans "Event attribute:" %} {{ p.name }}
</option>
{% endfor %}
{% for p in request.event.item_meta_properties.all %}
<option value="itemmeta:{{ p.name }}">
{% trans "Item attribute:" %} {{ p.name }}
</option>
{% endfor %}
<option value="other_i18n">{% trans "Other… (multilingual)" %}</option>
<option value="other">{% trans "Other…" %}</option>
</select>
<textarea type="text" value="" class="input-block-level form-control"
id="toolbox-content-other"></textarea>
<div class="i18n-form-group" id="toolbox-content-other-i18n">
{% for l in request.event.settings.locales %}
<textarea id="toolbox-content-other-{{ l }}" rows="3" class="input-block-level form-control" title="{{ l }}" lang="{{ l }}"></textarea>
{% endfor %}
</div>
<p class="help-block" id="toolbox-content-other-help">
<a href="?placeholders=true" target="_blank">{% trans "Show available placeholders" %}</a>
</p>
</div>
</div>
</div>
</div>
<div class="editor-toolbox-text panel panel-default">
@@ -1,5 +1,4 @@
{% load static %}
@font-face {
font-family: 'AND';
font-style: normal;
@@ -15,7 +14,7 @@
{% for family, styles in fonts.items %}
{% for style, formats in styles.items %}
{% if "sample" not in style and "pdf_only" not in style %}
{% if "sample" not in style %}
@font-face {
font-family: '{{ family }}';
{% if style == "italic" or style == "bolditalic" %}
@@ -28,9 +27,9 @@
{% else %}
font-weight: normal;
{% endif %}
src: {% if "woff2" in formats %}{% if '//' in formats.woff2 %}url('{{ formats.woff2 }}'){% else %}url('{% static formats.woff2 %}'){% endif %} format('woff2'),{% endif %}
{% if "woff" in formats %}{% if '//' in formats.woff %}url('{{ formats.woff }}'){% else %}url('{% static formats.woff %}'){% endif %} format('woff'),{% endif %}
{% if "truetype" in formats %}{% if '//' in formats.truetype %}url('{{ formats.truetype }}'){% else %}url('{% static formats.truetype %}'){% endif %} format('truetype'){% endif %};
src: {% if "woff2" in formats %}url('{% static formats.woff2 %}') format('woff2'),{% endif %}
{% if "woff" in formats %}url('{% static formats.woff %}') format('woff'),{% endif %}
{% if "truetype" in formats %}url('{% static formats.truetype %}') format('truetype'){% endif %};
}
.preload-font[data-family="{{family}}"][data-style="{{style}}"] {
font-family: '{{ family }}', 'AND';
@@ -503,13 +503,11 @@
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
</div>
</div>
@@ -167,13 +167,11 @@
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
{% bootstrap_field f.available_from form_group_class="" layout="bulkedit_inline" %}
</div>
<div class="col-md-5">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
{% bootstrap_field f.available_until form_group_class="" layout="bulkedit_inline" %}
</div>
</div>
@@ -145,13 +145,11 @@
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
{% bootstrap_field f.available_from form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
</div>
</div>
@@ -1,6 +0,0 @@
{% load i18n %}
{% if mode == "hide" %}
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Hide product if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-eye-slash"></span></span>
{% else %}
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Show info text if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-info-circle"></span></span>
{% endif %}
@@ -10,10 +10,6 @@
<div class="alert alert-danger">
{% trans "The waiting list is disabled, so if the event is sold out, people cannot add themselves to this list. If you want to enable it, go to the event settings." %}
</div>
{% elif not request.event.waiting_list_active and not request.event.has_subevents %}
<div class="alert alert-danger">
{% trans "The waiting list is no longer active for this event. The waiting list no longer affects quotas and no longer notifies waiting users." %}
</div>
{% endif %}
<div class="row">
{% if 'can_change_orders' in request.eventpermset %}
+3 -4
View File
@@ -38,8 +38,8 @@ from django.views.generic.base import RedirectView
from pretix.control.views import (
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
main, oauth, orderimport, orders, organizer, pdf, search, shredder,
subevents, typeahead, user, users, vouchers, waitinglist,
main, new_dashboard, oauth, orderimport, orders, organizer, pdf, search,
shredder, subevents, typeahead, user, users, vouchers, waitinglist,
)
urlpatterns = [
@@ -239,7 +239,7 @@ urlpatterns = [
re_path(r'^search/orders/$', search.OrderSearch.as_view(), name='search.orders'),
re_path(r'^search/payments/$', search.PaymentSearch.as_view(), name='search.payments'),
re_path(r'^event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include([
re_path(r'^$', dashboards.event_index, name='event.index'),
re_path(r'^$', new_dashboard.IndexView.as_view(), name='event.index'),
re_path(r'^qrcode.(?P<filetype>(png|jpeg|gif|svg))$', event.EventQRCode.as_view(), name='event.qrcode'),
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
@@ -293,7 +293,6 @@ urlpatterns = [
re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),
re_path(r'^items/select2/itemvar$', typeahead.itemvar_select2, name='event.items.itemvar.select2'),
re_path(r'^items/select2/itemvars$', typeahead.itemvars_select2, name='event.items.itemvars.select2'),
re_path(r'^items/select2/variation$', typeahead.variations_select2, name='event.items.variations.select2'),
re_path(r'^categories/$', item.CategoryList.as_view(), name='event.items.categories'),
re_path(r'^categories/select2$', typeahead.category_select2, name='event.items.categories.select2'),
+32 -53
View File
@@ -32,11 +32,11 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import base64
import json
import logging
import time
from urllib.parse import quote, urlparse
from urllib.parse import quote
import webauthn
from django.conf import settings
@@ -54,7 +54,6 @@ from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from django_otp import match_token
from webauthn.helpers import generate_challenge
from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import (
@@ -63,6 +62,7 @@ from pretix.base.forms.auth import (
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.base.services.mail import SendMailException
from pretix.helpers.http import redirect_to_url
from pretix.helpers.webauthn import generate_challenge
logger = logging.getLogger(__name__)
@@ -389,10 +389,6 @@ def get_u2f_appid(request):
return settings.SITE_URL
def get_webauthn_rp_id(request):
return urlparse(settings.SITE_URL).hostname
class Login2FAView(TemplateView):
template_name = 'pretixcontrol/auth/login_2fa.html'
@@ -431,41 +427,25 @@ class Login2FAView(TemplateView):
devices = U2FDevice.objects.filter(user=self.user)
for d in devices:
credential_current_sign_count = d.sign_count if isinstance(d, WebAuthnDevice) else 0
try:
webauthn_assertion_response = webauthn.verify_authentication_response(
credential=resp,
expected_challenge=base64.b64decode(challenge),
expected_rp_id=get_webauthn_rp_id(self.request),
expected_origin=settings.SITE_URL,
credential_public_key=d.webauthnpubkey,
credential_current_sign_count=credential_current_sign_count,
)
sign_count = webauthn_assertion_response.new_sign_count
if sign_count < credential_current_sign_count:
raise Exception("Possible replay attack, sign count not higher")
except Exception:
wu = d.webauthnuser
if isinstance(d, U2FDevice):
# https://www.w3.org/TR/webauthn/#sctn-appid-extension says
# "When verifying the assertion, expect that the rpIdHash MAY be the hash of the AppID instead of the RP ID."
try:
webauthn_assertion_response = webauthn.verify_authentication_response(
credential=resp,
expected_challenge=base64.b64decode(challenge),
expected_rp_id=get_u2f_appid(self.request),
expected_origin=settings.SITE_URL,
credential_public_key=d.webauthnpubkey,
credential_current_sign_count=credential_current_sign_count,
)
if webauthn_assertion_response.new_sign_count < 1:
raise Exception("Possible replay attack, sign count set")
except Exception:
logger.exception('U2F login failed')
else:
valid = True
break
else:
logger.exception('Webauthn login failed')
# RP_ID needs to be appId for U2F devices, but we can't
# set it that way in U2FDevice.webauthnuser, since that
# breaks the frontend part.
wu.rp_id = settings.SITE_URL
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
wu,
resp,
challenge,
settings.SITE_URL,
uv_required=False # User Verification
)
sign_count = webauthn_assertion_response.verify()
except Exception:
logger.exception('U2F login failed')
else:
if isinstance(d, WebAuthnDevice):
d.sign_count = sign_count
@@ -491,24 +471,23 @@ class Login2FAView(TemplateView):
ctx = super().get_context_data()
if 'webauthn_challenge' in self.request.session:
del self.request.session['webauthn_challenge']
challenge = generate_challenge()
self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode()
challenge = generate_challenge(32)
self.request.session['webauthn_challenge'] = challenge
devices = [
device.webauthndevice for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user)
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.user)
] + [
device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.user)
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.user)
]
if devices:
auth_options = webauthn.generate_authentication_options(
rp_id=get_webauthn_rp_id(self.request),
challenge=challenge,
allow_credentials=devices,
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
devices,
challenge
)
# Backwards compatibility to U2F
j = json.loads(webauthn.options_to_json(auth_options))
j["extensions"] = {"appid": get_u2f_appid(self.request)}
ctx['jsondata'] = json.dumps(j)
ad = webauthn_assertion_options.assertion_dict
ad['extensions'] = {
'appid': get_u2f_appid(self.request)
}
ctx['jsondata'] = json.dumps(ad)
return ctx
def get(self, request, *args, **kwargs):
-6
View File
@@ -1483,12 +1483,6 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
def get_object(self, queryset=None) -> Item:
o = super().get_object(queryset)
if o.hide_without_voucher:
o.require_voucher = True
return o
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['plugin_forms'] = self.plugin_forms
+266
View File
@@ -0,0 +1,266 @@
import datetime
import json
from decimal import Decimal
import dateutil
from django.db.models import (
DateTimeField, F, Max, OuterRef, Prefetch, Q, Subquery, Sum,
)
from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils import formats, timezone
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.models import SubEvent
from pretix.base.models.orders import (
CancellationRequest, Order, OrderPayment, OrderPosition, OrderRefund,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.timeline import timeline_for_event
from pretix.control.forms.event import CommentForm
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import ChartContainingView
NUM_WIDGET = str('<div class="numwidget">'
'<span class="num">{num}</span>'
'<span class="text"><span class="label-primary">{text}</span></span>'
'<span class="text-add">{text_add}</span></div>')
class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView):
template_name = 'pretixcontrol/event/new_index.html'
permission = 'can_view_orders'
def get_context_data(self, **kwargs):
subevent = None
if self.request.GET.get("subevent", "") != "" and self.request.event.has_subevents:
i = self.request.GET.get("subevent", "")
try:
subevent = self.request.event.subevents.get(pk=i)
except SubEvent.DoesNotExist:
pass
can_view_orders = self.request.user.has_event_permission(self.request.organizer, self.request.event,
'can_view_orders',
request=self.request)
# can_change_event_settings = self.request.user.has_event_permission(self.request.organizer, self.request.event,
# 'can_change_event_settings',
# request=self.request)
ctx = {
'subevent': subevent,
'comment_form': CommentForm(initial={'comment': self.request.event.comment},
readonly=True), # not can_change_event_settings),
}
if subevent:
opqs = OrderPosition.objects.filter(subevent=subevent)
else:
opqs = OrderPosition.objects
ctx['shop_state'] = {
'display_size': 'small',
'priority': 1000,
'content': '<span class="{cls}">{t1} {state} <span class="fa {icon}"></span></span>'.format(
t1=_('Your ticket shop is'),
state=_('live') if self.request.event.live and not self.request.event.testmode else (
_('live and in test mode') if self.request.event.live else (
_('not yet public') if not self.request.event.testmode else (
_('in private test mode')
)
)
),
icon='fa-check-circle' if self.request.event.live and not self.request.event.testmode else (
'fa-warning' if self.request.event.live else (
'fa-times-circle' if not self.request.event.testmode else (
'fa-lock'
)
)
),
cls='live' if self.request.event.live else 'off'
),
'url': reverse('control:event.live', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
})
}
qs = self.request.event.checkin_lists.filter(subevent=subevent)
sales_channels = get_all_sales_channels()
for cl in qs:
if cl.subevent:
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
cl.auto_checkin_sales_channels = [sales_channels[channel] for channel in cl.auto_checkin_sales_channels]
ctx['checkinlists'] = qs
qs = self.request.event.subevents
if list:
qs = qs.prefetch_related(
Prefetch('quotas',
queryset=self.request.event.quotas.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
to_attr='first_quotas')
)
ctx['subevents'] = qs
quotas = []
for s in ctx['subevents']:
s.first_quotas = s.first_quotas[:4]
quotas += list(s.first_quotas)
qa = QuotaAvailability(early_out=False)
for q in quotas:
qa.queue(q)
qa.compute()
for q in quotas:
q.cached_avail = qa.results[q]
q.cached_availability_paid_orders = qa.count_paid_orders.get(q, 0)
if q.size is not None:
q.percent_paid = min(
100,
round(q.cached_availability_paid_orders / q.size * 100) if q.size > 0 else 100
)
tickc = opqs.filter(
order__event=self.request.event, item__admission=True,
order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
).count()
paidc = opqs.filter(
order__event=self.request.event, item__admission=True,
order__status=Order.STATUS_PAID,
).count()
ctx['attendees_paid_ordered'] = {
'content': NUM_WIDGET.format(
num=f'{tickc}',
text=_('<span class="fa fa-user icon"></span> Attendees'),
text_add=f'{paidc} paid, {tickc - paidc} pending'),
'priority': 100,
'url': reverse('control:event.orders.overview', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
}
if subevent:
rev = opqs.filter(
order__event=self.request.event, order__status=Order.STATUS_PAID
).aggregate(
sum=Sum('price')
)['sum'] or Decimal('0.00')
else:
rev = Order.objects.filter(
event=self.request.event,
status=Order.STATUS_PAID
).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')
ctx['total_revenue'] = {
'content': NUM_WIDGET.format(
num='<span class="icon">{currency}</span> {amount}'.format(
currency=self.request.event.currency,
amount=formats.localize(round_decimal(rev, self.request.event.currency))
),
text=_('Total revenue'),
text_add=''
),
'priority': 100,
'url': reverse('control:event.orders.overview', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
}
cache = self.request.event.cache
ckey = str(subevent.pk) if subevent else 'all'
tz = timezone.get_current_timezone()
op_date = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
payment_date__isnull=False
).values('order').annotate(
m=Max('payment_date')
).values(
'm'
).order_by()
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
payment_date__isnull=False
).values('order').annotate(
m=Max('payment_date')
).values(
'm'
).order_by()
ctx['rev_data'] = cache.get('statistics_rev_data' + ckey)
if not ctx['rev_data']:
rev_by_day = {}
if subevent:
for o in OrderPosition.objects.annotate(
payment_date=Subquery(op_date, output_field=DateTimeField())
).filter(order__event=self.request.event,
subevent=subevent,
order__status=Order.STATUS_PAID,
payment_date__isnull=False).values('payment_date', 'price'):
day = o['payment_date'].astimezone(tz).date()
rev_by_day[day] = rev_by_day.get(day, 0) + o['price']
else:
for o in Order.objects.annotate(
payment_date=Subquery(p_date, output_field=DateTimeField())
).filter(event=self.request.event,
status=Order.STATUS_PAID,
payment_date__isnull=False).values('payment_date', 'total'):
day = o['payment_date'].astimezone(tz).date()
rev_by_day[day] = rev_by_day.get(day, 0) + o['total']
data = []
total = 0
for d in dateutil.rrule.rrule(
dateutil.rrule.DAILY,
dtstart=min(rev_by_day.keys() if rev_by_day else [datetime.date.today()]),
until=max(rev_by_day.keys() if rev_by_day else [datetime.date.today()])):
d = d.date()
rev = float(rev_by_day.get(d, 0))
if True: # rev != 0:
total += rev
data.append({
'date': d.strftime('%Y-%m-%d'),
'revenue': round(total, 2),
})
ctx['rev_data'] = json.dumps(data)
cache.set('statistics_rev_data' + ckey, ctx['rev_data'])
ctx['has_overpaid_orders'] = can_view_orders and Order.annotate_overpayments(self.request.event.orders).filter(
Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
| Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
).exists()
ctx['has_pending_orders_with_full_payment'] = can_view_orders and Order.annotate_overpayments(
self.request.event.orders).filter(
Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0) & Q(
require_approval=False)
).exists()
ctx['has_pending_refunds'] = can_view_orders and OrderRefund.objects.filter(
order__event=self.request.event,
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_EXTERNAL)
).exists()
ctx['has_pending_approvals'] = can_view_orders and self.request.event.orders.filter(
status=Order.STATUS_PENDING,
require_approval=True
).exists()
ctx['has_cancellation_requests'] = can_view_orders and CancellationRequest.objects.filter(
order__event=self.request.event
).exists()
ctx['timeline'] = [
{
'date': t.datetime.astimezone(self.request.event.timezone).date(),
'entry': t,
'time': t.datetime.astimezone(self.request.event.timezone)
}
for t in timeline_for_event(self.request.event, subevent)
]
ctx['today'] = now().astimezone(self.request.event.timezone).date()
ctx['nearly_now'] = now().astimezone(self.request.event.timezone) - datetime.timedelta(seconds=20)
# resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
return ctx

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