forked from CGM_Public/pretix_original
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75fd41d6f6 | ||
|
|
20b3f43891 | ||
|
|
0b41aeb7f1 | ||
|
|
8e71102ab2 | ||
|
|
5ebaa63f6f | ||
|
|
e68cea8542 | ||
|
|
db0af4e46f | ||
|
|
b701854127 | ||
|
|
dd5d614da2 | ||
|
|
1c1652e76b | ||
|
|
64838c53f1 | ||
|
|
b47f57cf8e |
@@ -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
|
||||
|
||||
@@ -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``
|
||||
@@ -70,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``.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
""""""""""""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -37,12 +37,9 @@ contact_email string Contact person
|
||||
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
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -65,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
|
||||
@@ -111,9 +107,7 @@ Endpoints
|
||||
"contact_email": "johnson@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,9 +158,7 @@ Endpoints
|
||||
"contact_email": "johnson@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,
|
||||
@@ -396,9 +388,7 @@ Endpoints
|
||||
"contact_email": "johnson@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,
|
||||
@@ -456,9 +446,7 @@ Endpoints
|
||||
"contact_email": "johnson@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,
|
||||
@@ -573,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.
|
||||
@@ -609,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,
|
||||
@@ -648,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
|
||||
@@ -683,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:**
|
||||
@@ -717,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
|
||||
@@ -774,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,4 +1,4 @@
|
||||
sphinx==7.2.*
|
||||
sphinx==7.0.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
+34
-31
@@ -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==23.5",
|
||||
"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.5.*",
|
||||
"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.5.2", # 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==4.25.*",
|
||||
"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.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.40.*",
|
||||
"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==13.*",
|
||||
"pytest-mock==3.10.*",
|
||||
"pytest-rerunfailures==11.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.5.*",
|
||||
"pytest==8.0.*",
|
||||
"pytest-xdist==3.3.*",
|
||||
"pytest==7.3.*",
|
||||
"responses",
|
||||
]
|
||||
|
||||
|
||||
@@ -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.2.0"
|
||||
__version__ = "2023.11.0.dev0"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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=""),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
+505
-12
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -44,25 +44,6 @@ from pretix.multidomain.urlreverse import (
|
||||
_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):
|
||||
|
||||
"""
|
||||
@@ -84,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:
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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']):
|
||||
|
||||
@@ -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."
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -3229,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': {
|
||||
@@ -3663,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'),
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -72,7 +72,6 @@ class EventSlugBanlistValidator(BanlistValidator):
|
||||
'widget',
|
||||
'customer',
|
||||
'account',
|
||||
'lead',
|
||||
]
|
||||
|
||||
|
||||
@@ -93,7 +92,6 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
|
||||
'api',
|
||||
'csp_report',
|
||||
'widget',
|
||||
'lead',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -219,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
|
||||
|
||||
@@ -259,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(),
|
||||
@@ -276,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):
|
||||
@@ -426,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
|
||||
|
||||
@@ -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,
|
||||
@@ -502,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"),
|
||||
@@ -592,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
|
||||
@@ -685,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 = []
|
||||
@@ -938,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',
|
||||
@@ -1350,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)
|
||||
|
||||
@@ -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 it’s 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',
|
||||
]
|
||||
|
||||
@@ -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', {})
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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">
|
||||
···
|
||||
</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">
|
||||
···
|
||||
</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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" %}">
|
||||
|
||||
@@ -234,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"
|
||||
@@ -332,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>
|
||||
@@ -414,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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
-6
@@ -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 %}
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -119,9 +119,9 @@ from pretix.control.forms.filter import (
|
||||
RefundFilterForm,
|
||||
)
|
||||
from pretix.control.forms.orders import (
|
||||
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
CancelForm, CommentForm, ConfirmPaymentForm, DenyForm, EventCancelForm,
|
||||
ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm,
|
||||
OrderFeeChangeForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||
)
|
||||
@@ -1014,10 +1014,9 @@ class OrderPaymentConfirm(OrderView):
|
||||
|
||||
@cached_property
|
||||
def mark_paid_form(self):
|
||||
return MarkPaidForm(
|
||||
return ConfirmPaymentForm(
|
||||
instance=self.order,
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
payment=self.payment,
|
||||
)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
@@ -1028,19 +1027,8 @@ class OrderPaymentConfirm(OrderView):
|
||||
'order': self.order,
|
||||
})
|
||||
try:
|
||||
payment_date = None
|
||||
if self.mark_paid_form.cleaned_data['payment_date'] != now().date():
|
||||
payment_date = make_aware(datetime.combine(
|
||||
self.mark_paid_form.cleaned_data['payment_date'],
|
||||
time(hour=0, minute=0, second=0)
|
||||
), self.order.event.timezone)
|
||||
|
||||
self.payment.amount = self.mark_paid_form.cleaned_data['amount']
|
||||
self.payment.save(update_fields=['amount'])
|
||||
self.payment.confirm(user=self.request.user,
|
||||
send_mail=self.mark_paid_form.cleaned_data['send_email'],
|
||||
count_waitinglist=False,
|
||||
payment_date=payment_date,
|
||||
force=self.mark_paid_form.cleaned_data.get('force', False))
|
||||
except Quota.QuotaExceededException as e:
|
||||
messages.error(self.request, str(e))
|
||||
|
||||
@@ -281,7 +281,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
'name': q.name,
|
||||
'release_after_exit': q.release_after_exit,
|
||||
'ignore_for_event_availability': q.ignore_for_event_availability,
|
||||
'itemvars': [str(i.pk) for i in q.items.all() if (len(i.variations.all()) == 0)] + [
|
||||
'itemvars': [str(i.pk) for i in q.items.all()] + [
|
||||
'{}-{}'.format(v.item_id, v.pk) for v in q.variations.all()
|
||||
]
|
||||
} for q in self.copy_from.quotas.prefetch_related('items', 'variations')
|
||||
@@ -295,7 +295,6 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
]
|
||||
extra = 0
|
||||
|
||||
kwargs['searchable_selection'] = True
|
||||
formsetclass = inlineformset_factory(
|
||||
SubEvent, Quota,
|
||||
form=QuotaForm, formset=QuotaFormSet, min_num=1, validate_min=True,
|
||||
@@ -400,19 +399,6 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
except SubEvent.DoesNotExist:
|
||||
pass
|
||||
|
||||
def _copy_from_date_to_relative(self, value):
|
||||
if not value:
|
||||
return value
|
||||
tz = self.request.event.timezone
|
||||
days = (self.copy_from.date_from.astimezone(tz).date() - value.astimezone(tz).date()).days
|
||||
return RelativeDateWrapper(RelativeDate(
|
||||
days=abs(days),
|
||||
base_date_name='date_from',
|
||||
time=value.astimezone(tz).time(),
|
||||
minutes=None,
|
||||
is_after=days < 0,
|
||||
))
|
||||
|
||||
@cached_property
|
||||
def itemvar_forms(self):
|
||||
se_item_instances = {
|
||||
@@ -425,21 +411,15 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
if self.copy_from:
|
||||
se_item_instances = {
|
||||
sei.item_id: SubEventItem(
|
||||
item=sei.item,
|
||||
price=sei.price,
|
||||
disabled=sei.disabled,
|
||||
available_from=sei.available_from,
|
||||
available_until=sei.available_until
|
||||
item=sei.item, price=sei.price, disabled=sei.disabled,
|
||||
available_from=sei.available_from, available_until=sei.available_until
|
||||
)
|
||||
for sei in SubEventItem.objects.filter(subevent=self.copy_from).select_related('item')
|
||||
}
|
||||
se_var_instances = {
|
||||
sei.variation_id: SubEventItemVariation(
|
||||
variation=sei.variation,
|
||||
price=sei.price,
|
||||
disabled=sei.disabled,
|
||||
available_from=sei.available_from,
|
||||
available_until=sei.available_until
|
||||
variation=sei.variation, price=sei.price, disabled=sei.disabled,
|
||||
available_from=sei.available_from, available_until=sei.available_until
|
||||
)
|
||||
for sei in SubEventItemVariation.objects.filter(subevent=self.copy_from).select_related('variation')
|
||||
}
|
||||
@@ -449,34 +429,17 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
if i.has_variations:
|
||||
for v in i.variations.all():
|
||||
inst = se_var_instances.get(v.pk) or SubEventItemVariation(subevent=self.object, variation=v)
|
||||
if self.copy_from:
|
||||
initial = {
|
||||
'rel_available_from': self._copy_from_date_to_relative(inst.available_from),
|
||||
'rel_available_until': self._copy_from_date_to_relative(inst.available_until)
|
||||
}
|
||||
else:
|
||||
initial = {}
|
||||
formlist.append(self.itemvarformclass(
|
||||
prefix='itemvar-{}'.format(v.pk),
|
||||
item=i,
|
||||
variation=v,
|
||||
initial=initial,
|
||||
item=i, variation=v,
|
||||
instance=inst,
|
||||
data=(self.request.POST if self.request.method == "POST" else None)
|
||||
))
|
||||
else:
|
||||
inst = se_item_instances.get(i.pk) or SubEventItem(subevent=self.object, item=i)
|
||||
if self.copy_from:
|
||||
initial = {
|
||||
'rel_available_from': self._copy_from_date_to_relative(inst.available_from),
|
||||
'rel_available_until': self._copy_from_date_to_relative(inst.available_until)
|
||||
}
|
||||
else:
|
||||
initial = {}
|
||||
formlist.append(self.itemformclass(
|
||||
prefix='item-{}'.format(i.pk),
|
||||
item=i,
|
||||
initial=initial,
|
||||
instance=inst,
|
||||
data=(self.request.POST if self.request.method == "POST" else None)
|
||||
))
|
||||
@@ -794,8 +757,18 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
|
||||
initial['time_from'] = i.date_from.astimezone(tz).time()
|
||||
initial['time_to'] = i.date_to.astimezone(tz).time() if i.date_to else None
|
||||
initial['time_admission'] = i.date_admission.astimezone(tz).time() if i.date_admission else None
|
||||
initial['rel_presale_start'] = self._copy_from_date_to_relative(i.presale_start)
|
||||
initial['rel_presale_end'] = self._copy_from_date_to_relative(i.presale_end)
|
||||
initial['rel_presale_start'] = RelativeDateWrapper(RelativeDate(
|
||||
days=(i.date_from.astimezone(tz).date() - i.presale_start.astimezone(tz).date()).days,
|
||||
base_date_name='date_from',
|
||||
time=i.presale_start.astimezone(tz).time(),
|
||||
minutes=None
|
||||
)) if i.presale_start else None
|
||||
initial['rel_presale_end'] = RelativeDateWrapper(RelativeDate(
|
||||
days=(i.date_from.astimezone(tz).date() - i.presale_end.astimezone(tz).date()).days,
|
||||
base_date_name='date_from',
|
||||
time=i.presale_end.astimezone(tz).time(),
|
||||
minutes=None
|
||||
)) if i.presale_end else None
|
||||
else:
|
||||
kwargs['instance'] = SubEvent(event=self.request.event)
|
||||
initial['location'] = self.request.event.location
|
||||
|
||||
@@ -674,49 +674,6 @@ def itemvar_select2(request, **kwargs):
|
||||
{
|
||||
'id': k,
|
||||
'text': str(v),
|
||||
'inactive': d,
|
||||
}
|
||||
for k, v, d in choices
|
||||
],
|
||||
'pagination': {
|
||||
"more": total >= (offset + pagesize)
|
||||
}
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@event_permission_required(None)
|
||||
def itemvars_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
pagesize = 20
|
||||
offset = (page - 1) * pagesize
|
||||
|
||||
choices = []
|
||||
|
||||
# We are very unlikely to need pagination
|
||||
itemqs = request.event.items.prefetch_related('variations').filter(
|
||||
Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query))
|
||||
total = itemqs.count()
|
||||
|
||||
for i in itemqs[offset:offset + pagesize]:
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i, v.value), not v.active))
|
||||
else:
|
||||
choices.append((str(i.pk), str(i), not i.active))
|
||||
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'id': k,
|
||||
'text': str(v),
|
||||
'inactive': d,
|
||||
}
|
||||
for k, v, d in choices
|
||||
],
|
||||
|
||||
@@ -35,9 +35,10 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
import webauthn
|
||||
from django.conf import settings
|
||||
@@ -56,7 +57,6 @@ from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_scopes import scopes_disabled
|
||||
from webauthn.helpers import generate_challenge, generate_user_handle
|
||||
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.forms.auth import ReauthForm
|
||||
@@ -70,9 +70,9 @@ from pretix.control.forms.users import StaffSessionForm
|
||||
from pretix.control.permissions import (
|
||||
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
||||
)
|
||||
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
|
||||
from pretix.control.views.auth import get_u2f_appid
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.helpers.u2f import websafe_encode
|
||||
from pretix.helpers.webauthn import generate_challenge, generate_ukey
|
||||
|
||||
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -105,41 +105,25 @@ class ReauthView(TemplateView):
|
||||
devices = U2FDevice.objects.filter(user=self.request.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
|
||||
@@ -178,24 +162,23 @@ class ReauthView(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.request.user)
|
||||
device.webauthnuser for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user)
|
||||
] + [
|
||||
device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)
|
||||
device.webauthnuser for device in U2FDevice.objects.filter(confirmed=True, user=self.request.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)
|
||||
ctx['form'] = self.form
|
||||
return ctx
|
||||
|
||||
@@ -404,26 +387,23 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
if 'webauthn_challenge' in self.request.session:
|
||||
del self.request.session['webauthn_challenge']
|
||||
|
||||
challenge = generate_challenge()
|
||||
ukey = generate_user_handle()
|
||||
challenge = generate_challenge(32)
|
||||
ukey = generate_ukey()
|
||||
|
||||
self.request.session['webauthn_challenge'] = base64.b64encode(challenge).decode()
|
||||
self.request.session['webauthn_register_ukey'] = base64.b64encode(ukey).decode()
|
||||
self.request.session['webauthn_challenge'] = challenge
|
||||
self.request.session['webauthn_register_ukey'] = ukey
|
||||
|
||||
devices = [
|
||||
device.webauthndevice for device in WebAuthnDevice.objects.filter(confirmed=True, user=self.request.user)
|
||||
] + [
|
||||
device.webauthndevice for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)
|
||||
]
|
||||
make_credential_options = webauthn.generate_registration_options(
|
||||
rp_id=get_webauthn_rp_id(self.request),
|
||||
rp_name=get_webauthn_rp_id(self.request),
|
||||
user_id=ukey,
|
||||
user_name=self.request.user.email,
|
||||
challenge=challenge,
|
||||
exclude_credentials=devices,
|
||||
make_credential_options = webauthn.WebAuthnMakeCredentialOptions(
|
||||
challenge,
|
||||
urlparse(settings.SITE_URL).netloc,
|
||||
urlparse(settings.SITE_URL).netloc,
|
||||
ukey,
|
||||
self.request.user.email,
|
||||
str(self.request.user),
|
||||
settings.SITE_URL,
|
||||
attestation="none"
|
||||
)
|
||||
ctx['jsondata'] = webauthn.options_to_json(make_credential_options)
|
||||
ctx['jsondata'] = json.dumps(make_credential_options.registration_dict)
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -432,13 +412,30 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
challenge = self.request.session['webauthn_challenge']
|
||||
ukey = self.request.session['webauthn_register_ukey']
|
||||
resp = json.loads(self.request.POST.get("token"))
|
||||
trust_anchor_dir = os.path.normpath(os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'../../static/webauthn_trusted_attestation_roots' # currently does not exist
|
||||
))
|
||||
# We currently do not check attestation certificates, since there's no real risk
|
||||
# and we do not have any policies specifying what devices can be used. (Also, we
|
||||
# didn't get it to work.)
|
||||
# Read more: https://fidoalliance.org/fido-technotes-the-truth-about-attestation/
|
||||
trusted_attestation_cert_required = False
|
||||
self_attestation_permitted = True
|
||||
none_attestation_permitted = True
|
||||
|
||||
registration_verification = webauthn.verify_registration_response(
|
||||
credential=resp,
|
||||
expected_challenge=base64.b64decode(challenge),
|
||||
expected_rp_id=get_webauthn_rp_id(self.request),
|
||||
expected_origin=settings.SITE_URL,
|
||||
webauthn_registration_response = webauthn.WebAuthnRegistrationResponse(
|
||||
urlparse(settings.SITE_URL).netloc,
|
||||
settings.SITE_URL,
|
||||
resp,
|
||||
challenge,
|
||||
trust_anchor_dir,
|
||||
trusted_attestation_cert_required,
|
||||
self_attestation_permitted,
|
||||
none_attestation_permitted,
|
||||
uv_required=False
|
||||
)
|
||||
webauthn_credential = webauthn_registration_response.verify()
|
||||
|
||||
# Check that the credentialId is not yet registered to any other user.
|
||||
# If registration is requested for a credential that is already registered
|
||||
@@ -446,7 +443,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
# ceremony, or it MAY decide to accept the registration, e.g. while deleting
|
||||
# the older registration.
|
||||
credential_id_exists = WebAuthnDevice.objects.filter(
|
||||
credential_id=registration_verification.credential_id
|
||||
credential_id=webauthn_credential.credential_id
|
||||
).first()
|
||||
if credential_id_exists:
|
||||
messages.error(request, _('This security device is already registered.'))
|
||||
@@ -454,11 +451,14 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
self.device.credential_id = websafe_encode(registration_verification.credential_id)
|
||||
self.device.ukey = websafe_encode(ukey)
|
||||
self.device.pub_key = websafe_encode(registration_verification.credential_public_key)
|
||||
self.device.sign_count = registration_verification.sign_count
|
||||
self.device.rp_id = get_webauthn_rp_id(request)
|
||||
webauthn_credential.credential_id = str(webauthn_credential.credential_id, "utf-8")
|
||||
webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8")
|
||||
|
||||
self.device.credential_id = webauthn_credential.credential_id
|
||||
self.device.ukey = ukey
|
||||
self.device.pub_key = webauthn_credential.public_key
|
||||
self.device.sign_count = webauthn_credential.sign_count
|
||||
self.device.rp_id = urlparse(settings.SITE_URL).netloc
|
||||
self.device.icon_url = settings.SITE_URL
|
||||
self.device.confirmed = True
|
||||
self.device.save()
|
||||
|
||||
@@ -44,8 +44,6 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
|
||||
# This will only work on secure cookies as well
|
||||
# https://www.chromestatus.com/feature/5633521622188032
|
||||
response.cookies[key]['secure'] = is_secure
|
||||
# CHIPS
|
||||
response.cookies[key]['Partitioned'] = True
|
||||
|
||||
|
||||
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
|
||||
|
||||
@@ -1,21 +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/>.
|
||||
#
|
||||
@@ -1,54 +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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
# Date according to https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
|
||||
SHORT_DATE_FORMAT = "Y-m-d"
|
||||
SHORT_DATETIME_FORMAT = 'Y-m-d P'
|
||||
TIME_FORMAT = 'P'
|
||||
WEEK_FORMAT = '\\W W, o'
|
||||
WEEK_DAY_FORMAT = 'D, M jS'
|
||||
SHORT_MONTH_DAY_FORMAT = 'm/d'
|
||||
|
||||
DATE_INPUT_FORMATS = [
|
||||
"%d/%m/%Y", # '15-05-2006'
|
||||
"%Y-%m-%d", # '2006-05-15'
|
||||
"%y-%m-%d", # '06-05-15'
|
||||
]
|
||||
TIME_INPUT_FORMATS = [
|
||||
'%I:%M %p',
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M:%S.%f', # '14:30:59.000200'
|
||||
'%H:%M', # '14:30'
|
||||
]
|
||||
@@ -44,12 +44,11 @@ def validate_uploaded_file_for_valid_image(f):
|
||||
# have to read the data into memory.
|
||||
if hasattr(f, 'temporary_file_path'):
|
||||
file = f.temporary_file_path()
|
||||
elif hasattr(f, 'read'):
|
||||
if hasattr(f, 'seek') and callable(f.seek):
|
||||
f.seek(0)
|
||||
file = BytesIO(f.read())
|
||||
else:
|
||||
file = BytesIO(f['content'])
|
||||
if hasattr(f, 'read'):
|
||||
file = BytesIO(f.read())
|
||||
else:
|
||||
file = BytesIO(f['content'])
|
||||
|
||||
try:
|
||||
try:
|
||||
|
||||
@@ -62,7 +62,7 @@ def soft_equals(a, b):
|
||||
|
||||
def hard_equals(a, b):
|
||||
"""Implements the '===' operator."""
|
||||
if type(a) is not type(b):
|
||||
if type(a) != type(b):
|
||||
return False
|
||||
return a == b
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#
|
||||
import types
|
||||
from datetime import datetime
|
||||
from http import cookies
|
||||
|
||||
from PIL import Image
|
||||
from requests.adapters import HTTPAdapter
|
||||
@@ -89,14 +88,7 @@ def monkeypatch_requests_timeout():
|
||||
HTTPAdapter.send = httpadapter_send
|
||||
|
||||
|
||||
def monkeypatch_cookie_morsel():
|
||||
# See https://code.djangoproject.com/ticket/34613
|
||||
cookies.Morsel._flags.add("partitioned")
|
||||
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
|
||||
|
||||
|
||||
def monkeypatch_all_at_ready():
|
||||
monkeypatch_vobject_performance()
|
||||
monkeypatch_pillow_safer()
|
||||
monkeypatch_requests_timeout()
|
||||
monkeypatch_cookie_morsel()
|
||||
|
||||
@@ -1,76 +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/>.
|
||||
#
|
||||
|
||||
# Backwards compatibility for old U2F key material, with code taken from
|
||||
#
|
||||
# https://github.com/Yubico/python-u2flib-server/blob/python-u2flib-server-4.0.1/u2flib_server/utils.py
|
||||
#
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
|
||||
PUB_KEY_DER_PREFIX = b'\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01' \
|
||||
b'\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42\x00'
|
||||
|
||||
|
||||
def pub_key_from_der(der):
|
||||
return load_der_public_key(PUB_KEY_DER_PREFIX + der, default_backend())
|
||||
|
||||
|
||||
def websafe_decode(data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode('ascii')
|
||||
data += b'=' * (-len(data) % 4)
|
||||
return urlsafe_b64decode(data)
|
||||
|
||||
|
||||
def websafe_encode(data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode('ascii')
|
||||
return urlsafe_b64encode(data).replace(b'=', b'').decode('ascii')
|
||||
@@ -19,16 +19,27 @@
|
||||
# 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 django import template
|
||||
|
||||
register = template.Library()
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
@register.filter("iter_weekdays")
|
||||
def iter_weekdays(weeks):
|
||||
for i in range(7):
|
||||
for w in weeks:
|
||||
if i <= len(w) and w[i]:
|
||||
yield w[i]['date']
|
||||
break
|
||||
def generate_challenge(challenge_len):
|
||||
return ''.join([
|
||||
random.SystemRandom().choice(string.ascii_letters + string.digits)
|
||||
for i in range(challenge_len)
|
||||
])
|
||||
|
||||
|
||||
def generate_ukey():
|
||||
"""
|
||||
Its value's id member is required, and contains an identifier
|
||||
for the account, specified by the Relying Party. This is not meant
|
||||
to be displayed to the user, but is used by the Relying Party to
|
||||
control the number of credentials - an authenticator will never
|
||||
contain more than one credential for a given Relying Party under
|
||||
the same id.
|
||||
A unique identifier for the entity. For a relying party entity,
|
||||
sets the RP ID. For a user account entity, this will be an
|
||||
arbitrary string specified by the relying party.
|
||||
"""
|
||||
return generate_challenge(20)
|
||||
+2611
-2787
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-28 13:11+0000\n"
|
||||
"POT-Creation-Date: 2024-01-11 10:07+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -135,11 +135,9 @@ msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:225
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:244
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:229
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:262
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:280
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
|
||||
@@ -169,7 +167,7 @@ msgstr ""
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:291
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:236
|
||||
msgid "Contacting your bank …"
|
||||
msgstr ""
|
||||
|
||||
@@ -458,62 +456,54 @@ msgid "Number of days with a previous entry"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:133
|
||||
msgid "Number of days with a previous entry since"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Number of days with a previous entry before"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:141
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:145
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:164
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:165
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:166
|
||||
msgid "Event start"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:175
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:167
|
||||
msgid "Event end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:176
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:168
|
||||
msgid "Event admission"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:177
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:169
|
||||
msgid "custom date and time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:178
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:170
|
||||
msgid "custom time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:179
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:171
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
msgid "Add condition"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:181
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:182
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
@@ -609,23 +599,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:791
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:783
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:786
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:952
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:944
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:992
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:984
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1067
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1059
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -795,153 +785,133 @@ msgid "Only available with a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Not yet available"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Currently not available"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||
"in a new tab to continue."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "Open ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"We could not create your cart, since there are currently too many users in "
|
||||
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgctxt "widget"
|
||||
msgid "Show variants"
|
||||
msgid "See variations"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Hide variants"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different date"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgctxt "widget"
|
||||
msgid "Next week"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgctxt "widget"
|
||||
msgid "Previous week"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||
@@ -949,83 +919,83 @@ msgid ""
|
||||
"again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgctxt "widget"
|
||||
msgid "Load more"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:72
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:73
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:86
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:87
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:88
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:89
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
+2627
-2867
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-28 13:11+0000\n"
|
||||
"POT-Creation-Date: 2024-01-11 10:07+0000\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -139,11 +139,9 @@ msgid "Continue"
|
||||
msgstr "المتابعة"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:225
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:244
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:229
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:262
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:280
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "جاري تأكيد الدفع الخاص بك …"
|
||||
|
||||
@@ -173,7 +171,7 @@ msgstr "جاري الاتصال بStripe …"
|
||||
msgid "Total"
|
||||
msgstr "المجموع"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:291
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:236
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "جاري الاتصال بالبنك الذي تتعامل معه …"
|
||||
|
||||
@@ -484,66 +482,54 @@ msgid "Number of days with a previous entry"
|
||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:133
|
||||
#, fuzzy
|
||||
#| msgid "Number of days with a previous entry"
|
||||
msgid "Number of days with a previous entry since"
|
||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
#, fuzzy
|
||||
#| msgid "Number of days with a previous entry"
|
||||
msgid "Number of days with a previous entry before"
|
||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:141
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:145
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:164
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr "جميع الشروط في الأسفل"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:165
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr "خيار واحد على الأقل"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:166
|
||||
msgid "Event start"
|
||||
msgstr "بداية الفعالية"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:175
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:167
|
||||
msgid "Event end"
|
||||
msgstr "نهاية الفعالية"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:176
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:168
|
||||
msgid "Event admission"
|
||||
msgstr "تسجيل الفعالية"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:177
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:169
|
||||
msgid "custom date and time"
|
||||
msgstr "تحديد التاريخ والوقت"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:178
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:170
|
||||
msgid "custom time"
|
||||
msgstr "الوقت المحدد"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:179
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:171
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr "القدرة على التحمل (الدقائق)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
msgid "Add condition"
|
||||
msgstr "أضف شرطا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:181
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
msgid "minutes"
|
||||
msgstr "الدقائق"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:182
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
@@ -639,23 +625,23 @@ msgstr "لا شيء"
|
||||
msgid "Selected only"
|
||||
msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:791
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:783
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:786
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:952
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:944
|
||||
msgid "Use a different name internally"
|
||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:992
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:984
|
||||
msgid "Click to close"
|
||||
msgstr "اضغط لاغلاق الصفحة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1067
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1059
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "لم تقم بحفظ التعديلات!"
|
||||
|
||||
@@ -844,50 +830,29 @@ msgid "Only available with a voucher"
|
||||
msgstr "متوفرة مع القسيمة فقط"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "currently available: %s"
|
||||
msgctxt "widget"
|
||||
msgid "Not yet available"
|
||||
msgstr "متوفر حاليا: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "currently available: %s"
|
||||
msgctxt "widget"
|
||||
msgid "Currently not available"
|
||||
msgstr "متوفر حاليا: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr "الحد الأدنى للطلب: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr "إغلاق متجر التذاكر"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr "لا يمكن تحميل متجر التذاكر."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||
"in a new tab to continue."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "Close ticket shop"
|
||||
@@ -895,24 +860,24 @@ msgctxt "widget"
|
||||
msgid "Open ticket shop"
|
||||
msgstr "إغلاق متجر التذاكر"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr "لا يمكن إنشاء سلة التسوق. الرجاء المحاولة مرة أخرى في وقت لاحق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"We could not create your cart, since there are currently too many users in "
|
||||
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr "قائمة الإنتظار"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
@@ -921,93 +886,82 @@ msgstr ""
|
||||
"لديك الآن سلة تسوق مفعلة لهذا الحدث. إذا قمت باختيار منتجات أخرى سيتم "
|
||||
"إضافتها إلى سلة التسوق الموجودة حالياً."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr "استئناف الدفع"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr "استبدال قسيمة"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr "استبدال"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr "رمز القسيمة"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr "إغلاق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr "استمرار"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "See variations"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgctxt "widget"
|
||||
msgid "Show variants"
|
||||
msgid "See variations"
|
||||
msgstr "أنظر إلى الاختلافات"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "See variations"
|
||||
msgctxt "widget"
|
||||
msgid "Hide variants"
|
||||
msgstr "أنظر إلى الاختلافات"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr "اختر فعالية أخرى"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different date"
|
||||
msgstr "اختر تاريخا آخر"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr "العودة"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr "الشهر القادم"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr "الشهر السابق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgctxt "widget"
|
||||
msgid "Next week"
|
||||
msgstr "الأسبوع القادم"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgctxt "widget"
|
||||
msgid "Previous week"
|
||||
msgstr "الأسبوع السابق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr "خيارات المقاعد"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||
@@ -1015,93 +969,89 @@ msgid ""
|
||||
"again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#, fuzzy
|
||||
#| msgid "Load more"
|
||||
msgctxt "widget"
|
||||
msgid "Load more"
|
||||
msgstr "تحميل المزيد"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "Mo"
|
||||
msgstr "الإثنين"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "Tu"
|
||||
msgstr "الثلاثاء"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "We"
|
||||
msgstr "الأربعاء"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:72
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "Th"
|
||||
msgstr "الخميس"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:73
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "Fr"
|
||||
msgstr "الجمعة"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
msgid "Sa"
|
||||
msgstr "السبت"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
msgid "Su"
|
||||
msgstr "الأحد"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
msgid "January"
|
||||
msgstr "يناير"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
msgid "February"
|
||||
msgstr "فبراير"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||
msgid "March"
|
||||
msgstr "مارس"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||
msgid "April"
|
||||
msgstr "أبريل"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
msgid "May"
|
||||
msgstr "مايو"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
msgid "June"
|
||||
msgstr "يونيو"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
msgid "July"
|
||||
msgstr "يوليو"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
msgid "August"
|
||||
msgstr "أغسطس"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:86
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
msgid "September"
|
||||
msgstr "سبتمبر"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:87
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
msgid "October"
|
||||
msgstr "أكتوبر"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:88
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
msgid "November"
|
||||
msgstr "نوفمبر"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:89
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
msgid "December"
|
||||
msgstr "ديسمبر"
|
||||
|
||||
#~ msgctxt "widget"
|
||||
#~ msgid "See variations"
|
||||
#~ msgstr "أنظر إلى الاختلافات"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "قم بمسح QR"
|
||||
|
||||
|
||||
+2622
-2854
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-28 13:11+0000\n"
|
||||
"POT-Creation-Date: 2024-01-11 10:07+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -136,11 +136,9 @@ msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:225
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:244
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:229
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:262
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:280
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
|
||||
@@ -170,7 +168,7 @@ msgstr ""
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:291
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:236
|
||||
msgid "Contacting your bank …"
|
||||
msgstr ""
|
||||
|
||||
@@ -459,62 +457,54 @@ msgid "Number of days with a previous entry"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:133
|
||||
msgid "Number of days with a previous entry since"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Number of days with a previous entry before"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:141
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:145
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:164
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:165
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:166
|
||||
msgid "Event start"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:175
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:167
|
||||
msgid "Event end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:176
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:168
|
||||
msgid "Event admission"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:177
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:169
|
||||
msgid "custom date and time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:178
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:170
|
||||
msgid "custom time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:179
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:171
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
msgid "Add condition"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:181
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:182
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
@@ -610,23 +600,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:791
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:783
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:786
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:952
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:944
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:992
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:984
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1067
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1059
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -802,153 +792,133 @@ msgid "Only available with a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Not yet available"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Currently not available"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||
"in a new tab to continue."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "Open ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"We could not create your cart, since there are currently too many users in "
|
||||
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgctxt "widget"
|
||||
msgid "Show variants"
|
||||
msgid "See variations"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Hide variants"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different date"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgctxt "widget"
|
||||
msgid "Next week"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgctxt "widget"
|
||||
msgid "Previous week"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||
@@ -956,83 +926,83 @@ msgid ""
|
||||
"again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgctxt "widget"
|
||||
msgid "Load more"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:72
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:73
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:86
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:87
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:88
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:89
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
+2617
-2862
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-28 13:11+0000\n"
|
||||
"POT-Creation-Date: 2024-01-11 10:07+0000\n"
|
||||
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -136,11 +136,9 @@ msgid "Continue"
|
||||
msgstr "Pokračovat"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:225
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:244
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:229
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:262
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:280
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "Potvrzuji vaši platbu …"
|
||||
|
||||
@@ -170,7 +168,7 @@ msgstr "Kontaktuji Stripe …"
|
||||
msgid "Total"
|
||||
msgstr "Celkem"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:291
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:236
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Kontaktuji vaši banku …"
|
||||
|
||||
@@ -475,66 +473,54 @@ msgid "Number of days with a previous entry"
|
||||
msgstr "Počet dní bez úprav"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:133
|
||||
#, fuzzy
|
||||
#| msgid "Number of days with a previous entry"
|
||||
msgid "Number of days with a previous entry since"
|
||||
msgstr "Počet dní bez úprav"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
#, fuzzy
|
||||
#| msgid "Number of days with a previous entry"
|
||||
msgid "Number of days with a previous entry before"
|
||||
msgstr "Počet dní bez úprav"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:141
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr "Minuty od předchozího vstupu (-1 pro první vstup)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:145
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr "Minuty od prvního vstupu (-1 pro první vstup)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:164
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr "Všechny následující podmínky (AND)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:165
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr "Alespoň jedna z následujících podmínek (OR)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:166
|
||||
msgid "Event start"
|
||||
msgstr "Začátek akce"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:175
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:167
|
||||
msgid "Event end"
|
||||
msgstr "Konec akce"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:176
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:168
|
||||
msgid "Event admission"
|
||||
msgstr "Vstup na akci"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:177
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:169
|
||||
msgid "custom date and time"
|
||||
msgstr "Pevný termín"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:178
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:170
|
||||
msgid "custom time"
|
||||
msgstr "Pevná doba"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:179
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:171
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr "Tolerance (v minutách)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
msgid "Add condition"
|
||||
msgstr "Přidat podmínku"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:181
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
msgid "minutes"
|
||||
msgstr "minuty"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:182
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplikát"
|
||||
|
||||
@@ -633,23 +619,23 @@ msgstr "Žádný"
|
||||
msgid "Selected only"
|
||||
msgstr "Pouze vybrané"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:791
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:783
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:786
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:952
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:944
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Interně používat jiný název"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:992
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:984
|
||||
msgid "Click to close"
|
||||
msgstr "Kliknutím zavřete"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1067
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1059
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Máte neuložené změny!"
|
||||
|
||||
@@ -833,42 +819,22 @@ msgid "Only available with a voucher"
|
||||
msgstr "K dispozici pouze s poukazem"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#, fuzzy
|
||||
#| msgid "Payment method unavailable"
|
||||
msgctxt "widget"
|
||||
msgid "Not yet available"
|
||||
msgstr "Způsob platby není k dispozici"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "currently available: %s"
|
||||
msgctxt "widget"
|
||||
msgid "Currently not available"
|
||||
msgstr "aktuálně k dispozici: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr "minimální množství pro objednávku: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr "Zavřít obchod"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr "Obchod vstupenek nemohl být načten."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||
@@ -877,17 +843,17 @@ msgstr ""
|
||||
"V současné době je v tomto obchodě s vstupenkami mnoho uživatelů. Otevřete "
|
||||
"prosím obchod v nové kartě."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "Open ticket shop"
|
||||
msgstr "Obchod vstupenek otevřit"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr "Nákupní košík se nepodařilo vytvořit. Zkuste to prosím znovu"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"We could not create your cart, since there are currently too many users in "
|
||||
@@ -897,12 +863,12 @@ msgstr ""
|
||||
"mnoho uživatelů. Klikněte prosím na \"Pokračovat\" a zkuste to znovu na nové "
|
||||
"kartě."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr "Čekací listina"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
@@ -911,93 +877,82 @@ msgstr ""
|
||||
"V současné době máte aktivní košík pro tuto událost. Pokud vyberete další "
|
||||
"produkty, budou přidány do vašeho košíku."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr "Obnovit checkout"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr "Uplatnit poukázku"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr "Uplatnit"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr "Kód poukázky"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr "Zavřít"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr "Pokračovat"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "See variations"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgctxt "widget"
|
||||
msgid "Show variants"
|
||||
msgid "See variations"
|
||||
msgstr "Zobrazit možnosti"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "See variations"
|
||||
msgctxt "widget"
|
||||
msgid "Hide variants"
|
||||
msgstr "Zobrazit možnosti"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr "Vybrat jinou událost"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different date"
|
||||
msgstr "Vybrat jiný datum"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr "Zpět"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr "Následující měsíc"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr "Předchozí měsíc"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgctxt "widget"
|
||||
msgid "Next week"
|
||||
msgstr "Příští týden"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgctxt "widget"
|
||||
msgid "Previous week"
|
||||
msgstr "Předchozí týden"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr "Otevřete výběr sedadla"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||
@@ -1008,91 +963,87 @@ msgstr ""
|
||||
"Pokud chcete, můžete se přidat na čekací listinu. Poté vás budeme "
|
||||
"informovat, pokud budou místa opět volná."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgctxt "widget"
|
||||
msgid "Load more"
|
||||
msgstr "Načíst více"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "Mo"
|
||||
msgstr "Po"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "Tu"
|
||||
msgstr "Út"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "We"
|
||||
msgstr "St"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:72
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "Th"
|
||||
msgstr "Čt"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:73
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "Fr"
|
||||
msgstr "Pá"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
msgid "Sa"
|
||||
msgstr "So"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
msgid "Su"
|
||||
msgstr "Ne"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
msgid "January"
|
||||
msgstr "Leden"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
msgid "February"
|
||||
msgstr "Únor"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||
msgid "March"
|
||||
msgstr "Březen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||
msgid "April"
|
||||
msgstr "Duben"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
msgid "May"
|
||||
msgstr "Květen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
msgid "June"
|
||||
msgstr "červen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
msgid "July"
|
||||
msgstr "Červenec"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
msgid "August"
|
||||
msgstr "Srpen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:86
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
msgid "September"
|
||||
msgstr "Září"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:87
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
msgid "October"
|
||||
msgstr "Říjen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:88
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
msgid "November"
|
||||
msgstr "Listopad"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:89
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
msgid "December"
|
||||
msgstr "Prosinec"
|
||||
|
||||
#~ msgctxt "widget"
|
||||
#~ msgid "See variations"
|
||||
#~ msgstr "Zobrazit možnosti"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Lead Scan QR kód"
|
||||
|
||||
|
||||
+2611
-2791
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-28 13:11+0000\n"
|
||||
"POT-Creation-Date: 2024-01-11 10:07+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -136,11 +136,9 @@ msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:225
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:244
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:229
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:262
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:280
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
|
||||
@@ -170,7 +168,7 @@ msgstr ""
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:291
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:236
|
||||
msgid "Contacting your bank …"
|
||||
msgstr ""
|
||||
|
||||
@@ -459,62 +457,54 @@ msgid "Number of days with a previous entry"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:133
|
||||
msgid "Number of days with a previous entry since"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Number of days with a previous entry before"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:141
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:145
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:137
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:164
|
||||
msgid "All of the conditions below (AND)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:165
|
||||
msgid "At least one of the conditions below (OR)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:166
|
||||
msgid "Event start"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:175
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:167
|
||||
msgid "Event end"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:176
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:168
|
||||
msgid "Event admission"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:177
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:169
|
||||
msgid "custom date and time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:178
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:170
|
||||
msgid "custom time"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:179
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:171
|
||||
msgid "Tolerance (minutes)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:172
|
||||
msgid "Add condition"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:181
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:173
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:182
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:174
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
@@ -610,23 +600,23 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:791
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:783
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:786
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:952
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:944
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:992
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:984
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1067
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1059
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -796,153 +786,133 @@ msgid "Only available with a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Not yet available"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Not available anymore"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Currently not available"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||
"in a new tab to continue."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "Open ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"We could not create your cart, since there are currently too many users in "
|
||||
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:45
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:53
|
||||
msgctxt "widget"
|
||||
msgid "Show variants"
|
||||
msgid "See variations"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Hide variants"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:54
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different date"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgctxt "widget"
|
||||
msgid "Next week"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgctxt "widget"
|
||||
msgid "Previous week"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"Some or all ticket categories are currently sold out. If you want, you can "
|
||||
@@ -950,83 +920,83 @@ msgid ""
|
||||
"again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgctxt "widget"
|
||||
msgid "Load more"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:67
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:72
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:68
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:73
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:69
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:70
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:71
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:74
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:75
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:76
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:77
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:78
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:79
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:80
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:81
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:86
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:82
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:87
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:83
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:88
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:84
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:89
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:85
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user