Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel 77c4cfbca8 Prepare for new docs, remove user docs as they move to own repo 2025-01-30 22:45:03 +01:00
397 changed files with 225882 additions and 266941 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"contributors": "https://crm.rami.io/cla/check/?project=pretix&checkContributor=",
"message": "Hey there! :) Thank you very much for offering a contribution to pretix! For legal reasons, we need you to sign a Contributor License Agreement in order to be able to merge the code. Sorry for the hassle :( Please download the agreement from https://pretix.eu/about/en/cla and send a signed copy to support@pretix.eu. Feel free to also contact us there or via comments here if you have any questions!\n\nFeel free to ignore me on documentation changes, translations, and trivial PRs like typo fixes (and similar single-line changes) we can merge these without a CLA as well.",
"message": "Hey there! :) Thank you very much for offering a contribution to pretix! For legal reasons, we need you to sign a Contributor License Agreement in order to be able to merge the code. Sorry for the hassle :( Please download the agreement from https://pretix.eu/about/en/cla and send a signed copy to support@pretix.eu. Feel free to also contact us there or via comments here if you have any questions!",
"label": "cla-signed"
}
+1 -1
View File
@@ -1,5 +1,5 @@
Code of Conduct
===============
We have a [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html)
We have a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html)
in place that applies to all project contributions, including issues, pull requests, etc.
+3 -3
View File
@@ -3,9 +3,9 @@ Contributing to pretix
Hey there and welcome to pretix!
* We've got a contributors guide in [our documentation](https://docs.pretix.eu/dev/development/contribution/) together with notes on the [development setup](https://docs.pretix.eu/dev/development/setup.html).
* We've got a contributors guide in [our documentation](https://docs.pretix.eu/en/latest/development/contribution/) together with notes on the [development setup](https://docs.pretix.eu/en/latest/development/setup.html).
* Please note that we have a [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html) in place that applies to all project contributions, including issues, pull requests, etc.
* Please note that we have a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html) in place that applies to all project contributions, including issues, pull requests, etc.
* Before we can accept a PR from you we'll need you to sign [our CLA](https://pretix.eu/about/en/cla). You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
* Before we can accept a PR from you we'll need you to sign [our CLA](https://pretix.eu/about/en/cla). You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/en/latest/license/faq.html#) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
+4 -4
View File
@@ -5,7 +5,7 @@ pretix
:target: https://pypi.python.org/pypi/pretix
.. image:: https://github.com/pretix/pretix/workflows/Documentation/badge.svg
:target: https://docs.pretix.eu/
:target: https://docs.pretix.eu/en/latest/
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
@@ -56,8 +56,8 @@ License
The code in this repository is covered by different licenses. Most of it is available to everyone under the terms of
the GNU AGPL license v3 with additional terms. See the LICENSE file for the complete license details.
.. _installation guide: https://docs.pretix.eu/self-hosting/installation/general/
.. _developer documentation: https://docs.pretix.eu/dev/development/index.html
.. _Code of Conduct: https://docs.pretix.eu/dev/development/contribution/codeofconduct.html
.. _installation guide: https://docs.pretix.eu/en/latest/admin/installation/index.html
.. _developer documentation: https://docs.pretix.eu/en/latest/development/index.html
.. _Code of Conduct: https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html
.. _pretix.eu: https://pretix.eu
.. _blog: https://pretix.eu/about/en/blog/
+2 -4
View File
@@ -2,11 +2,11 @@
## Reporting a vulnerability
If you discover a vulnerability with our software or server systems, please report it to us in private. Do not to attempt to harm our users, customer's data or our system's availability when looking for vulnerabilities.
If you discover a vulnerability with our software or server systems, please report it to us in private. Do not to attempt to harm our users, customer's data or our system's availability when looking for vulneratbilities.
Please contact us at security@pretix.eu with full details and steps to reproduce and allow reasonable time for us to resolve the issue before publishing your findings. If you wish to encrypt your email, you can find our GPG key [here](https://pretix.eu/.well-known/security@pretix.eu.asc).
Please also see our [Responsible disclosure policy](https://docs.pretix.eu/trust/security/disclosure/).
We're not large enough to run a formal bug bounty program, but if you find a serious vulnerability in our service, we will find a way to show our gratitude.
## Version support
@@ -18,5 +18,3 @@ subscribe to our [newsletter](https://pretix.eu/about/en/blog/) in the "News abo
category, we will also send you an email on security issues.
Past security issues are listed [on our website](https://pretix.eu/about/en/security).
Please also see our [Release cycle](https://docs.pretix.eu/trust/lifecycle/release-cycle/) documentation.
+2 -2
View File
@@ -23,7 +23,7 @@ Certificate download
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/certificate/ HTTP/1.1
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
@@ -38,7 +38,7 @@ Certificate download
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5 HTTP/1.1
GET /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/certificate/?result=1f550651-ae7b-4911-a76c-2be8f348aaa5 HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
+1 -8
View File
@@ -54,11 +54,6 @@ Checking a ticket in
this request twice with the same nonce, the second request will also succeed but will always
create only one check-in object even when the previous request was successful as well. This
allows for a certain level of idempotency and enables you to re-try after a connection failure.
:<json boolean use_order_locale: Specifies that pretix should use the customer's language (``locale`` field from the
order) when building texts (currently only the ``reason_explanation`` response field).
Defaults to ``false`` in which case the server will determine the language (currently
the event default language, might change in the future with support for the
``Accept-Language`` header).
:>json string status: ``"ok"``, ``"incomplete"``, or ``"error"``
:>json string reason: Reason code, only set on status ``"error"``, see below for possible values.
:>json string reason_explanation: Human-readable explanation, only set on status ``"error"`` and reason ``"rules"``, can be null.
@@ -67,9 +62,7 @@ Checking a ticket in
will only include check-ins for the selected list. (2) An additional boolean property
``require_attention`` will inform you whether either the order or the item have the
``checkin_attention`` flag set. (3) If ``attendee_name`` is empty, it may automatically fall
back to values from a parent product or from invoice addresses. (4) Additional properties
``order__status``, ``order__valid_if_pending``, ``order__require_approval``, and
``order__locale`` are included with details form the order for convenience.
back to values from a parent product or from invoice addresses.
:>json boolean require_attention: Whether or not the ``require_attention`` flag is set on the item or order.
:>json list checkin_texts: List of additional texts to show to the user.
:>json object list: Excerpt of information about the matching :ref:`check-in list <rest-checkinlists>` (if any was found),
-14
View File
@@ -35,10 +35,6 @@ subevent_mode strings Determines h
``"same"`` (discount is only applied for groups within
the same date), or ``"distinct"`` (discount is only applied
for groups with no two same dates).
subevent_date_from datetime The first date time of a subevent to which this discount can be applied
(or ``null``). Ignored in non-series events.
subevent_date_until datetime The last date time of a subevent to which this discount can be applied
(or ``null``). Ignored in non-series events.
condition_all_products boolean If ``true``, the discount condition applies to all items.
condition_limit_products list of integers If ``condition_all_products`` is not set, this is a list
of internal item IDs that the discount condition applies to.
@@ -109,8 +105,6 @@ Endpoints
"available_from": null,
"available_until": null,
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
"condition_all_products": true,
"condition_limit_products": [],
"condition_apply_to_addons": true,
@@ -169,8 +163,6 @@ Endpoints
"available_from": null,
"available_until": null,
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
"condition_all_products": true,
"condition_limit_products": [],
"condition_apply_to_addons": true,
@@ -215,8 +207,6 @@ Endpoints
"available_from": null,
"available_until": null,
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
"condition_all_products": true,
"condition_limit_products": [],
"condition_apply_to_addons": true,
@@ -250,8 +240,6 @@ Endpoints
"available_from": null,
"available_until": null,
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
"condition_all_products": true,
"condition_limit_products": [],
"condition_apply_to_addons": true,
@@ -314,8 +302,6 @@ Endpoints
"available_from": null,
"available_until": null,
"subevent_mode": "mixed",
"subevent_date_from": null,
"subevent_date_until": null,
"condition_all_products": true,
"condition_limit_products": [],
"condition_apply_to_addons": true,
-13
View File
@@ -69,10 +69,6 @@ hidden_if_available integer **DEPRECATED*
hidden_if_item_available integer The internal ID of a different item, or ``null``. If
set, this item won't be shown publicly as long as this
other item is available.
hidden_if_item_available_mode string If ``hide`` (the default), this item is hidden in the shop
if unavailable due to the ``hidden_if_item_available`` setting.
If ``info``, the item is visible, but can't be purchased,
and a note explaining the unavailability is displayed.
require_voucher boolean If ``true``, this item can only be bought using a
voucher that is specifically assigned to this item.
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
@@ -243,10 +239,6 @@ meta_data object Values set fo
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
deprecated.
.. versionchanged:: 2025.01
The ``hidden_if_item_available_mode`` attributes has been added.
Notes
-----
@@ -316,7 +308,6 @@ Endpoints
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -468,7 +459,6 @@ Endpoints
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -599,7 +589,6 @@ Endpoints
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -716,7 +705,6 @@ Endpoints
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false,
"hide_without_voucher": false,
"allow_cancel": true,
@@ -867,7 +855,6 @@ Endpoints
"available_until_mode": "hide",
"hidden_if_available": null,
"hidden_if_item_available": null,
"hidden_if_item_available_mode": "hide",
"require_voucher": false,
"hide_without_voucher": false,
"generate_tickets": null,
+20 -54
View File
@@ -75,9 +75,8 @@ positions list of objects List of order p
fees list of objects List of fees included in the order total. By default, only
non-canceled fees are included.
├ id integer Internal ID of the fee record
├ fee_type string Type of fee (currently ``payment``, ``shipping``,
``service``, ``cancellation``, ``insurance``, ``late``,
``other``, ``giftcard``)
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
``other``)
├ value money (string) Fee amount
├ description string Human-readable string with more details (can be empty)
├ internal_type string Internal string (i.e. ID of the payment provider),
@@ -110,7 +109,6 @@ cancellation_date datetime Time of order c
Will not be set for partial cancellations and is not
reliable for orders that have been cancelled,
reactivated and cancelled again.
plugin_data object Additional data added by plugins.
===================================== ========================== =======================================================
@@ -166,10 +164,6 @@ plugin_data object Additional data
The ``tax_code`` attribute has been added.
.. versionchanged:: 2025.2
The ``plugin_data`` attribute has been added.
.. _order-position-resource:
Order position resource
@@ -257,7 +251,6 @@ seat objects The assigned se
pdf_data object Data object required for ticket PDF generation. By default,
this field is missing. It will be added only if you add the
``pdf_data=true`` query parameter to your request.
plugin_data object Additional data added by plugins.
===================================== ========================== =======================================================
.. versionchanged:: 4.16
@@ -272,10 +265,6 @@ plugin_data object Additional data
The ``tax_code`` attribute has been added.
.. versionchanged:: 2025.2
The ``plugin_data`` attribute has been added.
.. _order-payment-resource:
Order payment resource
@@ -472,8 +461,7 @@ List of all orders
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
],
"plugin_data": {}
]
}
],
"downloads": [
@@ -495,8 +483,7 @@ List of all orders
}
],
"refunds": [],
"cancellation_date": null,
"plugin_data": {}
"cancellation_date": null
}
]
}
@@ -715,8 +702,7 @@ Fetching individual orders
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
],
"plugin_data": {}
]
}
],
"downloads": [
@@ -738,8 +724,7 @@ Fetching individual orders
}
],
"refunds": [],
"cancellation_date": null,
"plugin_data": {}
"cancellation_date": null
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -1686,8 +1671,7 @@ List of all order positions
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
],
"plugin_data": {}
]
}
]
}
@@ -1814,8 +1798,7 @@ Fetching individual positions
"output": "pdf",
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
}
],
"plugin_data": {}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -1943,14 +1926,9 @@ Manipulating individual positions
* ``valid_until``
* ``secret``
Changing parameters such as ``item`` or ``price`` will **not** automatically trigger creation of a new invoice,
you need to take care of that yourself.
Changing ``secret`` does not cause a new PDF ticket to be sent to the customer, nor does it cause the old secret
to be added to the revocation list, even if your ticket generator uses one.
**Example request**:
.. sourcecode:: http
@@ -2250,9 +2228,6 @@ otherwise, such as splitting an order or changing fees.
* ``cancel_fees``: A list of objects with the single key ``fee`` specifying an order fee ID.
* ``create_fees``: A list of objects describing new order fees with the fields ``fee_type``, ``value``, ``description``,
``internal_type``, ``tax_rule``
* ``recalculate_taxes``: If set to ``"keep_net"``, all taxes will be recalculated based on the tax rule and invoice
address, the net price will be kept. If set to ``"keep_gross"``, the gross price will be kept. If set to ``null``
(the default) the taxes are not recalculated.
@@ -2272,12 +2247,17 @@ otherwise, such as splitting an order or changing fees.
Content-Type: application/json
{
"cancel_positions": [
{
"position": 12373
}
],
"patch_positions": [
{
"position": 12374,
"body": {
"item": 12,
"variation": null,
"variation": None,
"subevent": 562,
"seat": "seat-guid-2",
"price": "99.99",
@@ -2285,11 +2265,6 @@ otherwise, such as splitting an order or changing fees.
}
}
],
"cancel_positions": [
{
"position": 12373
}
],
"split_positions": [
{
"position": 12375
@@ -2298,7 +2273,7 @@ otherwise, such as splitting an order or changing fees.
"create_positions": [
{
"item": 12,
"variation": null,
"variation": None,
"subevent": 562,
"seat": "seat-guid-2",
"price": "99.99",
@@ -2306,26 +2281,17 @@ otherwise, such as splitting an order or changing fees.
"attendee_name": "Peter",
}
],
"patch_fees": [
{
"fee": 51,
"body": {
"value": "12.00"
}
}
],
"cancel_fees": [
{
"fee": 49
}
],
"create_fees": [
"change_fees": [
{
"fee_type": "other",
"value": "1.50",
"description": "Example Fee",
"internal_type": "",
"tax_rule": 15
"fee": 51,
"body": {
"value": "12.00"
}
}
],
"reissue_invoice": true,
+2 -2
View File
@@ -23,7 +23,7 @@ There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals
:no-index:
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins
"""""""""
@@ -103,4 +103,4 @@ API
.. automodule:: pretix.api.signals
:no-index:
:members: register_device_security_profile, order_api_details, orderposition_api_details
:members: register_device_security_profile
-24
View File
@@ -84,8 +84,6 @@ A working example would be:
restricted = False
description = _("This plugin allows you to receive payments via PayPal")
compatibility = "pretix>=2.7.0"
settings_links = []
navigation_links = []
default_app_config = 'pretix_paypal.PaypalApp'
@@ -187,28 +185,6 @@ your Django app label.
with checking that the calling user is logged in, has appropriate permissions,
etc. We plan on providing native support for this in a later version.
To make your plugin views easily discoverable, you can specify links for "Go to"
and "Settings" buttons next to your entry on the plugin page. These links should be
added to the ``navigation_links`` and ``settings_links``, respectively, in the
``PretixPluginMeta`` class.
Each array entry consists of a tuple ``(label, urlname, kwargs)``. For the label,
either a string or a tuple of strings can be specified. In the latter case, the provided
strings will be merged with a separator indicating they are successive navigation steps
the user would need to take to reach the page via the regular menu
(e.g. "Payment > Bank transfer" as below).
.. code-block:: python
settings_links = [
((_("Payment"), _("Bank transfer")), "control:event.settings.payment.provider", {"provider": "banktransfer"}),
]
navigation_links = [
((_("Bank transfer"), _("Import bank data")), "plugins:banktransfer:import", {}),
((_("Bank transfer"), _("Export refunds")), "plugins:banktransfer:refunds.list", {}),
]
.. _Django app: https://docs.djangoproject.com/en/3.0/ref/applications/
.. _signal dispatcher: https://docs.djangoproject.com/en/3.0/topics/signals/
.. _namespace packages: https://legacy.python.org/dev/peps/pep-0420/
+20 -20
View File
@@ -28,18 +28,18 @@ classifiers = [
dependencies = [
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.13.*",
"BeautifulSoup4==4.12.*",
"bleach==6.2.*",
"celery==5.5.*",
"celery==5.4.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"css-inline==0.14.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.15",
"django-bootstrap3==25.1",
"django-bootstrap3==24.3",
"django-compressor==4.5.1",
"django-countries==7.6.*",
"django-filter==25.1",
"django-filter==24.3",
"django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1",
"django-hierarkey==1.2.*",
@@ -49,22 +49,22 @@ dependencies = [
"django-localflavor==4.0",
"django-markup",
"django-oauth-toolkit==2.3.*",
"django-otp==1.6.*",
"django-otp==1.5.*",
"django-phonenumber-field==7.3.*",
"django-redis==5.4.*",
"django-scopes==2.0.*",
"django-statici18n==2.6.*",
"djangorestframework==3.16.*",
"djangorestframework==3.15.*",
"dnspython==2.7.*",
"drf_ujson2==1.7.*",
"geoip2==5.*",
"geoip2==4.*",
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.5.*",
"kombu==5.4.*",
"libsass==0.23.*",
"lxml",
"markdown==3.8", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.7", # 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,25 +73,25 @@ dependencies = [
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.10.*",
"phonenumberslite==9.0.*",
"Pillow==11.2.*",
"phonenumberslite==8.13.*",
"Pillow==11.1.*",
"pretix-plugin-build",
"protobuf==6.30.*",
"protobuf==5.29.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
"pycryptodome==3.23.*",
"pypdf==5.4.*",
"pycryptodome==3.21.*",
"pypdf==5.1.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==8.2",
"qrcode==8.0",
"redis==5.2.*",
"reportlab==4.4.*",
"reportlab==4.2.*",
"requests==2.31.*",
"sentry-sdk==2.29.*",
"sentry-sdk==2.20.*",
"sepaxml==2.6.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -111,10 +111,10 @@ dev = [
"coverage",
"coveralls",
"fakeredis==2.26.*",
"flake8==7.2.*",
"flake8==7.1.*",
"freezegun",
"isort==6.0.*",
"pep8-naming==0.15.*",
"isort==5.13.*",
"pep8-naming==0.14.*",
"potypo",
"pytest-asyncio>=0.24",
"pytest-cache",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2025.5.0"
__version__ = "2024.12.0.dev0"
+3 -6
View File
@@ -93,7 +93,6 @@ ALL_LANGUAGES = [
('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')),
('cs', _('Czech')),
('hr', _('Croatian')),
('da', _('Danish')),
('nl', _('Dutch')),
('nl-informal', _('Dutch (informal)')),
@@ -101,7 +100,6 @@ ALL_LANGUAGES = [
('fi', _('Finnish')),
('gl', _('Galician')),
('el', _('Greek')),
('he', _('Hebrew')),
('id', _('Indonesian')),
('it', _('Italian')),
('ja', _('Japanese')),
@@ -122,11 +120,10 @@ LANGUAGES_OFFICIAL = {
'en', 'de', 'de-informal'
}
LANGUAGES_RTL = {
# When adding more right-to-left languages, also update pretix/static/pretixbase/scss/_rtl.scss
'ar', 'he'
'ar', 'hw'
}
LANGUAGES_INCUBATING = {
'pt-br', 'gl',
'fi', 'pt-br', 'gl',
}
LANGUAGES = ALL_LANGUAGES
LOCALE_PATHS = [
@@ -261,7 +258,7 @@ COMPRESS_FILTERS = {
CURRENCIES = [
c for c in currencies
if c.alpha_3 not in {
'USN', 'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
}
]
CURRENCY_PLACES = {
+1 -9
View File
@@ -81,13 +81,6 @@ class SalesChannelMigrationMixin:
def to_internal_value(self, data):
if "sales_channels" in data:
if data["sales_channels"] is None:
raise ValidationError({
"sales_channels": [
"The legacy attribute 'sales_channels' cannot be set to None, it must be a list."
]
})
prefetch_related_objects([self.organizer], "sales_channels")
all_channels = {
s.identifier for s in
@@ -96,7 +89,7 @@ class SalesChannelMigrationMixin:
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
raise ValidationError({
"all_sales_channels": [
"limit_sales_channels": [
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
"the list of all sales channels."
]
@@ -116,7 +109,6 @@ class SalesChannelMigrationMixin:
else:
data["all_sales_channels"] = False
data["limit_sales_channels"] = data["sales_channels"]
del data["sales_channels"]
if data.get("all_sales_channels"):
+1 -1
View File
@@ -176,7 +176,7 @@ class BaseCartPositionCreateSerializer(I18nAwareModelSerializer):
def create(self, validated_data):
validated_data.pop('_quotas')
answers_data = validated_data.pop('answers', [])
answers_data = validated_data.pop('answers')
attendee_name = validated_data.pop('attendee_name', '')
if attendee_name and not validated_data.get('attendee_name_parts'):
-1
View File
@@ -84,7 +84,6 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=Checkin.CHECKIN_TYPES, default=Checkin.TYPE_ENTRY)
ignore_unpaid = serializers.BooleanField(default=False, required=False)
questions_supported = serializers.BooleanField(default=True, required=False)
use_order_locale = serializers.BooleanField(default=False, required=False)
nonce = serializers.CharField(required=False, allow_null=True)
datetime = serializers.DateTimeField(required=False, allow_null=True)
answers = serializers.JSONField(required=False, allow_null=True)
+5 -6
View File
@@ -38,12 +38,11 @@ class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
class Meta:
model = Discount
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
'available_from', 'available_until', 'subevent_mode', 'subevent_date_from', 'subevent_date_until',
'condition_all_products', 'condition_limit_products', 'condition_apply_to_addons',
'condition_min_count', 'condition_min_value', 'benefit_discount_matching_percent',
'benefit_only_apply_to_cheapest_n_matches', 'benefit_same_products', 'benefit_limit_products',
'benefit_apply_to_addons', 'benefit_ignore_voucher_discounted',
'condition_ignore_voucher_discounted')
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-6
View File
@@ -378,8 +378,6 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
if prop.name not in meta_data:
current_object.delete()
instance._prefetched_objects_cache.clear()
# Item Meta properties
if item_meta_properties is not None:
current = list(event.item_meta_properties.all())
@@ -400,8 +398,6 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
if prop.name not in list(item_meta_properties.keys()):
prop.delete()
instance._prefetched_objects_cache.clear()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
current_mappings = {
@@ -934,8 +930,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
'order_phone_asked',
'order_phone_required',
'name_scheme',
'reusable_media_type_barcode',
'reusable_media_type_nfc_uid',
+1 -1
View File
@@ -272,7 +272,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
'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',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days',
+4 -35
View File
@@ -46,7 +46,6 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.item import (
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
)
from pretix.api.signals import order_api_details, orderposition_api_details
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
@@ -495,18 +494,6 @@ class OrderPositionListSerializer(serializers.ListSerializer):
return data
class OrderPositionPluginDataField(serializers.Field):
def to_representation(self, value: OrderPosition):
d = {}
if value and value.pk:
for recv, resp in orderposition_api_details.send(
sender=self.context.get("event") or value.order.event,
orderposition=value
):
d.update(resp)
return d
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True)
print_logs = PrintLogSerializer(many=True, read_only=True)
@@ -517,7 +504,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
seat = InlineSeatSerializer(read_only=True)
country = CompatibleCountryField(source='*')
attendee_name = serializers.CharField(required=False)
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
class Meta:
list_serializer_class = OrderPositionListSerializer
@@ -527,7 +513,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use', 'plugin_data')
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id',
@@ -607,7 +593,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
order__locale = serializers.SlugRelatedField(read_only=True, slug_field='locale', source='order')
class Meta:
model = OrderPosition
@@ -616,7 +601,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
'order__locale', 'valid_from', 'valid_until', 'blocked')
'valid_from', 'valid_until', 'blocked')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -745,18 +730,6 @@ class OrderListSerializer(serializers.ListSerializer):
return data
class OrderPluginDataField(serializers.Field):
def to_representation(self, value: Order):
d = {}
if value and value.pk:
for recv, resp in order_api_details.send(
sender=self.context.get("event") or value.event,
order=value
):
d.update(resp)
return d
class OrderSerializer(I18nAwareModelSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
invoice_address = InvoiceAddressSerializer(allow_null=True)
@@ -774,7 +747,6 @@ class OrderSerializer(I18nAwareModelSerializer):
queryset=SalesChannel.objects.none(),
required=False,
)
plugin_data = OrderPluginDataField(source='*', allow_null=True, read_only=True)
class Meta:
model = Order
@@ -783,7 +755,7 @@ class OrderSerializer(I18nAwareModelSerializer):
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data',
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
)
read_only_fields = (
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
@@ -1103,7 +1075,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
queryset=SalesChannel.objects.none(),
required=False,
)
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1111,7 +1082,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
self.fields['expires'].required = False
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
self.fields["locale"].choices = self.context['event'].settings.locales
class Meta:
model = Order
@@ -1519,8 +1489,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
self.context['event'],
order.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
for cp in order_positions
]
)
+2 -57
View File
@@ -30,7 +30,7 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField,
OrderFeeCreateSerializer, OrderPositionCreateSerializer,
OrderPositionCreateSerializer,
)
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
from pretix.base.services.orders import OrderError
@@ -104,54 +104,6 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
raise ValidationError(str(e))
class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
order = serializers.SlugRelatedField(slug_field='code', queryset=Order.objects.none(), required=True, allow_null=False)
value = serializers.DecimalField(required=True, allow_null=False, decimal_places=2,
max_digits=13)
internal_type = serializers.CharField(required=False, default="")
class Meta:
model = OrderFee
fields = ('order', 'fee_type', 'value', 'description', 'internal_type', 'tax_rule')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.context:
return
self.fields['order'].queryset = self.context['event'].orders.all()
self.fields['tax_rule'].queryset = self.context['event'].tax_rules.all()
if 'order' in self.context:
del self.fields['order']
def validate(self, data):
data = super().validate(data)
if 'order' in self.context:
data['order'] = self.context['order']
return data
def create(self, validated_data):
ocm = self.context['ocm']
try:
f = OrderFee(
order=validated_data['order'],
fee_type=validated_data['fee_type'],
value=validated_data.get('value'),
description=validated_data.get('description'),
internal_type=validated_data.get('internal_type'),
tax_rule=validated_data.get('tax_rule'),
)
f._calculate_tax()
ocm.add_fee(f)
if self.context.get('commit', True):
ocm.commit()
return validated_data['order'].fees.order_by('-pk').first()
else:
return OrderFee() # fake to appease DRF
except OrderError as e:
raise ValidationError(str(e))
class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
answers = AnswerSerializer(many=True)
country = CompatibleCountryField(source='*')
@@ -251,7 +203,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
class Meta:
model = OrderPosition
fields = (
'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule', 'valid_from', 'valid_until', 'secret'
'item', 'variation', 'subevent', 'seat', 'price', 'tax_rule', 'valid_from', 'valid_until'
)
def __init__(self, *args, **kwargs):
@@ -319,7 +271,6 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
tax_rule = validated_data.get('tax_rule', instance.tax_rule)
valid_from = validated_data.get('valid_from', instance.valid_from)
valid_until = validated_data.get('valid_until', instance.valid_until)
secret = validated_data.get('secret', instance.secret)
change_item = None
if item != instance.item or variation != instance.variation:
@@ -352,9 +303,6 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
if valid_until != instance.valid_until:
ocm.change_valid_until(instance, valid_until)
if secret != instance.secret:
ocm.change_ticket_secret(instance, secret)
if self.context.get('commit', True):
ocm.commit()
instance.refresh_from_db()
@@ -453,9 +401,6 @@ class OrderChangeOperationSerializer(serializers.Serializer):
self.fields['split_positions'] = SelectPositionSerializer(
many=True, required=False, context=self.context
)
self.fields['create_fees'] = OrderFeeCreateForExistingOrderSerializer(
many=True, required=False, context=self.context
)
self.fields['patch_fees'] = PatchFeeSerializer(
many=True, required=False, context=self.context
)
-5
View File
@@ -22,7 +22,6 @@
import logging
from decimal import Decimal
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.utils.crypto import get_random_string
@@ -78,7 +77,6 @@ class CustomerSerializer(I18nAwareModelSerializer):
last_login = serializers.DateTimeField(read_only=True)
date_joined = serializers.DateTimeField(read_only=True)
last_modified = serializers.DateTimeField(read_only=True)
locale = serializers.ChoiceField(choices=settings.LANGUAGES, default='en')
class Meta:
model = Customer
@@ -426,9 +424,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'organizer_logo_image_inherit',
'organizer_logo_image',
'privacy_url',
'accessibility_url',
'accessibility_title',
'accessibility_text',
'cookie_consent',
'cookie_consent_dialog_title',
'cookie_consent_dialog_text',
@@ -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 rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
@@ -27,17 +26,12 @@ from pretix.base.models import WaitingListEntry
class WaitingListSerializer(I18nAwareModelSerializer):
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
class Meta:
model = WaitingListEntry
fields = ('id', 'created', 'name', 'name_parts', 'email', 'phone', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
read_only_fields = ('id', 'created', 'voucher')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["locale"].choices = self.context['event'].settings.locales
def validate(self, data):
data = super().validate(data)
event = self.context['event']
@@ -66,7 +60,4 @@ class WaitingListSerializer(I18nAwareModelSerializer):
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
data['name_parts']['_scheme'] = event.settings.name_scheme
if data.get('locale', None) is None:
data['locale'] = event.settings.locale
return data
+1 -23
View File
@@ -26,7 +26,7 @@ from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.api.models import ApiCall, WebHookCall
from pretix.base.signals import EventPluginSignal, periodic_task
from pretix.base.signals import periodic_task
from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal()
@@ -43,28 +43,6 @@ return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurit
or a list of such instances.
"""
order_api_details = EventPluginSignal()
"""
Arguments: ``order``
This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers
should return a dictionary that is combined with the dictionaries of all other plugins.
Note that doing database or network queries in receivers to this signal is discouraged
and could cause serious performance issues. The main purpose is to provide information
from e.g. ``meta_info`` to the API consumer,
"""
orderposition_api_details = EventPluginSignal()
"""
Arguments: ``orderposition``
This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers
should return a dictionary that is combined with the dictionaries of all other plugins.
Note that doing database or network queries in receivers to this signal is discouraged
and could cause serious performance issues. The main purpose is to provide information
from e.g. ``meta_info`` to the API consumer,
"""
@receiver(periodic_task)
@scopes_disabled()
+2 -8
View File
@@ -165,7 +165,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
if not serializer.validated_data.get('position'):
kwargs['position'] = OrderPosition.all.filter(
order__event=self.request.event,
secret=serializer.validated_data['raw_barcode']
).first()
@@ -420,7 +419,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False):
source_type='barcode', legacy_url_support=False, simulate=False, gate=None):
if not checkinlists:
raise ValidationError('No check-in list passed.')
@@ -694,11 +693,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
pass
# 6. Pass to our actual check-in logic
if use_order_locale:
locale = op.order.locale
else:
locale = op.order.event.settings.locale
with language(locale):
with language(op.order.event.settings.locale):
try:
perform_checkin(
op=op,
@@ -913,7 +908,6 @@ class CheckinRPCRedeemView(views.APIView):
expand=self.request.query_params.getlist('expand'),
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
questions_supported=s.validated_data['questions_supported'],
use_order_locale=s.validated_data['use_order_locale'],
canceled_supported=True,
request=self.request, # this is not clean, but we need it in the serializers for URL generation
legacy_url_support=False,
+7 -13
View File
@@ -63,8 +63,7 @@ from pretix.api.serializers.order import (
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
OrderFeeChangeSerializer, OrderFeeCreateForExistingOrderSerializer,
OrderPositionChangeSerializer,
OrderFeeChangeSerializer, OrderPositionChangeSerializer,
OrderPositionCreateForExistingOrderSerializer,
OrderPositionInfoPatchSerializer,
)
@@ -185,7 +184,7 @@ with scopes_disabled():
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
matching_positions = OrderPosition.all.filter(
matching_positions = OrderPosition.objects.filter(
Q(order=OuterRef('pk')) & Q(
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
| Q(secret__istartswith=u)
@@ -452,9 +451,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
comment = request.data.get('comment', None)
cancellation_fee = request.data.get('cancellation_fee', None)
if cancellation_fee:
cancellation_fee = serializers.DecimalField(max_digits=13, decimal_places=2).to_internal_value(
cancellation_fee,
)
try:
cancellation_fee = float(Decimal(cancellation_fee))
except:
cancellation_fee = None
order = self.get_object()
if not order.cancel_allowed():
@@ -707,7 +707,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
)
def create(self, request, *args, **kwargs):
if 'send_mail' in request.data and 'send_email' not in request.data and isinstance(request.data, dict):
if 'send_mail' in request.data and 'send_email' not in request.data:
request.data['send_email'] = request.data['send_mail']
serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
@@ -988,12 +988,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
ocm.cancel_fee(r['fee'])
canceled_fees.add(r['fee'])
for r in serializer.validated_data.get('create_fees', []):
pos_serializer = OrderFeeCreateForExistingOrderSerializer(
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
)
pos_serializer.create(r)
for r in serializer.validated_data.get('patch_fees', []):
if r['fee'] in canceled_fees:
continue
+11 -14
View File
@@ -35,22 +35,19 @@ def get_powered_by(request, safelink=True):
d = gs.settings.license_check_input
if d.get('poweredby_name'):
if d.get('poweredby_url'):
msg = gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>').format(
name=d['poweredby_name'],
a_name_attr='href="{}" target="_blank" rel="noopener"'.format(
sl(d['poweredby_url']) if safelink else d['poweredby_url'],
),
a_attr='href="{}" target="_blank" rel="noopener"'.format(
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
)
n = '<a href="{}" target="_blank" rel="noopener">{}</a>'.format(
sl(d['poweredby_url']) if safelink else d['poweredby_url'],
d['poweredby_name']
)
else:
msg = gettext('<a {a_attr}>powered by {name} based on pretix</a>').format(
name=d['poweredby_name'],
a_attr='href="{}" target="_blank" rel="noopener"'.format(
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
)
n = d['poweredby_name']
msg = gettext('powered by {name} based on <a {a_attr}>pretix</a>').format(
name=n,
a_attr='href="{}" target="_blank" rel="noopener"'.format(
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
)
)
else:
msg = gettext('<a %(a_attr)s>ticketing powered by pretix</a>') % {
'a_attr': 'href="{}" target="_blank" rel="noopener"'.format(
@@ -74,7 +71,7 @@ def contextprocessor(request):
try:
ctx['poweredby'] = get_powered_by(request, safelink=True)
except Exception:
ctx['poweredby'] = '<a href="https://pretix.eu/" target="_blank" rel="noopener">powered by pretix</a>'
ctx['poweredby'] = 'powered by <a href="https://pretix.eu/" target="_blank" rel="noopener">pretix</a>'
if settings.DEBUG and 'runserver' not in sys.argv:
ctx['debug_warning'] = True
elif 'runserver' in sys.argv:
+3 -19
View File
@@ -24,7 +24,7 @@ import hashlib
import logging
import time
from datetime import datetime
from urllib.parse import parse_qsl, urlencode, urljoin
from urllib.parse import urlencode, urljoin
import jwt
import requests
@@ -139,16 +139,11 @@ def oidc_validate_and_complete_config(config):
)
)
if "query_parameters" in config and config["query_parameters"]:
config["query_parameters"] = urlencode(
parse_qsl(config["query_parameters"])
)
config['provider_config'] = provider_config
return config
def oidc_authorize_url(provider, state, redirect_uri, pkce_code_verifier):
def oidc_authorize_url(provider, state, redirect_uri):
endpoint = provider.configuration['provider_config']['authorization_endpoint']
params = {
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
@@ -159,18 +154,10 @@ def oidc_authorize_url(provider, state, redirect_uri, pkce_code_verifier):
'state': state,
'redirect_uri': redirect_uri,
}
if "query_parameters" in provider.configuration and provider.configuration["query_parameters"]:
params.update(parse_qsl(provider.configuration["query_parameters"]))
if pkce_code_verifier and "S256" in provider.configuration['provider_config'].get('code_challenge_methods_supported', []):
params["code_challenge"] = base64.urlsafe_b64encode(hashlib.sha256(pkce_code_verifier.encode()).digest()).decode().rstrip("=")
params["code_challenge_method"] = "S256"
return endpoint + '?' + urlencode(params)
def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier):
def oidc_validate_authorization(provider, code, redirect_uri):
endpoint = provider.configuration['provider_config']['token_endpoint']
# Wall of shame and RFC ignorant IDPs
@@ -192,9 +179,6 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
'redirect_uri': redirect_uri,
}
if pkce_code_verifier and "S256" in provider.configuration['provider_config'].get('code_challenge_methods_supported', []):
params["code_verifier"] = pkce_code_verifier
if token_endpoint_auth_method == 'client_secret_post':
params['client_id'] = provider.configuration['client_id']
params['client_secret'] = provider.configuration['client_secret']
+1 -1
View File
@@ -712,7 +712,7 @@ class OrderListExporter(MultiSheetListExporter):
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
get_name_parts_localized(op.attendee_name_parts, k) if op.attendee_name_parts else ''
get_name_parts_localized(op.attendee_name_parts, k)
)
row += [
op.attendee_email,
+1 -5
View File
@@ -108,10 +108,8 @@ class WaitingListExporter(ListExporter):
_('Name'),
_('Email'),
_('Phone number'),
_('Product'),
_('Product ID'),
_('Product name'),
_('Variation'),
_('Variation ID'),
_('Event slug'),
_('Event name'),
pgettext_lazy('subevents', 'Date'), # Name of subevent
@@ -148,9 +146,7 @@ class WaitingListExporter(ListExporter):
entry.email,
entry.phone,
str(entry.item) if entry.item else "",
str(entry.item.pk) if entry.item else "",
str(entry.variation) if entry.variation else "",
str(entry.variation.pk) if entry.variation else "",
entry.event.slug,
entry.event.name,
entry.subevent.name if entry.subevent else "",
+3 -3
View File
@@ -93,7 +93,7 @@ class MarkdownTextarea(forms.Textarea):
'<div class="i18n-form-group">%s<div class="i18n-field-markdown-note">%s</div></div>' % (
super()._render(template_name, context, renderer=None),
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/guides/markdown/" target="_blank">Markdown</a>'
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
)
@@ -104,7 +104,7 @@ class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/guides/markdown/" target="_blank">Markdown</a>'
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
@@ -116,7 +116,7 @@ class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
rendered_widgets = rendered_widgets + [
'<div class="i18n-field-markdown-note">%s</div>' % (
_("You can use {markup_name} in this field.").format(
markup_name='<a href="https://docs.pretix.eu/guides/markdown/" target="_blank">Markdown</a>'
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
)
)
]
+13 -59
View File
@@ -58,7 +58,6 @@ from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
from django.utils.timezone import get_current_timezone
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import countries
@@ -84,8 +83,8 @@ from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
@@ -128,13 +127,7 @@ class NamePartsWidget(forms.MultiWidget):
if fname == 'title' and self.titles:
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
elif fname == 'salutation':
widgets.append(Select(
attrs=a,
choices=[
('', '---'),
('empty', '({})'.format(pgettext_lazy("name_salutation", "not specified"))),
] + PERSON_NAME_SALUTATIONS
))
widgets.append(Select(attrs=a, choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS))
else:
widgets.append(self.widget(attrs=a))
super().__init__(widgets, attrs)
@@ -252,10 +245,7 @@ class NamePartsFormField(forms.MultiValueField):
d.pop('validators', None)
field = forms.ChoiceField(
**d,
choices=[
('', '---'),
('empty', '({})'.format(pgettext_lazy("name_salutation", "not specified"))),
] + PERSON_NAME_SALUTATIONS
choices=[('', '---'), ('empty', '')] + PERSON_NAME_SALUTATIONS
)
else:
field = forms.CharField(**defaults)
@@ -731,7 +721,7 @@ class BaseQuestionsForm(forms.Form):
'data-country-information-url': reverse('js_helpers.states'),
}),
)
c = [('', '---')]
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else ''
cc = None
state = None
@@ -871,23 +861,6 @@ class BaseQuestionsForm(forms.Form):
attrs['data-min'] = q.valid_date_min.isoformat()
if q.valid_date_max:
attrs['data-max'] = q.valid_date_max.isoformat()
if not help_text:
if q.valid_date_min and q.valid_date_max:
help_text = format_lazy(
'Please enter a date between {min} and {max}.',
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
)
elif q.valid_date_min:
help_text = format_lazy(
'Please enter a date no earlier than {min}.',
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
)
elif q.valid_date_max:
help_text = format_lazy(
'Please enter a date no later than {max}.',
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
)
field = forms.DateField(
label=label, required=required,
help_text=help_text,
@@ -906,23 +879,6 @@ class BaseQuestionsForm(forms.Form):
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
if not help_text:
if q.valid_datetime_min and q.valid_datetime_max:
help_text = format_lazy(
'Please enter a date and time between {min} and {max}.',
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
)
elif q.valid_datetime_min:
help_text = format_lazy(
'Please enter a date and time no earlier than {min}.',
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
)
elif q.valid_datetime_max:
help_text = format_lazy(
'Please enter a date and time no later than {max}.',
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
)
field = SplitDateTimeField(
label=label, required=required,
help_text=help_text,
@@ -1069,9 +1025,10 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'autocomplete': 'address-level2',
}),
'company': forms.TextInput(attrs={
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -1102,8 +1059,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs)
self.fields["company"].widget.attrs["data-display-dependency"] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
self.fields["company"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
if not self.ask_vat_id:
del self.fields['vat_id']
@@ -1123,7 +1080,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['country'].choices = CachedCountries()
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
c = [('', '---')]
c = [('', pgettext_lazy('address', 'Select state'))]
fprefix = self.prefix + '-' if self.prefix else ''
cc = None
if fprefix + 'country' in self.data:
@@ -1132,19 +1089,16 @@ class BaseInvoiceAddressForm(forms.ModelForm):
cc = str(self.initial['country'])
elif self.instance and self.instance.country:
cc = str(self.instance.country)
state_label = pgettext_lazy('address', 'State')
if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1])
if cc in COUNTRY_STATE_LABEL:
state_label = COUNTRY_STATE_LABEL[cc]
elif fprefix + 'state' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'state']
self.fields['state'] = forms.ChoiceField(
label=state_label,
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
widget=forms.Select(attrs={
@@ -1187,9 +1141,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
)
if self.address_required and not self.company_required and not self.all_optional:
if not event.settings.invoice_name_required:
self.fields['name_parts'].widget.attrs['data-required-if'] = f'input[name="{self.add_prefix("is_business")}"][value="individual"]'
self.fields['name_parts'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_0'
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
self.fields['company'].widget.attrs['data-required-if'] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
self.fields['company'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_1'
if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary']
+1 -57
View File
@@ -62,58 +62,6 @@ from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
def addon_aware_groupby(iterable, key, is_addon):
"""
We use groupby() to visually group identical lines on an invoice. For example, instead of
Product 1 5.00 EUR
Product 1 5.00 EUR
Product 1 5.00 EUR
Product 2 7.00 EUR
We want to print
3x Product 1 5.00 EUR = 15.00 EUR
Product 2 7.00 EUR
However, this fails for setups with addon-products since groupby() only groups consecutive
lines with the same identity. So in
Product 1 5.00 EUR
+ Addon 1 2.00 EUR
Product 1 5.00 EUR
+ Addon 1 2.00 EUR
Product 1 5.00 EUR
+ Addon 2 3.00 EUR
There is no consecutive repetition of the same entity. This function provides a specialised groupby which
understands the product/addon relationship and packs groups of these addons together if they are, in fact,
identical groups:
2x Product 1 5.00 EUR = 10.00 EUR
+ 2x Addon 1 2.00 EUR = 4.00 EUR
Product 1 5.00 EUR
+ Addon 2 3.00 EUR
"""
packed_groups = []
for i in iterable:
if is_addon(i):
packed_groups[-1].append(i)
else:
packed_groups.append([i])
# Each packed_groups element contains a list with the parent product as first element, and any addon products following
def _reorder(packed_groups):
# Emit the products as individual products again, reordered by "all parent products, then all addon products"
# within each group.
for _, repeated_groups in groupby(packed_groups, key=lambda g: tuple(key(a) for a in g)):
for repeated_items in zip(*repeated_groups):
yield from repeated_items
return groupby(_reorder(packed_groups), key)
class NumberedCanvas(Canvas):
def __init__(self, *args, **kwargs):
self.font_regular = kwargs.pop('font_regular')
@@ -696,11 +644,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
line.event_date_from, line.event_date_to)
total = Decimal('0.00')
for (description, tax_rate, tax_name, net_value, gross_value, *ignored), lines in addon_aware_groupby(
self.invoice.lines.all(),
key=_group_key,
is_addon=lambda l: l.description.startswith(" +"),
):
for (description, tax_rate, tax_name, net_value, gross_value, *ignored), lines in groupby(self.invoice.lines.all(), key=_group_key):
lines = list(lines)
if has_taxes:
if len(lines) > 1:
-165
View File
@@ -1,165 +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 collections import defaultdict
from typing import Optional
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from pretix.base.signals import EventPluginRegistry
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
if a_map:
if 'href' not in a_map:
a_map['val'] = format_html('<i>{val}</i>', **a_map)
elif is_active:
a_map['val'] = format_html('<a href="{href}">{val}</a>', **a_map)
elif event and plugin_name:
a_map['val'] = format_html(
'<i>{val}</i> <a href="{plugin_href}">'
'<span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span></a>',
**a_map,
errmes=_("The relevant plugin is currently not active. To activate it, click here to go to the plugin settings."),
plugin_href=reverse('control:event.settings.plugins', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '#plugin_' + plugin_name,
)
else:
a_map['val'] = format_html(
'<i>{val}</i> <span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span>',
**a_map,
errmes=_("The relevant plugin is currently not active."),
)
return format_html(wrapper, **a_map)
class LogEntryTypeRegistry(EventPluginRegistry):
def __init__(self):
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
def register(self, *objs):
for obj in objs:
if not isinstance(obj, LogEntryType):
raise TypeError('Entries must be derived from LogEntryType')
if obj.__module__.startswith('pretix.base.'):
raise TypeError('Must not register base classes, only derived ones')
return super().register(*objs)
def new_from_dict(self, data):
"""
Register multiple instance of a `LogEntryType` class with different `action_type`
and plain text strings, as given by the items of the specified data dictionary.
This method is designed to be used as a decorator as follows:
.. code-block:: python
@log_entry_types.new_from_dict({
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
# ...
})
class CoreItemLogEntryType(ItemLogEntryType):
# ...
:param data: action types and descriptions
``{"some_action_type": "Plain text description", ...}``
"""
def reg(clz):
for action_type, plain in data.items():
self.register(clz(action_type=action_type, plain=plain))
return clz
return reg
"""
Registry for LogEntry types.
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
They are annotated with their ``action_type`` and the defining ``plugin``.
"""
log_entry_types = LogEntryTypeRegistry()
class LogEntryType:
"""
Base class for a type of LogEntry, identified by its action_type.
"""
def __init__(self, action_type=None, plain=None):
if action_type:
self.action_type = action_type
if plain:
self.plain = plain
def display(self, logentry, data):
"""
Returns the message to be displayed for a given logentry of this type.
:return: `str` or `LazyI18nString`
"""
if hasattr(self, 'plain'):
plain = str(self.plain)
if '{' in plain:
data = defaultdict(lambda: '?', data)
return plain.format_map(data)
else:
return plain
def get_object_link_info(self, logentry) -> Optional[dict]:
"""
Return information to generate a link to the `content_object` of a given log entry.
Not implemented in the base class, causing the object link to be omitted.
:return: Dictionary with the keys ``href`` (URL to view/edit the object) and
``val`` (text for the anchor element)
"""
pass
def get_object_link(self, logentry):
a_map = self.get_object_link_info(logentry)
return make_link(a_map, self.object_link_wrapper)
object_link_wrapper = '{val}'
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
raise NotImplementedError
class NoOpShredderMixin:
def shred_pii(self, logentry):
pass
class ClearDataShredderMixin:
def shred_pii(self, logentry):
logentry.data = None
+140 -34
View File
@@ -19,20 +19,137 @@
# 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 typing import Optional
from collections import defaultdict
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.models import (
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
Voucher,
)
from pretix.base.signals import EventPluginRegistry
from .logentrytype_registry import ( # noqa
ClearDataShredderMixin, LogEntryType, NoOpShredderMixin, log_entry_types,
make_link, LogEntryTypeRegistry,
)
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
if a_map:
if is_active:
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
elif event and plugin_name:
a_map['val'] = (
'<i>{val}</i> <a href="{plugin_href}">'
'<span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span></a>'
).format_map({
**a_map,
"errmes": _("The relevant plugin is currently not active. To activate it, click here to go to the plugin settings."),
"plugin_href": reverse('control:event.settings.plugins', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '#plugin_' + plugin_name,
})
else:
a_map['val'] = '<i>{val}</i> <span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span>'.format_map({
**a_map,
"errmes": _("The relevant plugin is currently not active."),
})
return wrapper.format_map(a_map)
class LogEntryTypeRegistry(EventPluginRegistry):
def __init__(self):
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
def register(self, *objs):
for obj in objs:
if not isinstance(obj, LogEntryType):
raise TypeError('Entries must be derived from LogEntryType')
if obj.__module__ == LogEntryType.__module__:
raise TypeError('Must not register base classes, only derived ones')
return super().register(*objs)
def new_from_dict(self, data):
"""
Register multiple instance of a `LogEntryType` class with different `action_type`
and plain text strings, as given by the items of the specified data dictionary.
This method is designed to be used as a decorator as follows:
.. code-block:: python
@log_entry_types.new_from_dict({
'pretix.event.item.added': _('The product has been created.'),
'pretix.event.item.changed': _('The product has been changed.'),
# ...
})
class CoreItemLogEntryType(ItemLogEntryType):
# ...
:param data: action types and descriptions
``{"some_action_type": "Plain text description", ...}``
"""
def reg(clz):
for action_type, plain in data.items():
self.register(clz(action_type=action_type, plain=plain))
return clz
return reg
"""
Registry for LogEntry types.
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
They are annotated with their ``action_type`` and the defining ``plugin``.
"""
log_entry_types = LogEntryTypeRegistry()
class LogEntryType:
"""
Base class for a type of LogEntry, identified by its action_type.
"""
def __init__(self, action_type=None, plain=None):
if action_type:
self.action_type = action_type
if plain:
self.plain = plain
def display(self, logentry):
"""
Returns the message to be displayed for a given logentry of this type.
:return: `str` or `LazyI18nString`
"""
if hasattr(self, 'plain'):
plain = str(self.plain)
if '{' in plain:
data = defaultdict(lambda: '?', logentry.parsed_data)
return plain.format_map(data)
else:
return plain
def get_object_link_info(self, logentry) -> dict:
"""
Return information to generate a link to the `content_object` of a given log entry.
Not implemented in the base class, causing the object link to be omitted.
:return: Dictionary with the keys ``href`` (containing a URL to view/edit the object) and ``val`` (containing the
escaped text for the anchor element)
"""
pass
def get_object_link(self, logentry):
a_map = self.get_object_link_info(logentry)
return make_link(a_map, self.object_link_wrapper)
object_link_wrapper = '{val}'
def shred_pii(self, logentry):
"""
To be used for shredding personally identified information contained in the data field of a LogEntry of this
type.
"""
raise NotImplementedError
class EventLogEntryType(LogEntryType):
@@ -40,27 +157,15 @@ class EventLogEntryType(LogEntryType):
Base class for any `LogEntry` type whose `content_object` is either an `Event` itself or belongs to a specific `Event`.
"""
def get_object_link_info(self, logentry) -> Optional[dict]:
if hasattr(self, 'object_link_viewname'):
content = logentry.content_object
if not content:
if logentry.content_type_id:
return {
'val': _('(deleted)'),
}
else:
return
if hasattr(self, 'content_type') and not isinstance(content, self.content_type):
return
def get_object_link_info(self, logentry) -> dict:
if hasattr(self, 'object_link_viewname') and logentry.content_object:
return {
'href': reverse(self.object_link_viewname, kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
**self.object_link_args(content),
**self.object_link_args(logentry.content_object),
}),
'val': self.object_link_display_name(logentry.content_object),
'val': escape(self.object_link_display_name(logentry.content_object)),
}
def object_link_args(self, content_object):
@@ -77,7 +182,6 @@ class EventLogEntryType(LogEntryType):
class OrderLogEntryType(EventLogEntryType):
object_link_wrapper = _('Order {val}')
object_link_viewname = 'control:event.order'
content_type = Order
def object_link_args(self, order):
return {'code': order.code}
@@ -90,7 +194,6 @@ class VoucherLogEntryType(EventLogEntryType):
object_link_wrapper = _('Voucher {val}')
object_link_viewname = 'control:event.voucher'
object_link_argname = 'voucher'
content_type = Voucher
def object_link_display_name(self, voucher):
if len(voucher.code) > 6:
@@ -102,46 +205,49 @@ class ItemLogEntryType(EventLogEntryType):
object_link_wrapper = _('Product {val}')
object_link_viewname = 'control:event.item'
object_link_argname = 'item'
content_type = Item
class SubEventLogEntryType(EventLogEntryType):
object_link_wrapper = pgettext_lazy('subevent', 'Date {val}')
object_link_viewname = 'control:event.subevent'
object_link_argname = 'subevent'
content_type = SubEvent
class QuotaLogEntryType(EventLogEntryType):
object_link_wrapper = _('Quota {val}')
object_link_viewname = 'control:event.items.quotas.show'
object_link_argname = 'quota'
content_type = Quota
class DiscountLogEntryType(EventLogEntryType):
object_link_wrapper = _('Discount {val}')
object_link_viewname = 'control:event.items.discounts.edit'
object_link_argname = 'discount'
content_type = Discount
class ItemCategoryLogEntryType(EventLogEntryType):
object_link_wrapper = _('Category {val}')
object_link_viewname = 'control:event.items.categories.edit'
object_link_argname = 'category'
content_type = ItemCategory
class QuestionLogEntryType(EventLogEntryType):
object_link_wrapper = _('Question {val}')
object_link_viewname = 'control:event.items.questions.show'
object_link_argname = 'question'
content_type = Question
class TaxRuleLogEntryType(EventLogEntryType):
object_link_wrapper = _('Tax rule {val}')
object_link_viewname = 'control:event.settings.tax.edit'
object_link_argname = 'rule'
content_type = TaxRule
class NoOpShredderMixin:
def shred_pii(self, logentry):
pass
class ClearDataShredderMixin:
def shred_pii(self, logentry):
logentry.data = None
@@ -1,18 +0,0 @@
# Generated by Django 4.2.16 on 2025-01-23 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0275_alter_question_valid_number_max_and_more"),
]
operations = [
migrations.AddField(
model_name="item",
name="hidden_if_item_available_mode",
field=models.CharField(default="hide", max_length=16),
),
]
@@ -1,28 +0,0 @@
# Generated by Django 4.2.17 on 2025-02-07 16:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0276_item_hidden_if_item_available_mode"),
]
operations = [
migrations.AddField(
model_name="customerssoclient",
name="require_pkce",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="customerssogrant",
name="code_challenge",
field=models.TextField(null=True),
),
migrations.AddField(
model_name="customerssogrant",
name="code_challenge_method",
field=models.CharField(max_length=255, null=True),
),
]
@@ -1,41 +0,0 @@
# Generated by Django 4.2.16 on 2025-02-28 13:25
from django.db import migrations, models
def remove_duplicates(apps, schema_editor):
UserKnownLoginSource = apps.get_model("pretixbase", "UserKnownLoginSource")
unique_fields = ["user", "agent_type", "device_type", "os_type", "country"]
duplicates = (
UserKnownLoginSource.objects
.values(*unique_fields)
.order_by()
.annotate(latest_id=models.Max('id'), count=models.Count('id'))
.filter(count__gt=1)
)
for duplicate in duplicates:
(
UserKnownLoginSource.objects
.filter(**{x: duplicate[x] for x in unique_fields})
.exclude(id=duplicate["latest_id"])
.delete()
)
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0277_customerssoclient_require_pkce_and_more"),
]
operations = [
migrations.RunPython(remove_duplicates, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name="userknownloginsource",
unique_together={
("user", "agent_type", "device_type", "os_type", "country")
},
),
]
@@ -1,23 +0,0 @@
# Generated by Django 4.2.19 on 2025-03-18 09:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0278_login_source_add_unique_together'),
]
operations = [
migrations.AddField(
model_name='discount',
name='subevent_date_from',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='discount',
name='subevent_date_until',
field=models.DateTimeField(blank=True, null=True),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 4.2.20 on 2025-05-14 14:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0279_discount_event_date_from_discount_event_date_until'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='max_extend',
field=models.DateTimeField(null=True),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 4.2.16 on 2025-05-20 11:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0280_cartposition_max_extend"),
]
operations = [
migrations.AddField(
model_name="event",
name="is_remote",
field=models.BooleanField(default=False),
),
]
-3
View File
@@ -602,9 +602,6 @@ class UserKnownLoginSource(models.Model):
country = FastCountryField(null=True, blank=True)
last_seen = models.DateTimeField()
class Meta:
unique_together = ('user', 'agent_type', 'device_type', 'os_type', 'country')
class StaffSession(models.Model):
user = models.ForeignKey('User', on_delete=models.PROTECT)
-6
View File
@@ -416,10 +416,6 @@ class CustomerSSOClient(LoggedModel):
authorization_grant_type = models.CharField(
max_length=32, choices=GRANT_TYPES, verbose_name=_("Grant type"), default=GRANT_AUTHORIZATION_CODE,
)
require_pkce = models.BooleanField(
verbose_name=_("Require PKCE extension"),
default=False,
)
redirect_uris = models.TextField(
blank=False,
verbose_name=_("Redirection URIs"),
@@ -485,8 +481,6 @@ class CustomerSSOGrant(models.Model):
expires = models.DateTimeField()
redirect_uri = models.TextField()
scope = models.TextField(blank=True)
code_challenge = models.TextField(blank=True, null=True)
code_challenge_method = models.CharField(max_length=255, blank=True, null=True)
class CustomerSSOAccessToken(models.Model):
+3 -21
View File
@@ -36,9 +36,7 @@ from django_scopes import ScopedManager
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
PositionInfo = namedtuple('PositionInfo',
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'is_addon_to',
'voucher_discount'])
PositionInfo = namedtuple('PositionInfo', ['item_id', 'subevent_id', 'line_price_gross', 'is_addon_to', 'voucher_discount'])
class Discount(LoggedModel):
@@ -173,17 +171,6 @@ class Discount(LoggedModel):
"access to sold-out quota will still receive the discount."),
)
subevent_date_from = models.DateTimeField(
verbose_name=pgettext_lazy("subevent", "Available for dates starting from"),
null=True,
blank=True,
)
subevent_date_until = models.DateTimeField(
verbose_name=pgettext_lazy("subevent", "Available for dates starting until"),
null=True,
blank=True,
)
# more feature ideas:
# - max_usages_per_order
# - promote_to_user_if_almost_satisfied
@@ -368,15 +355,11 @@ class Discount(LoggedModel):
# First, filter out everything not even covered by our product scope
condition_candidates = [
idx
for idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount) in
positions.items()
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount) in positions.items()
if (
(self.condition_all_products or item_id in limit_products) and
(self.condition_apply_to_addons or not is_addon_to) and
(not self.condition_ignore_voucher_discounted or voucher_discount is None or voucher_discount == Decimal('0.00'))
and (not subevent_id or (
self.subevent_date_from is None or subevent_date_from >= self.subevent_date_from)) and (
self.subevent_date_until is None or subevent_date_from <= self.subevent_date_until)
)
]
@@ -386,8 +369,7 @@ class Discount(LoggedModel):
benefit_products = {p.pk for p in self.benefit_limit_products.all()}
benefit_candidates = [
idx
for idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount) in
positions.items()
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount) in positions.items()
if (
item_id in benefit_products and
(self.benefit_apply_to_addons or not is_addon_to) and
+25 -43
View File
@@ -60,7 +60,6 @@ from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext, gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
@@ -172,7 +171,7 @@ class EventMixin:
self.date_to.astimezone(tz), ("D" if short else "l")
)
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str:
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False) -> str:
"""
Returns a formatted string containing the start date and the end date
of the event with respect to the current locale and to the ``show_date_to``
@@ -181,48 +180,36 @@ class EventMixin:
tz = tz or self.timezone
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
df, dt = self.date_from, self.date_from
show_times = try_to_show_times
else:
df, dt = self.date_from, self.date_to
show_times = try_to_show_times and self.settings.show_times and (
# Show times if start and end are on the same day ("08:00-10:00")
dt.astimezone(tz).date() == df.astimezone(tz).date() or
# Show times if start and end are on consecutive days and less than 24h ("23:00-03:00")
(dt.astimezone(tz).date() == df.astimezone(tz).date() + timedelta(days=1) and
dt.astimezone(tz).time() < df.astimezone(tz).time())
)
d = daterange(df.astimezone(tz), dt.astimezone(tz), as_html)
if show_times:
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
time_str = _date(self.date_from.astimezone(tz), "TIME_FORMAT")
else:
time_str = '{}{}'.format(
_date(self.date_from.astimezone(tz), "TIME_FORMAT"),
_date(self.date_to.astimezone(tz), "TIME_FORMAT"),
)
if as_html:
d = format_html(
d + ' <time datetime="{}" data-timezone="{}" data-time-short>{}</time>',
self.date_from.isoformat(),
str(self.timezone),
time_str,
)
else:
d = d + ' ' + time_str
return d
def get_date_range_display_with_times(self) -> str: # Helper for usage from templates
return self.get_date_range_display(try_to_show_times=True)
def get_date_range_display_with_times_as_html(self) -> str: # Helper for usage from templates
return self.get_date_range_display(try_to_show_times=True, as_html=True)
return daterange(df.astimezone(tz), dt.astimezone(tz), as_html)
def get_date_range_display_as_html(self, tz=None, force_show_end=False) -> str:
return self.get_date_range_display(tz, force_show_end, as_html=True)
def get_time_range_display(self, tz=None, force_show_end=False) -> str:
"""
Returns a formatted string containing the start time and sometimes the end time
of the event with respect to the current locale and to the ``show_date_to``
setting. Dates are not shown. This is usually used in combination with get_date_range_display
"""
tz = tz or self.timezone
show_date_to = self.date_to and (self.settings.show_date_to or force_show_end) and (
# Show date to if start and end are on the same day ("08:00-10:00")
self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() or
# Show date to if start and end are on consecutive days and less than 24h ("23:00-03:00")
(self.date_to.astimezone(tz).date() == self.date_from.astimezone(tz).date() + timedelta(days=1) and
self.date_to.astimezone(tz).time() < self.date_from.astimezone(tz).time())
# Do not show end time if this is a 5-day event because there's no way to make it understandable
)
if show_date_to:
return '{} {}'.format(
_date(self.date_from.astimezone(tz), "TIME_FORMAT"),
_date(self.date_to.astimezone(tz), "TIME_FORMAT"),
)
return _date(self.date_from.astimezone(tz), "TIME_FORMAT")
@property
def timezone(self):
return pytz_deprecation_shim.timezone(self.settings.timezone)
@@ -616,11 +603,6 @@ class Event(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Location"),
)
is_remote = models.BooleanField(
default=False,
verbose_name=_("This event is remote or partially remote."),
help_text=_("This will be used to let users know if the event is in a different timezone and lets us calculate users local times."),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
+4 -25
View File
@@ -442,12 +442,8 @@ class Item(LoggedModel):
UNAVAIL_MODE_INFO = "info"
UNAVAIL_MODES = (
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
(UNAVAIL_MODE_INFO, _("Show product with info on why its unavailable")),
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
)
UNAVAIL_MODE_ICONS = {
UNAVAIL_MODE_HIDDEN: 'eye-slash',
UNAVAIL_MODE_INFO: 'info'
}
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
@@ -600,11 +596,6 @@ class Item(LoggedModel):
"be a short period in which both products are visible while all tickets of the referenced "
"product are reserved, but not yet sold.")
)
hidden_if_item_available_mode = models.CharField(
choices=UNAVAIL_MODES,
default=UNAVAIL_MODE_HIDDEN,
max_length=16,
)
require_voucher = models.BooleanField(
verbose_name=_('This product can only be bought using a voucher.'),
default=False,
@@ -821,8 +812,7 @@ class Item(LoggedModel):
def ask_attendee_data(self):
return self.admission and self.personalized
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None,
include_bundled=False, force_fixed_gross_price=False):
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
price = price if price is not None else self.default_price
bundled_sum = Decimal('0.00')
@@ -851,7 +841,7 @@ class Item(LoggedModel):
else:
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
override_tax_rate=override_tax_rate, currency=currency or self.event.currency,
subtract_from_gross=bundled_sum, force_fixed_gross_price=force_fixed_gross_price)
subtract_from_gross=bundled_sum)
if bundled_sum:
t.name = "MIXED!"
@@ -895,8 +885,6 @@ class Item(LoggedModel):
return 'available_from'
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
return 'available_until'
elif self.hidden_if_item_available and self._dependency_available:
return 'hidden_if_item_available'
else:
return None
@@ -1837,7 +1825,7 @@ class Question(LoggedModel):
))
llen = len(answer.split(','))
elif all(isinstance(o, QuestionOption) for o in answer):
return answer
return o
else:
l_ = list(self.options.filter(
Q(pk__in=[a for a in answer if isinstance(a, int) or a.isdigit()]) |
@@ -1916,15 +1904,6 @@ class Question(LoggedModel):
if event != item.event:
raise ValidationError(_('One or more items do not belong to this event.'))
def clean(self):
if self.valid_date_max and self.valid_date_min and self.valid_date_min > self.valid_date_max:
raise ValidationError(_("The maximum date must not be before the minimum value."))
if self.valid_datetime_max and self.valid_datetime_min and self.valid_datetime_min > self.valid_datetime_max:
raise ValidationError(_("The maximum date must not be before the minimum value."))
if self.valid_number_max and self.valid_number_min and self.valid_number_min > self.valid_number_max:
raise ValidationError(_("The maximum value must not be lower than the minimum value."))
super().clean()
class QuestionOption(models.Model):
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)
+3 -12
View File
@@ -37,10 +37,10 @@ import logging
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import connections, models
from django.db import models
from django.utils.functional import cached_property
from pretix.base.logentrytype_registry import log_entry_types, make_link
from pretix.base.logentrytypes import log_entry_types, make_link
from pretix.base.signals import is_app_active, logentry_object_link
@@ -93,7 +93,7 @@ class LogEntry(models.Model):
def display(self):
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type:
return log_entry_type.display(self, self.parsed_data)
return log_entry_type.display(self)
from ..signals import logentry_display
@@ -165,15 +165,6 @@ class LogEntry(models.Model):
def delete(self, using=None, keep_parents=False):
raise TypeError("Logs cannot be deleted.")
@classmethod
def bulk_create_and_postprocess(cls, objects):
if connections['default'].features.can_return_rows_from_bulk_insert:
cls.objects.bulk_create(objects)
else:
for le in objects:
le.save()
cls.bulk_postprocess(objects)
@classmethod
def bulk_postprocess(cls, objects):
from pretix.api.webhooks import notify_webhooks
+2 -9
View File
@@ -355,7 +355,7 @@ class Order(LockModel, LoggedModel):
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
self.log_action(
self.event.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
@@ -1199,8 +1199,6 @@ class Order(LockModel, LoggedModel):
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
@@ -2859,8 +2857,6 @@ class OrderPosition(AbstractPosition):
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [],
}
)
@@ -3098,10 +3094,7 @@ class CartPosition(AbstractPosition):
verbose_name=_("Expiration date"),
db_index=True
)
max_extend = models.DateTimeField(
verbose_name=_("Limit for extending expiration date"),
null=True
)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2, default=Decimal('0.00'),
verbose_name=_('Tax rate')
-2
View File
@@ -286,8 +286,6 @@ class WaitingListEntry(LoggedModel):
'subject': subject,
'message': email_content,
'recipient': recipient,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
+95 -157
View File
@@ -43,6 +43,7 @@ from zoneinfo import ZoneInfo
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import transaction
from django.dispatch import receiver
@@ -92,73 +93,15 @@ class PaymentProviderForm(Form):
cleaned_data = super().clean()
for k, v in self.fields.items():
val = cleaned_data.get(k)
if hasattr(v, '_required') and v._required and not val:
if v._required and not val:
self.add_error(k, _('This field is required.'))
return cleaned_data
class GiftCardPaymentForm(PaymentProviderForm):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
self.testmode = kwargs.pop('testmode')
self.positions = kwargs.pop('positions')
self.used_cards = kwargs.pop('used_cards')
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
if "code" not in cleaned_data:
return cleaned_data
code = cleaned_data["code"].strip()
msg = ""
for p in self.positions:
if p.item.issue_giftcard:
msg = _("You cannot pay with gift cards when buying a gift card.")
self.add_error('code', msg)
return cleaned_data
try:
event = self.event
gc = event.organizer.accepted_gift_cards.get(
secret=code
)
if gc.currency != event.currency:
msg = _("This gift card does not support this currency.")
elif gc.testmode and not self.testmode:
msg = _("This gift card can only be used in test mode.")
elif not gc.testmode and self.testmode:
msg = _("Only test gift cards can be used in test mode.")
elif gc.expires and gc.expires < time_machine_now():
msg = _("This gift card is no longer valid.")
elif gc.value <= Decimal("0.00"):
msg = _("All credit on this gift card has been used.")
if msg:
self.add_error('code', msg)
return cleaned_data
if gc.pk in self.used_cards:
self.add_error('code', _("This gift card is already used for your payment."))
return cleaned_data
except GiftCard.DoesNotExist:
if event.vouchers.filter(code__iexact=code).exists():
msg = _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection.")
self.add_error('code', msg)
else:
msg = _("This gift card is not known.")
self.add_error('code', msg)
except GiftCard.MultipleObjectsReturned:
msg = _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event.")
self.add_error('code', msg)
return cleaned_data
class BasePaymentProvider:
"""
This is the base class for all payment providers.
"""
payment_form_template_name = 'pretixpresale/event/checkout_payment_form_default.html'
def __init__(self, event: Event):
self.event = event
@@ -387,18 +330,18 @@ class BasePaymentProvider:
label=_('Enable payment method'),
required=False,
)),
('_availability_start',
RelativeDateField(
label=_('Available from'),
help_text=_('Users will not be able to choose this payment provider before the given date.'),
required=False,
)),
('_availability_date',
RelativeDateField(
label=_('Available until'),
help_text=_('Users will not be able to choose this payment provider after the given date.'),
required=False,
)),
('_availability_start',
RelativeDateField(
label=_('Available from'),
help_text=_('Users will not be able to choose this payment provider before the given date.'),
required=False,
)),
('_total_min',
forms.DecimalField(
label=_('Minimum order total'),
@@ -751,7 +694,7 @@ class BasePaymentProvider:
:param order: Only set when this is a change to a new payment method for an existing order.
"""
form = self.payment_form(request)
template = get_template(self.payment_form_template_name)
template = get_template('pretixpresale/event/checkout_payment_form_default.html')
ctx = {'request': request, 'form': form}
return template.render(ctx)
@@ -1014,19 +957,12 @@ class BasePaymentProvider:
def cancel_payment(self, payment: OrderPayment):
"""
Will be called to cancel a payment. The default implementation fails if the payment is
``OrderPayment.PAYMENT_STATE_PENDING`` and ``abort_pending_allowed`` is false. Otherwise, it just sets the
payment state to canceled. In some cases you might want to modify this behaviour to notify the external provider
of the cancellation.
Will be called to cancel a payment. The default implementation just sets the payment state to canceled,
but in some cases you might want to notify an external provider.
On success, you should set ``payment.state = OrderPayment.PAYMENT_STATE_CANCELED`` (or call the super method).
On failure, you should raise a PaymentException.
"""
if payment.state == OrderPayment.PAYMENT_STATE_PENDING and not self.abort_pending_allowed:
raise PaymentException(_(
"This payment is already being processed and can not be canceled any more."
))
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save(update_fields=['state'])
@@ -1365,9 +1301,6 @@ class OffsettingProvider(BasePaymentProvider):
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders']))
def refund_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
return self.payment_control_render(request, payment)
class GiftCardPayment(BasePaymentProvider):
identifier = "giftcard"
@@ -1375,49 +1308,6 @@ class GiftCardPayment(BasePaymentProvider):
multi_use_supported = True
execute_payment_needs_user = False
verbose_name = _("Gift card")
payment_form_class = GiftCardPaymentForm
payment_form_template_name = 'pretixcontrol/giftcards/checkout.html'
def payment_form(self, request: HttpRequest) -> Form:
# Unfortunately, in payment_form we do not know if we're in checkout
# or in an existing order. But we need to do the validation logic in the
# form to get the error messages in the right places for accessbility :-(
if 'checkout' in request.resolver_match.url_name:
cs = cart_session(request)
used_cards = [
p.get('info_data', {}).get('gift_card')
for p in cs.get('payments', [])
if p.get('info_data', {}).get('gift_card')
]
positions = get_cart(request)
testmode = self.event.testmode
else:
used_cards = []
order = self.event.orders.get(code=request.resolver_match.kwargs["order"])
positions = order.positions.all()
testmode = order.testmode
form = self.payment_form_class(
event=self.event,
used_cards=used_cards,
positions=positions,
testmode=testmode,
data=(request.POST if request.method == 'POST' and request.POST.get("payment") == self.identifier else None),
prefix='payment_%s' % self.identifier,
initial={
k.replace('payment_%s_' % self.identifier, ''): v
for k, v in request.session.items()
if k.startswith('payment_%s_' % self.identifier)
}
)
form.fields = self.payment_form_fields
for k, v in form.fields.items():
v._required = v.required
v.required = False
v.widget.is_required = False
return form
@property
def public_name(self) -> str:
@@ -1450,19 +1340,6 @@ class GiftCardPayment(BasePaymentProvider):
f.move_to_end("_enabled", last=False)
return f
@property
def payment_form_fields(self):
fields = [
(
"code",
forms.CharField(
label=_("Gift card code"),
required=True,
),
),
]
return OrderedDict(fields)
@property
def test_mode_message(self) -> str:
return _("In test mode, only test cards will work.")
@@ -1473,6 +1350,11 @@ class GiftCardPayment(BasePaymentProvider):
def order_change_allowed(self, order: Order) -> bool:
return super().order_change_allowed(order) and self.event.organizer.has_gift_cards
def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
return get_template('pretixcontrol/giftcards/checkout.html').render({
'request': request,
})
def checkout_confirm_render(self, request, order=None, info_data=None) -> str:
return get_template('pretixcontrol/giftcards/checkout_confirm.html').render({
'info_data': info_data,
@@ -1540,6 +1422,21 @@ class GiftCardPayment(BasePaymentProvider):
def _add_giftcard_to_cart(self, cs, gc):
from pretix.base.services.cart import add_payment_to_cart_session
if gc.currency != self.event.currency:
raise ValidationError(_("This gift card does not support this currency."))
if gc.testmode and not self.event.testmode:
raise ValidationError(_("This gift card can only be used in test mode."))
if not gc.testmode and self.event.testmode:
raise ValidationError(_("Only test gift cards can be used in test mode."))
if gc.expires and gc.expires < time_machine_now():
raise ValidationError(_("This gift card is no longer valid."))
if gc.value <= Decimal("0.00"):
raise ValidationError(_("All credit on this gift card has been used."))
for p in cs.get('payments', []):
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
raise ValidationError(_("This gift card is already used for your payment."))
add_payment_to_cart_session(
cs,
self,
@@ -1551,32 +1448,73 @@ class GiftCardPayment(BasePaymentProvider):
)
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
form = self.payment_form(request)
if not form.is_valid():
return False
for p in get_cart(request):
if p.item.issue_giftcard:
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
return
gc = self.event.organizer.accepted_gift_cards.get(
secret=form.cleaned_data["code"]
)
cs = cart_session(request)
self._add_giftcard_to_cart(cs, gc)
return True
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard").strip()
)
cs = cart_session(request)
try:
self._add_giftcard_to_cart(cs, gc)
return True
except ValidationError as e:
messages.error(request, str(e.message))
return
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection."))
else:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str, None]:
form = self.payment_form(request)
if not form.is_valid():
return False
gc = self.event.organizer.accepted_gift_cards.get(
secret=form.cleaned_data["code"]
)
payment.info_data = {
'gift_card': gc.pk,
'gift_card_secret': gc.secret,
'retry': True
}
payment.amount = min(payment.amount, gc.value)
payment.save()
return True
for p in payment.order.positions.all():
if p.item.issue_giftcard:
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
return
try:
gc = self.event.organizer.accepted_gift_cards.get(
secret=request.POST.get("giftcard").strip()
)
if gc.currency != self.event.currency:
messages.error(request, _("This gift card does not support this currency."))
return
if gc.testmode and not payment.order.testmode:
messages.error(request, _("This gift card can only be used in test mode."))
return
if not gc.testmode and payment.order.testmode:
messages.error(request, _("Only test gift cards can be used in test mode."))
return
if gc.expires and gc.expires < time_machine_now():
messages.error(request, _("This gift card is no longer valid."))
return
if gc.value <= Decimal("0.00"):
messages.error(request, _("All credit on this gift card has been used."))
return
payment.info_data = {
'gift_card': gc.pk,
'gift_card_secret': gc.secret,
'retry': True
}
payment.amount = min(payment.amount, gc.value)
payment.save()
return True
except GiftCard.DoesNotExist:
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard").strip()).exists():
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
"the product selection."))
else:
messages.error(request, _("This gift card is not known."))
except GiftCard.MultipleObjectsReturned:
messages.error(request, _("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
def execute_payment(self, request: HttpRequest, payment: OrderPayment, is_early_special_case=False) -> str:
for p in payment.order.positions.all():
-4
View File
@@ -822,10 +822,6 @@ class Renderer:
kwargs = {}
if o.get('nowhitespace', False):
kwargs['barBorder'] = 0
if o.get('color'):
kwargs['barFillColor'] = Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255)
qrw = QrCodeWidget(content, barLevel=level, barHeight=reqs, barWidth=reqs, **kwargs)
d = Drawing(reqs, reqs)
d.add(qrw)
+19 -72
View File
@@ -45,7 +45,6 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
from django.db.models.aggregates import Min
from django.dispatch import receiver
from django.utils.timezone import make_aware, now
from django.utils.translation import (
@@ -276,10 +275,7 @@ class CartManager:
}
def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
invoice_address: InvoiceAddress=None, widget_data=None, reservation_time: timedelta=None):
"""
Creates a new CartManager for an event.
"""
invoice_address: InvoiceAddress=None, widget_data=None, expiry=None):
self.event = event
self.cart_id = cart_id
self.real_now_dt = now()
@@ -290,17 +286,11 @@ class CartManager:
self._subevents_cache = {}
self._variations_cache = {}
self._seated_cache = {}
self._expiry = None
self._explicit_expiry = expiry
self.invoice_address = invoice_address
self._widget_data = widget_data or {}
self._sales_channel = sales_channel
self.num_extended_positions = 0
if reservation_time:
self._reservation_time = reservation_time
else:
self._reservation_time = timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
self._expiry = self.real_now_dt + self._reservation_time
self._max_expiry_extend = self.real_now_dt + (self._reservation_time * 11)
@property
def positions(self):
@@ -315,6 +305,14 @@ class CartManager:
self._seated_cache[item, subevent] = item.seat_category_mappings.filter(subevent=subevent).exists()
return self._seated_cache[item, subevent]
def _calculate_expiry(self):
if self._explicit_expiry:
self._expiry = self._explicit_expiry
else:
self._expiry = self.real_now_dt + timedelta(
minutes=self.event.settings.get('reservation_time', as_type=int)
)
def _check_presale_dates(self):
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
raise CartError(error_messages['not_started'])
@@ -331,27 +329,9 @@ class CartManager:
raise CartError(error_messages['payment_ended'])
def _extend_expiry_of_valid_existing_positions(self):
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
# delta to reduce risk of extending already expired CartPositions.
padded_now_dt = self.real_now_dt + timedelta(seconds=5)
# Make sure we do not extend past the max_extend timestamp, allowing users to extend their valid positions up
# to 11 times the reservation time. If we add new positions to the cart while valid positions exist, the new
# positions' reservation will also be limited to max_extend of the oldest position.
# Only after all positions expire, an ExtendOperation may reset max_extend to another 11x reservation_time.
max_extend_existing = self.positions.filter(expires__gt=padded_now_dt).aggregate(m=Min('max_extend'))['m']
if max_extend_existing:
self._expiry = min(self._expiry, max_extend_existing)
self._max_expiry_extend = max_extend_existing
# Extend this user's cart session to ensure all items in the cart expire at the same time
# We can extend the reservation of items which are not yet expired without risk
if self._expiry > padded_now_dt:
self.num_extended_positions += self.positions.filter(
expires__gt=padded_now_dt, expires__lt=self._expiry,
).update(
expires=self._expiry,
)
self.positions.filter(expires__gt=self.real_now_dt).update(expires=self._expiry)
def _delete_out_of_timeframe(self):
err = None
@@ -644,9 +624,6 @@ class CartManager:
if p.is_bundled:
continue
if p.custom_price_input and p.custom_price_input != p.listed_price:
continue
if p.listed_price is None:
if p.addon_to_id and is_included_for_free(p.item, p.addon_to):
listed_price = Decimal('0.00')
@@ -1266,7 +1243,6 @@ class CartManager:
item=op.item,
variation=op.variation,
expires=self._expiry,
max_extend=self._max_expiry_extend,
cart_id=self.cart_id,
voucher=op.voucher,
addon_to=op.addon_to if op.addon_to else None,
@@ -1315,9 +1291,7 @@ class CartManager:
event=self.event,
item=b.item,
variation=b.variation,
expires=self._expiry,
max_extend=self._max_expiry_extend,
cart_id=self.cart_id,
expires=self._expiry, cart_id=self.cart_id,
voucher=None,
addon_to=cp,
subevent=b.subevent,
@@ -1344,14 +1318,12 @@ class CartManager:
op.position.delete()
elif available_count == 1:
op.position.expires = self._expiry
op.position.max_extend = self._max_expiry_extend
op.position.listed_price = op.listed_price
op.position.price_after_voucher = op.price_after_voucher
# op.position.price will be updated by recompute_final_prices_and_taxes()
if op.position.pk not in deleted_positions:
try:
op.position.save(force_update=True, update_fields=['expires', 'max_extend', 'listed_price', 'price_after_voucher'])
self.num_extended_positions += 1
op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher'])
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
@@ -1374,10 +1346,8 @@ class CartManager:
op.position.price_after_voucher = op.price_after_voucher
op.position.voucher = op.voucher
if op.position.custom_price_input and op.position.custom_price_input == op.position.listed_price:
op.position.custom_price_input = op.price_after_voucher
# op.position.price will be set in recompute_final_prices_and_taxes
op.position.save(update_fields=['price_after_voucher', 'voucher', 'custom_price_input'])
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:
@@ -1423,8 +1393,7 @@ class CartManager:
self.event,
self._sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in positions
]
)
@@ -1441,11 +1410,14 @@ class CartManager:
def commit(self):
self._check_presale_dates()
self._check_max_cart_size()
self._calculate_expiry()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = err or self._check_min_per_voucher()
self.real_now_dt = now()
self._extend_expiry_of_valid_existing_positions()
err = self._perform_operations() or err
self.recompute_final_prices_and_taxes()
@@ -1654,31 +1626,6 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
raise CartError(error_messages['busy'])
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> dict:
"""
Resets the expiry time of a cart to the configured reservation time of this event.
Limited to 11x the reservation time.
:param event: The event ID in question
:param cart_id: The cart ID of the cart to modify
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
except SalesChannel.DoesNotExist:
raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
cm.commit()
return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise CartError(error_messages['busy'])
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
+1 -2
View File
@@ -120,8 +120,7 @@ class CrossSellingService:
self.event,
self.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
bool(cp.addon_to), cp.is_bundled,
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled,
cp.listed_price - cp.price_after_voucher)
for cp in self.cartpositions
],
+1 -1
View File
@@ -531,7 +531,7 @@ def send_invoices_to_organizer(sender, **kwargs):
if i.event.settings.invoice_email_organizer:
with language(i.event.settings.locale):
mail(
email=[e.strip() for e in i.event.settings.invoice_email_organizer.split(",")],
email=i.event.settings.invoice_email_organizer,
subject=_('New invoice: {number}').format(number=i.number),
template=LazyI18nString.from_gettext(_(
'Hello,\n\n'
+1 -1
View File
@@ -674,7 +674,7 @@ def replace_images_with_cid_paths(body_html):
if body_html:
email = BeautifulSoup(body_html, "lxml")
cid_images = []
for image in email.find_all('img'):
for image in email.findAll('img'):
original_image_src = image['src']
try:
+135 -203
View File
@@ -98,9 +98,8 @@ 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_expiry_changed, order_fee_calculation, order_paid, order_placed,
order_reactivated, order_split, order_valid_if_pending, periodic_task,
validate_order,
order_fee_calculation, order_paid, order_placed, order_reactivated,
order_split, order_valid_if_pending, periodic_task, validate_order,
)
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
from pretix.celery_app import app
@@ -303,7 +302,6 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
'state_change': was_expired
}
)
order_expiry_changed.send(sender=order.event, order=order)
if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count()
@@ -875,8 +873,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
event,
sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in sorted_positions
]
)
@@ -1564,7 +1561,6 @@ class OrderChangeManager:
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff'))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
ChangeSecretOperation = namedtuple('ChangeSecretOperation', ('position', 'new_secret'))
ChangeValidFromOperation = namedtuple('ChangeValidFromOperation', ('position', 'valid_from'))
ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until'))
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
@@ -1672,9 +1668,6 @@ class OrderChangeManager:
def regenerate_secret(self, position: OrderPosition):
self._operations.append(self.RegenerateSecretOperation(position))
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
self._operations.append(self.ChangeSecretOperation(position, new_secret))
def change_valid_from(self, position: OrderPosition, new_value: datetime):
self._operations.append(self.ChangeValidFromOperation(position, new_value))
@@ -1689,8 +1682,7 @@ class OrderChangeManager:
def change_price(self, position: OrderPosition, price: Decimal):
tax_rule = self._current_tax_rules().get(position.pk, position.tax_rule) or TaxRule.zero()
price = tax_rule.tax(price, base_price_is='gross', invoice_address=self._invoice_address,
force_fixed_gross_price=True)
price = tax_rule.tax(price, base_price_is='gross')
if position.issued_gift_cards.exists():
raise OrderError(self.error_messages['gift_card_change'])
@@ -1755,8 +1747,7 @@ class OrderChangeManager:
self._operations.append(self.AddFeeOperation(fee, fee.value))
def change_fee(self, fee: OrderFee, value: Decimal):
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross', invoice_address=self._invoice_address,
force_fixed_gross_price=True)
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
self._totaldiff += value.gross - fee.value
self._invoice_dirty = True
self._operations.append(self.FeeValueOperation(fee, value, value.gross - fee.value))
@@ -1791,8 +1782,7 @@ class OrderChangeManager:
if price is None:
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
elif not isinstance(price, TaxedPrice):
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address,
force_fixed_gross_price=True)
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
@@ -2221,79 +2211,73 @@ class OrderChangeManager:
nextposid = self.order.all_positions.aggregate(m=Max('positionid'))['m'] + 1
split_positions = []
secret_dirty = set()
position_cache = {}
fee_cache = {}
for op in self._operations:
if isinstance(op, self.ItemOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.item', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'old_item': position.item.pk,
'old_variation': position.variation.pk if position.variation else None,
'position': op.position.pk,
'positionid': op.position.positionid,
'old_item': op.position.item.pk,
'old_variation': op.position.variation.pk if op.position.variation else None,
'new_item': op.item.pk,
'new_variation': op.variation.pk if op.variation else None,
'old_price': position.price,
'addon_to': position.addon_to_id,
'new_price': position.price
'old_price': op.position.price,
'addon_to': op.position.addon_to_id,
'new_price': op.position.price
})
position.item = op.item
position.variation = op.variation
position._calculate_tax()
op.position.item = op.item
op.position.variation = op.variation
op.position._calculate_tax()
if position.voucher_budget_use is not None and position.voucher and not position.addon_to_id:
listed_price = get_listed_price(position.item, position.variation, position.subevent)
if not position.item.tax_rule or position.item.tax_rule.price_includes_tax:
price_after_voucher = max(position.price, position.voucher.calculate_price(listed_price))
if op.position.voucher_budget_use is not None and op.position.voucher and not op.position.addon_to_id:
listed_price = get_listed_price(op.position.item, op.position.variation, op.position.subevent)
if not op.position.item.tax_rule or op.position.item.tax_rule.price_includes_tax:
price_after_voucher = max(op.position.price, op.position.voucher.calculate_price(listed_price))
else:
price_after_voucher = max(position.price - position.tax_value, position.voucher.calculate_price(listed_price))
position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
secret_dirty.add(position)
position.save()
price_after_voucher = max(op.position.price - op.position.tax_value, op.position.voucher.calculate_price(listed_price))
op.position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
secret_dirty.add(op.position)
op.position.save()
elif isinstance(op, self.MembershipOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.membership', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'old_membership_id': position.used_membership_id,
'position': op.position.pk,
'positionid': op.position.positionid,
'old_membership_id': op.position.used_membership_id,
'new_membership_id': op.membership.pk if op.membership else None,
})
position.used_membership = op.membership
position.save()
op.position.used_membership = op.membership
op.position.save()
elif isinstance(op, self.SeatOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'old_seat': position.seat.name if position.seat else "-",
'position': op.position.pk,
'positionid': op.position.positionid,
'old_seat': op.position.seat.name if op.position.seat else "-",
'new_seat': op.seat.name if op.seat else "-",
'old_seat_id': position.seat.pk if position.seat else None,
'old_seat_id': op.position.seat.pk if op.position.seat else None,
'new_seat_id': op.seat.pk if op.seat else None,
})
position.seat = op.seat
secret_dirty.add(position)
position.save()
op.position.seat = op.seat
secret_dirty.add(op.position)
op.position.save()
elif isinstance(op, self.SubeventOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.subevent', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'old_subevent': position.subevent.pk,
'position': op.position.pk,
'positionid': op.position.positionid,
'old_subevent': op.position.subevent.pk,
'new_subevent': op.subevent.pk,
'old_price': position.price,
'new_price': position.price
'old_price': op.position.price,
'new_price': op.position.price
})
position.subevent = op.subevent
secret_dirty.add(position)
if position.voucher_budget_use is not None and position.voucher and not position.addon_to_id:
listed_price = get_listed_price(position.item, position.variation, position.subevent)
if not position.item.tax_rule or position.item.tax_rule.price_includes_tax:
price_after_voucher = max(position.price, position.voucher.calculate_price(listed_price))
op.position.subevent = op.subevent
secret_dirty.add(op.position)
if op.position.voucher_budget_use is not None and op.position.voucher and not op.position.addon_to_id:
listed_price = get_listed_price(op.position.item, op.position.variation, op.position.subevent)
if not op.position.item.tax_rule or op.position.item.tax_rule.price_includes_tax:
price_after_voucher = max(op.position.price, op.position.voucher.calculate_price(listed_price))
else:
price_after_voucher = max(position.price - position.tax_value, position.voucher.calculate_price(listed_price))
position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
position.save()
price_after_voucher = max(op.position.price - op.position.tax_value, op.position.voucher.calculate_price(listed_price))
op.position.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
op.position.save()
elif isinstance(op, self.AddFeeOperation):
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
'fee': op.fee.pk,
@@ -2302,79 +2286,70 @@ class OrderChangeManager:
op.fee._calculate_tax()
op.fee.save()
elif isinstance(op, self.FeeValueOperation):
fee = fee_cache.setdefault(op.fee.pk, op.fee)
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
'fee': fee.pk,
'old_price': fee.value,
'fee': op.fee.pk,
'old_price': op.fee.value,
'new_price': op.value.gross
})
fee.value = op.value.gross
fee._calculate_tax()
fee.save()
op.fee.value = op.value.gross
op.fee._calculate_tax()
op.fee.save()
elif isinstance(op, self.PriceOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'old_price': position.price,
'addon_to': position.addon_to_id,
'position': op.position.pk,
'positionid': op.position.positionid,
'old_price': op.position.price,
'addon_to': op.position.addon_to_id,
'new_price': op.price.gross
})
position.price = op.price.gross
position.tax_rate = op.price.rate
position.tax_value = op.price.tax
position.tax_code = op.price.code
position.save(update_fields=['price', 'tax_rate', 'tax_value', 'tax_code'])
op.position.price = op.price.gross
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.tax_code = op.price.code
op.position.save()
elif isinstance(op, self.TaxRuleOperation):
if isinstance(op.position, OrderPosition):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'addon_to': position.addon_to_id,
'old_taxrule': position.tax_rule.pk if position.tax_rule else None,
'position': op.position.pk,
'positionid': op.position.positionid,
'addon_to': op.position.addon_to_id,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
'new_taxrule': op.tax_rule.pk
})
position._calculate_tax(op.tax_rule)
position.save()
elif isinstance(op.position, OrderFee):
fee = fee_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
'fee': fee.pk,
'fee_type': fee.fee_type,
'old_taxrule': fee.tax_rule.pk if fee.tax_rule else None,
'fee': op.position.pk,
'fee_type': op.position.fee_type,
'old_taxrule': op.position.tax_rule.pk if op.position.tax_rule else None,
'new_taxrule': op.tax_rule.pk
})
fee._calculate_tax(op.tax_rule)
fee.save()
op.position._calculate_tax(op.tax_rule)
op.position.save()
elif isinstance(op, self.CancelFeeOperation):
fee = fee_cache.setdefault(op.fee.pk, op.fee)
self.order.log_action('pretix.event.order.changed.cancelfee', user=self.user, auth=self.auth, data={
'fee': fee.pk,
'fee_type': fee.fee_type,
'old_price': fee.value,
'fee': op.fee.pk,
'fee_type': op.fee.fee_type,
'old_price': op.fee.value,
})
fee.canceled = True
fee.save(update_fields=['canceled'])
op.fee.canceled = True
op.fee.save(update_fields=['canceled'])
elif isinstance(op, self.CancelOperation):
position = position_cache.setdefault(op.position.pk, op.position)
for gc in position.issued_gift_cards.all():
for gc in op.position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
if gc.value < position.price:
if gc.value < op.position.price:
raise OrderError(_(
'A position can not be canceled since the gift card {card} purchased in this order has '
'already been redeemed.').format(
card=gc.secret
))
else:
gc.transactions.create(value=-position.price, order=self.order, acceptor=self.order.event.organizer)
gc.transactions.create(value=-op.position.price, order=self.order, acceptor=self.order.event.organizer)
for m in position.granted_memberships.with_usages().all():
for m in op.position.granted_memberships.with_usages().all():
m.canceled = True
m.save()
for opa in position.addons.all():
opa = position_cache.setdefault(opa.pk, opa)
for opa in op.position.addons.all():
for gc in opa.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
if gc.value < opa.position.price:
@@ -2408,22 +2383,22 @@ class OrderChangeManager:
)
opa.save(update_fields=['canceled', 'secret'])
self.order.log_action('pretix.event.order.changed.cancel', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'old_item': position.item.pk,
'old_variation': position.variation.pk if position.variation else None,
'old_price': position.price,
'position': op.position.pk,
'positionid': op.position.positionid,
'old_item': op.position.item.pk,
'old_variation': op.position.variation.pk if op.position.variation else None,
'old_price': op.position.price,
'addon_to': None,
})
position.canceled = True
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
op.position.canceled = True
if op.position.voucher:
Voucher.objects.filter(pk=op.position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
assign_ticket_secret(
event=self.event, position=position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
event=self.event, position=op.position, force_invalidate_if_revokation_list_used=True, force_invalidate=False, save=False
)
if position in secret_dirty:
secret_dirty.remove(position)
position.save(update_fields=['canceled', 'secret'])
if op.position in secret_dirty:
secret_dirty.remove(op.position)
op.position.save(update_fields=['canceled', 'secret'])
elif isinstance(op, self.AddOperation):
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
@@ -2448,28 +2423,13 @@ class OrderChangeManager:
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
})
elif isinstance(op, self.SplitOperation):
position = position_cache.setdefault(op.position.pk, op.position)
split_positions.append(position)
split_positions.append(op.position)
elif isinstance(op, self.RegenerateSecretOperation):
position = position_cache.setdefault(op.position.pk, op.position)
position.web_secret = generate_secret()
position.save(update_fields=["web_secret"])
op.position.web_secret = generate_secret()
op.position.save(update_fields=["web_secret"])
assign_ticket_secret(
event=self.event, position=position, force_invalidate=True, save=True
event=self.event, position=op.position, force_invalidate=True, save=True
)
if position in secret_dirty:
secret_dirty.remove(position)
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
'order': self.order.pk})
self.order.log_action('pretix.event.order.changed.secret', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
})
elif isinstance(op, self.ChangeSecretOperation):
if OrderPosition.all.filter(order__event=self.event, secret=op.new_secret).exists():
raise OrderError('You cannot assign a position secret that already exists.')
op.position.secret = op.new_secret
op.position.save(update_fields=["secret"])
if op.position in secret_dirty:
secret_dirty.remove(op.position)
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
@@ -2479,68 +2439,64 @@ class OrderChangeManager:
'positionid': op.position.positionid,
})
elif isinstance(op, self.ChangeValidFromOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.valid_from', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'position': op.position.pk,
'positionid': op.position.positionid,
'new_value': op.valid_from.isoformat() if op.valid_from else None,
'old_value': position.valid_from.isoformat() if position.valid_from else None,
'old_value': op.position.valid_from.isoformat() if op.position.valid_from else None,
})
position.valid_from = op.valid_from
position.save(update_fields=['valid_from'])
secret_dirty.add(position)
op.position.valid_from = op.valid_from
op.position.save(update_fields=['valid_from'])
secret_dirty.add(op.position)
elif isinstance(op, self.ChangeValidUntilOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.valid_until', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'position': op.position.pk,
'positionid': op.position.positionid,
'new_value': op.valid_until.isoformat() if op.valid_until else None,
'old_value': position.valid_until.isoformat() if position.valid_until else None,
'old_value': op.position.valid_until.isoformat() if op.position.valid_until else None,
})
position.valid_until = op.valid_until
position.save(update_fields=['valid_until'])
secret_dirty.add(position)
op.position.valid_until = op.valid_until
op.position.save(update_fields=['valid_until'])
secret_dirty.add(op.position)
elif isinstance(op, self.AddBlockOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.add_block', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'position': op.position.pk,
'positionid': op.position.positionid,
'block_name': op.block_name,
})
if position.blocked:
if op.block_name not in position.blocked:
position.blocked = position.blocked + [op.block_name]
if op.position.blocked:
if op.block_name not in op.position.blocked:
op.position.blocked = op.position.blocked + [op.block_name]
else:
position.blocked = [op.block_name]
op.position.blocked = [op.block_name]
if op.ignore_from_quota_while_blocked is not None:
position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if position.blocked:
position.blocked_secrets.update_or_create(
op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if op.position.blocked:
op.position.blocked_secrets.update_or_create(
event=self.event,
secret=position.secret,
secret=op.position.secret,
defaults={
'blocked': True,
'updated': now(),
}
)
elif isinstance(op, self.RemoveBlockOperation):
position = position_cache.setdefault(op.position.pk, op.position)
self.order.log_action('pretix.event.order.changed.remove_block', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
'position': op.position.pk,
'positionid': op.position.positionid,
'block_name': op.block_name,
})
if position.blocked and op.block_name in position.blocked:
position.blocked = [b for b in position.blocked if b != op.block_name]
if not position.blocked:
position.blocked = None
if op.position.blocked and op.block_name in op.position.blocked:
op.position.blocked = [b for b in op.position.blocked if b != op.block_name]
if not op.position.blocked:
op.position.blocked = None
if op.ignore_from_quota_while_blocked is not None:
position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if not position.blocked:
op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked
op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked'])
if not op.position.blocked:
try:
bs = position.blocked_secrets.get(secret=position.secret)
bs = op.position.blocked_secrets.get(secret=op.position.secret)
bs.blocked = False
bs.save()
except BlockedTicketSecret.DoesNotExist:
@@ -2768,11 +2724,7 @@ class OrderChangeManager:
def _check_complete_cancel(self):
current = self.order.positions.count()
cancels = sum([
1 + o.position.addons.count() for o in self._operations if isinstance(o, self.CancelOperation)
]) + len([
o for o in self._operations if isinstance(o, self.SplitOperation)
])
cancels = len([o for o in self._operations if isinstance(o, (self.CancelOperation, self.SplitOperation))])
adds = len([o for o in self._operations if isinstance(o, self.AddOperation)])
if current > 0 and current - cancels + adds < 1:
raise OrderError(self.error_messages['complete_cancel'])
@@ -3162,34 +3114,14 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
}
)
new_invoice_created = False
if recreate_invoices:
# Lock to prevent duplicate invoice creation
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
i = order.invoices.filter(is_cancellation=False).last()
has_active_invoice = i and not i.canceled
if has_active_invoice and order.total != oldtotal:
if i and order.total != oldtotal and not i.canceled:
generate_cancellation(i)
generate_invoice(order)
new_invoice_created = True
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
if order.event.settings.get('invoice_generate') == 'True' or (
order.event.settings.get('invoice_generate') == 'paid' and
new_payment.payment_provider.requires_invoice_immediately
):
if has_active_invoice:
generate_cancellation(i)
i = generate_invoice(order)
new_invoice_created = True
order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
order.create_transactions()
return old_fee, new_fee, fee, new_payment, new_invoice_created
return old_fee, new_fee, fee, new_payment
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
+5 -8
View File
@@ -21,7 +21,6 @@
#
import re
from collections import defaultdict
from datetime import datetime
from decimal import Decimal
from typing import List, Optional, Tuple, Union
@@ -163,14 +162,14 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
positions: List[Tuple[int, Optional[int], Optional[datetime], Decimal, bool, bool, Decimal]],
positions: List[Tuple[int, Optional[int], Decimal, bool, bool, Decimal]],
collect_potential_discounts: Optional[defaultdict]=None) -> List[Tuple[Decimal, Optional[Discount]]]:
"""
Applies any dynamic discounts to a cart
:param event: Event the cart belongs to
:param sales_channel: Sales channel the cart was created with
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
@@ -192,14 +191,12 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
result = discount.apply({
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount)
for
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)
in enumerate(positions)
idx: PositionInfo(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount) in enumerate(positions)
if not is_bundled and idx not in new_prices
}, collect_potential_discounts)
for k in result.keys():
result[k] = (result[k], discount)
new_prices.update(result)
return [new_prices.get(idx, (p[3], None)) for idx, p in enumerate(positions)]
return [new_prices.get(idx, (p[2], None)) for idx, p in enumerate(positions)]
+2 -2
View File
@@ -134,13 +134,13 @@ def order_overview(
qs = qs.filter(item__admission=True)
items = items.filter(admission=True)
if date_from and isinstance(date_from, date) and not isinstance(date_from, datetime):
if date_from and isinstance(date_from, date):
date_from = make_aware(datetime.combine(
date_from,
time(hour=0, minute=0, second=0, microsecond=0)
), event.timezone)
if date_until and isinstance(date_until, date) and not isinstance(date_until, datetime):
if date_until and isinstance(date_until, date):
date_until = make_aware(datetime.combine(
date_until + timedelta(days=1),
time(hour=0, minute=0, second=0, microsecond=0)
+1 -4
View File
@@ -62,9 +62,6 @@ class VATIDTemporaryError(VATIDError):
def _validate_vat_id_NO(vat_id, country_code):
# Inspired by vat_moss library
if not vat_id.startswith("NO"):
# prefix is not usually used in Norway, but expected by vat_moss library
vat_id = "NO" + vat_id
try:
vat_id = vat_moss.id.normalize(vat_id)
except ValueError:
@@ -155,7 +152,7 @@ def _validate_vat_id_EU(vat_id, country_code):
valid_elements = envelope.findall('./soap:Body/vat:checkVatResponse/vat:valid', namespaces)
if not valid_elements:
logger.error(
f'VAT ID checking failed for {country_code} due to missing <valid> tag, response was: {return_xml}'
f'VAT ID checking failed for {country_code} due to missing <valid> tag'
)
raise VATIDTemporaryError(error_messages['unavailable'])
+13 -65
View File
@@ -71,7 +71,6 @@ from pretix.base.reldate import (
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
SerializerRelativeDateField, SerializerRelativeDateTimeField,
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
)
@@ -1234,18 +1233,14 @@ DEFAULTS = {
'invoice_email_organizer': {
'default': '',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
'form_class': forms.EmailField,
'serializer_class': serializers.EmailField,
'form_kwargs': dict(
label=_("Email address to receive a copy of each invoice"),
help_text=_("Each newly created invoice will be sent to this email address shortly after creation. You can "
"use this for an automated import of invoices to your accounting system. The invoice will be "
"the only attachment of the email."),
validators=[multimail_validate],
),
'serializer_kwargs': dict(
validators=[multimail_validate],
),
)
},
'show_items_outside_presale_period': {
'default': 'True',
@@ -1313,13 +1308,10 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_class': forms.BooleanField,
'form_kwargs': dict(
label=_("This shop represents an event"),
help_text=_(
"Uncheck this box if you are only selling something that has no specific date, such as gift cards or a "
"ticket that can be used any time. The system will then stop showing the event date in some places like "
"the event start page. Note that pretix still is a system built around events and the date may still "
"show up in other places."
),
label=_("Show event times and dates on the ticket shop"),
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
"also affects a few other locations, however it should not be expected that the date of the "
"event is shown nowhere to users."),
)
},
'show_date_to': {
@@ -2063,38 +2055,6 @@ DEFAULTS = {
),
'serializer_class': I18nURLField,
},
'accessibility_url': {
'default': None,
'type': LazyI18nString,
'form_class': I18nURLFormField,
'form_kwargs': dict(
label=_("Accessibility information URL"),
help_text=_("This should point e.g. to a part of your website that explains how your ticket shop complies "
"with accessibility regulation."),
widget=I18nTextInput,
),
'serializer_class': I18nURLField,
},
'accessibility_title': {
'default': LazyI18nString.from_gettext(gettext_noop("Accessibility information")),
'type': LazyI18nString,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Accessibility information title"),
widget=I18nTextInput,
),
'serializer_class': I18nURLField,
},
'accessibility_text': {
'default': None,
'type': LazyI18nString,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Accessibility information text"),
widget=I18nMarkdownTextarea,
),
'serializer_class': I18nURLField,
},
'confirm_texts': {
'default': LazyI18nStringList(),
'type': LazyI18nStringList,
@@ -2145,7 +2105,7 @@ DEFAULTS = {
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Event description"),
widget=I18nTextarea,
widget=I18nMarkdownTextarea,
help_text=_(
"You can use this to share information with your attendees, such as travel information or the link to a digital event. "
"If you keep it empty, we will put a link to the event shop, the admission time, and your organizer name in there. "
@@ -2815,7 +2775,7 @@ Your {organizer} team""")) # noqa: W291
),
},
'theme_color_success': {
'default': '#408252',
'default': '#50a167',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -2920,8 +2880,7 @@ Your {organizer} team""")) # noqa: W291
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
help_text=_('If you provide a logo image, we will by default not show your event name and date '
'in the page header. If you use a white background, we show your logo with a size of up '
'to 1140x120 pixels. Otherwise the maximum size is 1120x120 pixels. You '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.')
),
@@ -2964,8 +2923,7 @@ Your {organizer} team""")) # noqa: W291
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
help_text=_('If you provide a logo image, we will by default not show your organization name '
'in the page header. If you use a white background, we show your logo with a size of up '
'to 1140x120 pixels. Otherwise the maximum size is 1120x120 pixels. You '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.')
),
@@ -3026,7 +2984,7 @@ Your {organizer} team""")) # noqa: W291
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
'if only the center square is shown. If you do not fill this, we will use the logo given above.')
'only the center square is shown. If you do not fill this, we will use the logo given above.')
),
'serializer_class': UploadedFileField,
'serializer_kwargs': dict(
@@ -3330,8 +3288,6 @@ Your {organizer} team""")) # noqa: W291
label=_('Validity of gift card codes in years'),
help_text=_('If you set a number here, gift cards will by default expire at the end of the year after this '
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
min_value=0,
max_value=99,
)
},
'cookie_consent': {
@@ -3744,21 +3700,13 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
# are actually *used* in postal addresses. This is obviously not complete and opinionated.
# Country: [(List of subdivision types as defined by pycountry), (short or long form to be used)]
'AU': (['State', 'Territory'], 'short'),
'BR': (['Federal district', 'State'], 'short'),
'BR': (['State'], 'short'),
'CA': (['Province', 'Territory'], 'short'),
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
'JP': (['Prefecture'], 'long'),
'MY': (['State', 'Federal territory'], 'long'),
'MX': (['State', 'Federal district'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
'IT': (['Province', 'Free municipal consortium', 'Metropolitan city', 'Autonomous province',
'Free municipal consortium', 'Decentralized regional entity'], 'short'),
}
COUNTRY_STATE_LABEL = {
# Countries in which the "State" field should not be called "State"
'CA': pgettext_lazy('address', 'Province'),
'JP': pgettext_lazy('address', 'Prefecture'),
'IT': pgettext_lazy('address', 'Province'),
}
settings_hierarkey = Hierarkey(attribute_name='settings')
+1 -1
View File
@@ -370,7 +370,7 @@ class WaitingListShredder(BaseDataShredder):
def generate_files(self) -> List[Tuple[str, str, str]]:
yield 'waiting-list.json', 'application/json', json.dumps([
WaitingListSerializer(wle, context={"event": self.event}).data
WaitingListSerializer(wle).data
for wle in self.event.waitinglistentries.all()
], indent=4)
+1 -10
View File
@@ -46,6 +46,7 @@ app_cache = {}
def _populate_app_cache():
global app_cache
apps.check_apps_ready()
for ac in apps.app_configs.values():
app_cache[ac.name] = ac
@@ -562,16 +563,6 @@ as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_expiry_changed = EventPluginSignal()
"""
Arguments: ``order``
This signal is sent out every time an order expiry date is changed as an explicit operation (i.e. not if
this is the result of an approval or order change). The order object is given as the first argument.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
order_modified = EventPluginSignal()
"""
Arguments: ``order``
+1 -1
View File
@@ -28,7 +28,7 @@
<code>Host: {{ request.headers.Host }}</code>
{% if xfh %}
<br>
<code>X-Forwarded-Host: {{ xfh }}</code>
<code>X-Forwarded-For: {{ xfh }}</code>
{% if not settings.USE_X_FORWARDED_HOST %}({% trans "ignored" %}){% endif %}
{% endif %}
</dd>
-6
View File
@@ -1,6 +0,0 @@
{% load i18n %}
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 18 14"
class="{{ cls }}" role="img" aria-label="{% trans "Seat" %}">
<path d="M7.713 3.573c-.787-.124-1.677.472-1.511 1.529l.857 3.473c.116.579.578 1.086 1.317 1.086h3.166v3.504c0 1.108 1.556 1.113 1.556.019V8.682c0-.536-.376-1.116-1.099-1.116L9.52 7.563l-.752-2.936c-.147-.648-.583-.981-1.055-1.055v.001Z"></path>
<path d="M4.48 5.835a.6.6 0 0 0-.674.725l.71 3.441c.287 1.284 1.39 2.114 2.495 2.114h2.273c.807 0 .811-1.215-.01-1.215H7.188c-.753 0-1.375-.45-1.563-1.289l-.672-3.293c-.062-.3-.26-.452-.474-.483ZM7.433.102a1.468 1.468 0 1 0 0 2.937 1.469 1.469 0 1 0 0-2.937Z"></path>
</svg>
@@ -4,7 +4,7 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, user-scalable=false">
<style type="text/css">
body {
background-color: #eee;
@@ -15,7 +15,10 @@
{{ event.name }}
<br>
{% if event.has_subevents and ev.name|upper != event.name|upper %}{{ ev.name }}<br>{% endif %}
{{ ev.get_date_range_display_with_times }}
{{ ev.get_date_range_display }}
{% if event.settings.show_times %}
{{ ev.date_from|date:"TIME_FORMAT" }}
{% endif %}
</td>
</tr>
<tr>
@@ -104,7 +107,10 @@
{% if groupkey.2.name|upper != event.name|upper %}
{{ groupkey.2.name }} &middot;
{% endif %}
{{ groupkey.2.get_date_range_display_with_times }}
{{ groupkey.2.get_date_range_display }}
{% if event.settings.show_times %}
{{ groupkey.2.date_from|date:"TIME_FORMAT" }}
{% endif %}
{% if groupkey.2.location %}
<br>
{{ groupkey.2.location|oneline }}
-61
View File
@@ -1,61 +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 django import template
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from pretix.helpers.templatetags.simple_block_tag import (
register_simple_block_tag,
)
from django.utils.translation import gettext_lazy as _ # NOQA
register = template.Library()
@register_simple_block_tag(register)
def dialog(content, html_id, title, description, *args, **kwargs):
format_kwargs = {
"id": html_id,
"title": title,
"description": description,
"icon": format_html('<div class="modal-card-icon"><span class="fa fa-{}" aria-hidden="true"></span></div>', kwargs["icon"]) if "icon" in kwargs else "",
"alert": mark_safe('role="alertdialog"') if kwargs.get("alert", "False") != "False" else "",
"content": content,
}
result = """
<dialog {alert}
id="{id}" class="modal-card"
aria-labelledby="{id}-title"
aria-describedby="{id}-description">
<form method="dialog" class="modal-card-inner form-horizontal">
{icon}
<div class="modal-card-content">
<h2 id="{id}-title" class="modal-card-title h3">{title}</h2>
<p id="{id}-description" class="modal-card-description">{description}</p>
{content}
</div>
</form>
</dialog>
"""
return format_html(result, **format_kwargs)
-1
View File
@@ -73,7 +73,6 @@ class EventSlugBanlistValidator(BanlistValidator):
'customer',
'account',
'lead',
'accessibility',
]
+2 -9
View File
@@ -21,15 +21,12 @@
#
import pycountry
from django.http import JsonResponse
from django.utils.translation import pgettext
from pretix.base.addressvalidation import (
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
)
from pretix.base.models.tax import VAT_ID_COUNTRIES
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
)
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
def states(request):
@@ -38,11 +35,7 @@ def states(request):
'street': {'required': True},
'zipcode': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
'city': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
'state': {
'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS,
'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS,
'label': COUNTRY_STATE_LABEL.get(cc, pgettext('address', 'State')),
},
'state': {'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS, 'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS},
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
}
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
+1 -1
View File
@@ -80,4 +80,4 @@ def serve_metrics(request):
content = "\n".join(output) + "\n"
return HttpResponse(content, content_type="text/plain;version=1.0.0;escaping=allow-utf-8")
return HttpResponse(content)
+11 -9
View File
@@ -68,7 +68,7 @@ class AsyncMixin:
def get_check_url(self, task_id, ajax):
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
def _ajax_response_data(self, value):
def _ajax_response_data(self):
return {}
def _return_ajax_result(self, res, timeout=.5):
@@ -85,7 +85,7 @@ class AsyncMixin:
logger.warning('Ignored ResponseError in AsyncResult.get()')
except ConnectionError:
# Redis probably just restarted, let's just report not ready and retry next time
data = self._ajax_response_data(None)
data = self._ajax_response_data()
data.update({
'async_id': res.id,
'ready': False
@@ -93,7 +93,7 @@ class AsyncMixin:
return data
state, info = res.state, res.info
data = self._ajax_response_data(info)
data = self._ajax_response_data()
data.update({
'async_id': res.id,
'ready': ready,
@@ -102,21 +102,23 @@ class AsyncMixin:
if ready:
if state == states.SUCCESS and not isinstance(info, Exception):
smes = self.get_success_message(info)
if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
if smes:
messages.success(self.request, smes)
# TODO: Do not store message if the ajax client states that it will not redirect
# but handle the message itself
data.update({
'redirect': self.get_success_url(info),
'success': True,
'message': str(smes)
'message': str(self.get_success_message(info))
})
else:
smes = self.get_error_message(info)
if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
messages.error(self.request, smes)
messages.error(self.request, self.get_error_message(info))
# TODO: Do not store message if the ajax client states that it will not redirect
# but handle the message itself
data.update({
'redirect': self.get_error_url(),
'success': False,
'message': str(smes)
'message': str(self.get_error_message(info))
})
elif state == 'PROGRESS':
data.update({
+3 -4
View File
@@ -43,7 +43,7 @@ from django.utils.translation import get_language
from django_scopes import scope
from pretix.base.models.auth import StaffSession
from pretix.base.settings import COUNTRY_STATE_LABEL, GlobalSettingsObject
from pretix.base.settings import GlobalSettingsObject
from pretix.control.navigation import (
get_event_navigation, get_global_navigation, get_organizer_navigation,
)
@@ -81,13 +81,13 @@ def _default_context(request):
'DEBUG': settings.DEBUG,
}
_html_head = []
if getattr(request, 'event', None) and request.user.is_authenticated:
if hasattr(request, 'event') and request.user.is_authenticated:
for receiver, response in html_head.send(request.event, request=request):
_html_head.append(response)
ctx['html_head'] = "".join(_html_head)
_js_payment_weekdays_disabled = '[]'
if getattr(request, 'event', None) and getattr(request, 'organizer', None) and request.user.is_authenticated:
if getattr(request, 'event', None) and hasattr(request, 'organizer') and request.user.is_authenticated:
ctx['nav_items'] = get_event_navigation(request)
if request.event.settings.get('payment_term_weekdays'):
@@ -140,7 +140,6 @@ def _default_context(request):
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
ctx['js_locale'] = get_moment_locale()
ctx['select2locale'] = get_language()[:2]
ctx['COUNTRY_STATE_LABEL'] = COUNTRY_STATE_LABEL
ctx['warning_update_available'] = False
ctx['warning_update_check_active'] = False
-6
View File
@@ -45,8 +45,6 @@ class DiscountForm(I18nModelForm):
'limit_sales_channels',
'available_from',
'available_until',
'subevent_date_from',
'subevent_date_until',
'subevent_mode',
'condition_all_products',
'condition_limit_products',
@@ -64,8 +62,6 @@ class DiscountForm(I18nModelForm):
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
'subevent_date_from': SplitDateTimeField,
'subevent_date_until': SplitDateTimeField,
'condition_limit_products': ItemMultipleChoiceField,
'benefit_limit_products': ItemMultipleChoiceField,
'limit_sales_channels': SafeModelMultipleChoiceField,
@@ -74,8 +70,6 @@ class DiscountForm(I18nModelForm):
'subevent_mode': forms.RadioSelect,
'available_from': SplitDateTimePickerWidget(),
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
'subevent_date_from': SplitDateTimePickerWidget(),
'subevent_date_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_subevent_date_from_0'}),
'condition_limit_products': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '<[name$=all_products]',
'class': 'scrolling-multiple-choice',
+2 -6
View File
@@ -175,7 +175,6 @@ class EventWizardBasicsForm(I18nModelForm):
'presale_start',
'presale_end',
'location',
'is_remote',
'geo_lat',
'geo_lon',
]
@@ -232,7 +231,7 @@ class EventWizardBasicsForm(I18nModelForm):
raise ValidationError({
'timezone': _('Your default locale must be specified.')
})
if not data.get("no_taxes") and data.get("tax_rate") is None:
if not data.get("no_taxes") and not data.get("tax_rate"):
raise ValidationError({
'tax_rate': _('You have not specified a tax rate. If you do not want us to compute sales taxes, please '
'check "{field}" above.').format(field=self.fields["no_taxes"].label)
@@ -449,7 +448,6 @@ class EventUpdateForm(I18nModelForm):
'presale_start',
'presale_end',
'location',
'is_remote',
'geo_lat',
'geo_lon',
'all_sales_channels',
@@ -1477,9 +1475,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
def __iter__(self):
for country_code, country_name in super().__iter__():
yield country_code, country_name
if country_code in COUNTRIES_WITH_STATE_IN_ADDRESS and country_code not in {"IT"}:
# Special case for Italy: Provinces are used in addresses, but are too low-level to
# have influence on taxes, so we avoid the bloat in the list of selectable countries.
if country_code in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[country_code]
yield from sorted(((state.code, country_name + " - " + state.name)
for state in pycountry.subdivisions.get(country_code=country_code)
+17 -12
View File
@@ -70,7 +70,6 @@ from pretix.helpers.database import (
)
from pretix.helpers.dicts import move_to_end
from pretix.helpers.i18n import get_format_without_seconds, i18ncomp
from pretix.helpers.models import flatten_choices
PAYMENT_PROVIDERS = []
@@ -178,10 +177,10 @@ class FilterForm(forms.Form):
elif isinstance(v, Model):
val = '"' + str(v) + '"'
elif isinstance(f, forms.MultipleChoiceField):
valdict = dict(flatten_choices(f.choices))
valdict = dict(f.choices)
val = ' or '.join([str(valdict.get(m)) for m in v])
elif isinstance(f, forms.ChoiceField):
val = str(dict(flatten_choices(f.choices)).get(v))
val = str(dict(f.choices).get(v))
elif isinstance(v, datetime):
val = date_format(v, 'SHORT_DATETIME_FORMAT')
elif isinstance(v, Decimal):
@@ -197,6 +196,7 @@ class OrderFilterForm(FilterForm):
label=_('Search for…'),
widget=forms.TextInput(attrs={
'placeholder': _('Search for…'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -267,10 +267,9 @@ class OrderFilterForm(FilterForm):
Q(invoice_no__in=invoice_nos)
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
matching_positions = OrderPosition.all.filter(
matching_positions = OrderPosition.objects.filter(
Q(
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
| Q(company__icontains=u)
| Q(secret__istartswith=u)
| Q(pseudonymization_id__istartswith=u)
)
@@ -850,18 +849,12 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
).distinct()
for q in self.event.questions.all():
if fdata.get(f'question_{q.pk}'):
if q.type in (Question.TYPE_BOOLEAN, Question.TYPE_NUMBER):
if q.type == Question.TYPE_BOOLEAN:
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
answer__exact=fdata.get(f'question_{q.pk}')
)
elif q.type in (Question.TYPE_DATE, Question.TYPE_TIME, Question.TYPE_DATETIME):
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
orderposition__order_id=OuterRef('pk'),
answer__exact=str(fdata.get(f'question_{q.pk}'))
)
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
answers = QuestionAnswer.objects.filter(
question_id=q.pk,
@@ -987,6 +980,7 @@ class OrderPaymentSearchFilterForm(forms.Form):
label=_('Search for…'),
widget=forms.TextInput(attrs={
'placeholder': _('Search for…'),
'autofocus': 'autofocus'
}),
required=False,
)
@@ -1248,6 +1242,7 @@ class SubEventFilterForm(FilterForm):
label=_('Event name'),
widget=forms.TextInput(attrs={
'placeholder': _('Event name'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -1380,6 +1375,7 @@ class OrganizerFilterForm(FilterForm):
label=_('Organizer name'),
widget=forms.TextInput(attrs={
'placeholder': _('Organizer name'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -1437,6 +1433,7 @@ class GiftCardFilterForm(FilterForm):
label=_('Search query'),
widget=forms.TextInput(attrs={
'placeholder': _('Search query'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -1488,6 +1485,7 @@ class CustomerFilterForm(FilterForm):
label=_('Search query'),
widget=forms.TextInput(attrs={
'placeholder': _('Search query'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -1560,6 +1558,7 @@ class ReusableMediaFilterForm(FilterForm):
label=_('Search query'),
widget=forms.TextInput(attrs={
'placeholder': _('Search query'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -1614,6 +1613,7 @@ class TeamFilterForm(FilterForm):
label=_('Search query'),
widget=forms.TextInput(attrs={
'placeholder': _('Search query'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -1695,6 +1695,7 @@ class EventFilterForm(FilterForm):
label=_('Event name'),
widget=forms.TextInput(attrs={
'placeholder': _('Event name'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -1877,6 +1878,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
label=_('Search attendee…'),
widget=forms.TextInput(attrs={
'placeholder': _('Search attendee…'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -2025,6 +2027,7 @@ class UserFilterForm(FilterForm):
label=_('Search query'),
widget=forms.TextInput(attrs={
'placeholder': _('Search query'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -2116,6 +2119,7 @@ class VoucherFilterForm(FilterForm):
label=_('Search voucher'),
widget=forms.TextInput(attrs={
'placeholder': _('Search voucher'),
'autofocus': 'autofocus'
}),
required=False
)
@@ -2593,6 +2597,7 @@ class DeviceFilterForm(FilterForm):
label=_('Search query'),
widget=forms.TextInput(attrs={
'placeholder': _('Search query'),
'autofocus': 'autofocus'
}),
required=False
)
+16 -12
View File
@@ -120,7 +120,6 @@ class CategoryForm(I18nModelForm):
self.fields['cross_selling_condition'].widget.attrs['data-disable-dependent'] = 'true'
self.fields['cross_selling_condition'].widget.choices = self.fields['cross_selling_condition'].widget.choices[1:]
self.fields['cross_selling_condition'].required = False
self.fields['cross_selling_condition']._required = True # Do not display "Optional" label
self.fields['cross_selling_match_products'].widget = forms.CheckboxSelectMultiple(
attrs={
@@ -477,7 +476,6 @@ class ItemCreateForm(I18nModelForm):
'show_quota_left',
'hidden_if_available',
'hidden_if_item_available',
'hidden_if_item_available_mode',
'require_bundling',
'require_membership',
'grant_membership_type',
@@ -648,12 +646,18 @@ class ItemUpdateForm(I18nModelForm):
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons=Item.UNAVAIL_MODE_ICONS
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_ICONS
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect(
@@ -668,11 +672,6 @@ class ItemUpdateForm(I18nModelForm):
attrs={'data-checkbox-dependency': '#id_require_voucher'}
)
self.fields['hidden_if_item_available_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['hidden_if_item_available_mode'].choices,
option_icons=Item.UNAVAIL_MODE_ICONS
)
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(
@@ -854,7 +853,6 @@ class ItemUpdateForm(I18nModelForm):
'show_quota_left',
'hidden_if_available',
'hidden_if_item_available',
'hidden_if_item_available_mode',
'issue_giftcard',
'require_membership',
'require_membership_types',
@@ -972,12 +970,18 @@ class ItemVariationForm(I18nModelForm):
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons=Item.UNAVAIL_MODE_ICONS
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_ICONS
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
)
self.meta_fields = []
-11
View File
@@ -25,8 +25,6 @@ import socket
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from pretix.base.forms import SecretKeySettingsField, SettingsForm
@@ -56,15 +54,6 @@ class SMTPMailForm(SettingsForm):
smtp_password = SecretKeySettingsField(
label=_("Password"),
required=False,
validators=[RegexValidator(
r"^[A-Za-z0-9!\"#$%&'()*+,./:;<=>?@\^_`{}|~-]+$",
message=format_lazy(
_("The password contains characters not supported by our email system. Please only use characters "
"A-Z, a-z, 0-9, and common special characters ({characters})."),
characters=r'!"#$%%&\'()*+,-./:;<=>?@\^_`{}|~'
)
)]
)
smtp_use_tls = forms.BooleanField(
label=_("Use STARTTLS"),
-11
View File
@@ -33,7 +33,6 @@
# 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 os.path
from datetime import date, datetime, time
from decimal import Decimal
@@ -69,7 +68,6 @@ 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
from pretix.helpers.hierarkey import clean_filename
from pretix.helpers.money import change_decimal_field
@@ -725,9 +723,6 @@ class OrderMailForm(forms.Form):
help_text=_("Will be ignored if tickets exceed a given size limit to ensure email deliverability."),
required=False
)
attach_new_order = forms.BooleanField(
required=False
)
attach_invoices = forms.ModelMultipleChoiceField(
label=_("Attach invoices"),
widget=forms.CheckboxSelectMultiple,
@@ -764,12 +759,6 @@ class OrderMailForm(forms.Form):
self.fields['attach_invoices'].queryset = order.invoices.all()
self._set_field_placeholders('message', ['event', 'order'])
self._set_field_placeholders('subject', ['event', 'order'])
if order.event.settings.mail_attachment_new_order:
self.fields['attach_new_order'].label = _('Attach {file}').format(
file=clean_filename(os.path.basename(order.event.settings.mail_attachment_new_order.name))
)
else:
del self.fields['attach_new_order']
class OrderPositionMailForm(OrderMailForm):
+2 -15
View File
@@ -498,9 +498,6 @@ class OrganizerSettingsForm(SettingsForm):
'theme_round_borders',
'primary_font',
'privacy_url',
'accessibility_url',
'accessibility_title',
'accessibility_text',
'cookie_consent',
'cookie_consent_dialog_title',
'cookie_consent_dialog_text',
@@ -525,8 +522,7 @@ class OrganizerSettingsForm(SettingsForm):
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
required=False,
help_text=_('If you provide a logo image, we will by default not show your organization name '
'in the page header. If you use a white background, we show your logo with a size of up '
'to 1140x120 pixels. Otherwise the maximum size is 1120x120 pixels. You '
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
'can increase the size with the setting below. We recommend not using small details on the picture '
'as it will be resized on smaller screens.')
)
@@ -1047,15 +1043,6 @@ class SSOProviderForm(I18nModelForm):
label=pgettext_lazy('sso_oidc', 'Phone field'),
required=False,
)
config_oidc_query_parameters = forms.CharField(
label=pgettext_lazy('sso_oidc', 'Query parameters'),
help_text=pgettext_lazy('sso_oidc', 'Optional query parameters, that will be added to calls to '
'the authorization endpoint. Enter as: {example}'.format(
example='<code>param1=value1&amp;param2=value2</code>'
),
),
required=False,
)
class Meta:
model = CustomerSSOProvider
@@ -1117,7 +1104,7 @@ class SSOClientForm(I18nModelForm):
class Meta:
model = CustomerSSOClient
fields = ['is_active', 'name', 'client_id', 'client_type', 'authorization_grant_type', 'redirect_uris',
'allowed_scopes', 'require_pkce']
'allowed_scopes']
widgets = {
'authorization_grant_type': forms.RadioSelect,
'client_type': forms.RadioSelect,
-7
View File
@@ -178,13 +178,6 @@ class SubEventBulkEditForm(I18nModelForm):
widgets = {
}
def clean(self):
data = super().clean()
if self.prefix + "name" in self.data.getlist('_bulk'):
if not data.get("name"):
self.add_error("name", _("This field is required."))
return data
def save(self, commit=True):
objs = list(self.queryset)
fields = set()
+258 -312
View File
@@ -33,16 +33,17 @@
# 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 json
from collections import defaultdict
from decimal import Decimal
from typing import Optional
import bleach
import dateutil.parser
from django.dispatch import receiver
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import escape, format_html
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.strings import LazyI18nString
@@ -67,212 +68,128 @@ OVERVIEW_BANLIST = [
]
class OrderChangeLogEntryType(OrderLogEntryType):
prefix = _('The order has been changed:')
def _display_order_changed(event: Event, logentry: LogEntry):
data = json.loads(logentry.data)
def display(self, logentry, data):
return format_html('{} {}', self.prefix, self.display_prefixed(logentry.event, logentry, data))
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return super().display(logentry, data)
class OrderPositionChangeLogEntryType(OrderChangeLogEntryType):
prefix = _('The order has been changed:')
def display(self, logentry, data):
return super().display(logentry, {**data, 'posid': data.get('positionid', '?')})
@log_entry_types.new()
class OrderItemChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.item'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
text = _('The order has been changed:')
if logentry.action_type == 'pretix.event.order.changed.item':
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['old_variation']))
new_item = str(event.items.get(pk=data['new_item']))
if data['new_variation']:
new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation']))
return _('Position #{posid}: {old_item} ({old_price}) changed to {new_item} ({new_price}).').format(
return text + ' ' + _('Position #{posid}: {old_item} ({old_price}) changed '
'to {new_item} ({new_price}).').format(
posid=data.get('positionid', '?'),
old_item=old_item, new_item=new_item,
old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency),
)
@log_entry_types.new()
class OrderMembershipChanged(OrderPositionChangeLogEntryType):
action_type = 'pretix.event.order.changed.membership'
plain = _('Position #{posid}: Used membership changed.')
@log_entry_types.new()
class OrderSeatChanged(OrderPositionChangeLogEntryType):
action_type = 'pretix.event.order.changed.seat'
plain = _('Position #{posid}: Seat "{old_seat}" changed to "{new_seat}".')
@log_entry_types.new()
class OrderSubeventChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.subevent'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
elif logentry.action_type == 'pretix.event.order.changed.membership':
return text + ' ' + _('Position #{posid}: Used membership changed.').format(
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.seat':
return text + ' ' + _('Position #{posid}: Seat "{old_seat}" changed '
'to "{new_seat}".').format(
posid=data.get('positionid', '?'),
old_seat=data.get('old_seat'), new_seat=data.get('new_seat'),
)
elif logentry.action_type == 'pretix.event.order.changed.subevent':
old_se = str(event.subevents.get(pk=data['old_subevent']))
new_se = str(event.subevents.get(pk=data['new_subevent']))
return _('Position #{posid}: Event date "{old_event}" ({old_price}) changed '
'to "{new_event}" ({new_price}).').format(
return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price}) changed '
'to "{new_event}" ({new_price}).').format(
posid=data.get('positionid', '?'),
old_event=old_se, new_event=new_se,
old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency),
)
@log_entry_types.new()
class OrderPriceChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.price'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('Price of position #{posid} changed from {old_price} to {new_price}.').format(
elif logentry.action_type == 'pretix.event.order.changed.price':
return text + ' ' + _('Price of position #{posid} changed from {old_price} '
'to {new_price}.').format(
posid=data.get('positionid', '?'),
old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency),
)
@log_entry_types.new()
class OrderTaxRuleChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.tax_rule'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
elif logentry.action_type == 'pretix.event.order.changed.tax_rule':
if 'positionid' in data:
return _('Tax rule of position #{posid} changed from {old_rule} to {new_rule}.').format(
return text + ' ' + _('Tax rule of position #{posid} changed from {old_rule} '
'to {new_rule}.').format(
posid=data.get('positionid', '?'),
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
)
elif 'fee' in data:
return _('Tax rule of fee #{fee} changed from {old_rule} to {new_rule}.').format(
return text + ' ' + _('Tax rule of fee #{fee} changed from {old_rule} '
'to {new_rule}.').format(
fee=data.get('fee', '?'),
old_rule=TaxRule.objects.get(pk=data['old_taxrule']) if data['old_taxrule'] else '',
new_rule=TaxRule.objects.get(pk=data['new_taxrule']),
)
@log_entry_types.new()
class OrderFeeAdded(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.addfee'
plain = _('A fee has been added')
@log_entry_types.new()
class OrderFeeChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.feevalue'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('A fee was changed from {old_price} to {new_price}.').format(
elif logentry.action_type == 'pretix.event.order.changed.addfee':
return text + ' ' + str(_('A fee has been added'))
elif logentry.action_type == 'pretix.event.order.changed.feevalue':
return text + ' ' + _('A fee was changed from {old_price} to {new_price}.').format(
old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency),
)
@log_entry_types.new()
class OrderFeeRemoved(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.cancelfee'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('A fee of {old_price} was removed.').format(
elif logentry.action_type == 'pretix.event.order.changed.cancelfee':
return text + ' ' + _('A fee of {old_price} was removed.').format(
old_price=money_filter(Decimal(data['old_price']), event.currency),
)
@log_entry_types.new()
class OrderCanceled(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.cancel'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
elif logentry.action_type == 'pretix.event.order.changed.cancel':
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
return _('Position #{posid} ({old_item}, {old_price}) canceled.').format(
return text + ' ' + _('Position #{posid} ({old_item}, {old_price}) canceled.').format(
posid=data.get('positionid', '?'),
old_item=old_item,
old_price=money_filter(Decimal(data['old_price']), event.currency),
)
@log_entry_types.new()
class OrderPositionAdded(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.add'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
elif logentry.action_type == 'pretix.event.order.changed.add':
item = str(event.items.get(pk=data['item']))
if data['variation']:
item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation']))
if data['addon_to']:
addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to'])
return _('Position #{posid} created: {item} ({price}) as an add-on to position #{addon_to}.').format(
return text + ' ' + _('Position #{posid} created: {item} ({price}) as an add-on to '
'position #{addon_to}.').format(
posid=data.get('positionid', '?'),
item=item, addon_to=addon_to.positionid,
price=money_filter(Decimal(data['price']), event.currency),
)
else:
return _('Position #{posid} created: {item} ({price}).').format(
return text + ' ' + _('Position #{posid} created: {item} ({price}).').format(
posid=data.get('positionid', '?'),
item=item,
price=money_filter(Decimal(data['price']), event.currency),
)
@log_entry_types.new()
class OrderSecretChanged(OrderPositionChangeLogEntryType):
action_type = 'pretix.event.order.changed.secret'
plain = _('A new secret has been generated for position #{posid}.')
@log_entry_types.new()
class OrderValidFromChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.valid_from'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('The validity start date for position #{posid} has been changed to {value}.').format(
elif logentry.action_type == 'pretix.event.order.changed.secret':
return text + ' ' + _('A new secret has been generated for position #{posid}.').format(
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.valid_from':
return text + ' ' + _('The validity start date for position #{posid} has been changed to {value}.').format(
posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get(
'new_value') else ''
)
@log_entry_types.new()
class OrderValidUntilChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.valid_until'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
return _('The validity end date for position #{posid} has been changed to {value}.').format(
elif logentry.action_type == 'pretix.event.order.changed.valid_until':
return text + ' ' + _('The validity end date for position #{posid} has been changed to {value}.').format(
posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get('new_value') else ''
)
@log_entry_types.new()
class OrderChangedBlockAdded(OrderPositionChangeLogEntryType):
action_type = 'pretix.event.order.changed.add_block'
plain = _('A block has been added for position #{posid}.')
@log_entry_types.new()
class OrderChangedBlockRemoved(OrderPositionChangeLogEntryType):
action_type = 'pretix.event.order.changed.remove_block'
plain = _('A block has been removed for position #{posid}.')
@log_entry_types.new()
class OrderChangedSplit(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.split'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
elif logentry.action_type == 'pretix.event.order.changed.add_block':
return text + ' ' + _('A block has been added for position #{posid}.').format(
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.remove_block':
return text + ' ' + _('A block has been removed for position #{posid}.').format(
posid=data.get('positionid', '?'),
)
elif logentry.action_type == 'pretix.event.order.changed.split':
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
@@ -281,155 +198,194 @@ class OrderChangedSplit(OrderChangeLogEntryType):
'organizer': event.organizer.slug,
'code': data['new_order']
})
return format_html(
_('Position #{posid} ({old_item}, {old_price}) split into new order: {order}'),
return mark_safe(escape(text) + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
old_item=escape(old_item),
posid=data.get('positionid', '?'),
order=format_html('<a href="{}">{}</a>', url, data['new_order']),
order='<a href="{}">{}</a>'.format(url, data['new_order']),
old_price=money_filter(Decimal(data['old_price']), event.currency),
))
elif logentry.action_type == 'pretix.event.order.changed.split_from':
return _('This order has been created by splitting the order {order}').format(
order=data['original_order'],
)
@log_entry_types.new()
class OrderChangedSplitFrom(OrderLogEntryType):
action_type = 'pretix.event.order.changed.split_from'
def _display_checkin(event, logentry):
data = logentry.parsed_data
def display(self, logentry: LogEntry, data):
url = reverse('control:event.order', kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
'code': data['original_order']
})
return format_html(
_('This order has been created by splitting the order {order}'),
order=format_html('<a href="{}">{}</a>', url, data['original_order']),
)
show_dt = False
if 'datetime' in data:
dt = dateutil.parser.parse(data.get('datetime'))
show_dt = abs((logentry.datetime - dt).total_seconds()) > 5 or 'forced' in data
tz = event.timezone
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data:
try:
checkin_list = event.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
@log_entry_types.new_from_dict({
'pretix.event.checkin.unknown': (
_('Unknown scan of code "{barcode}" at {datetime} for list "{list}", type "{type}".'),
_('Unknown scan of code "{barcode}" for list "{list}", type "{type}".'),
),
'pretix.event.checkin.revoked': (
_('Scan of revoked code "{barcode}" at {datetime} for list "{list}", type "{type}", was uploaded.'),
_('Scan of revoked code "{barcode}" for list "{list}", type "{type}", was uploaded.'),
),
'pretix.event.checkin.denied': (
_('Denied scan of position #{posid} at {datetime} for list "{list}", type "{type}", error code "{errorcode}".'),
_('Denied scan of position #{posid} for list "{list}", type "{type}", error code "{errorcode}".'),
),
'pretix.control.views.checkin.reverted': _('The check-in of position #{posid} on list "{list}" has been reverted.'),
'pretix.event.checkin.reverted': _('The check-in of position #{posid} on list "{list}" has been reverted.'),
})
class CheckinErrorLogEntryType(OrderLogEntryType):
def display(self, logentry: LogEntry, data):
return self.display_plain(self.plain, logentry, data)
def display_plain(self, plain, logentry: LogEntry, data):
if isinstance(plain, tuple):
plain_with_dt, plain_without_dt = plain
else:
plain_with_dt, plain_without_dt = plain, plain
data = defaultdict(lambda: '?', data)
event = logentry.event
if 'list' in data and event:
try:
data['list'] = event.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
data['list'] = _("(unknown)")
else:
data['list'] = _("(unknown)")
data['barcode'] = data.get('barcode', '')[:16]
data['posid'] = logentry.parsed_data.get('positionid', '?')
if 'datetime' in data:
dt = dateutil.parser.parse(data.get('datetime'))
if abs((logentry.datetime - dt).total_seconds()) > 5 or data.get('forced'):
if event:
data['datetime'] = date_format(dt.astimezone(event.timezone), "SHORT_DATETIME_FORMAT")
return str(plain_with_dt).format_map(data)
return str(plain_without_dt).format_map(data)
@log_entry_types.new('pretix.event.checkin')
class CheckinLogEntryType(CheckinErrorLogEntryType):
def display(self, logentry: LogEntry, data):
if data.get('type') == Checkin.TYPE_EXIT:
return self.display_plain((
_('Position #{posid} has been checked out at {datetime} for list "{list}".'),
_('Position #{posid} has been checked out for list "{list}".'),
), logentry, data)
elif data.get('first'):
return self.display_plain((
_('Position #{posid} has been checked in at {datetime} for list "{list}".'),
_('Position #{posid} has been checked in for list "{list}".'),
), logentry, data)
elif data.get('forced'):
return self.display_plain(
_('A scan for position #{posid} at {datetime} for list "{list}" has been uploaded even though it has '
'been scanned already.'),
logentry, data
if logentry.action_type == 'pretix.event.checkin.unknown':
if show_dt:
return _(
'Unknown scan of code "{barcode}" at {datetime} for list "{list}", type "{type}".'
).format(
posid=data.get('positionid'),
type=data.get('type'),
barcode=data.get('barcode')[:16],
datetime=dt_formatted,
list=checkin_list
)
else:
return self.display_plain(
_('Position #{posid} has been scanned and rejected because it has already been scanned before '
'on list "{list}".'),
logentry, data
return _(
'Unknown scan of code "{barcode}" for list "{list}", type "{type}".'
).format(
posid=data.get('positionid'),
type=data.get('type'),
barcode=data.get('barcode')[:16],
list=checkin_list
)
@log_entry_types.new()
class OrderConsentLogEntryType(OrderLogEntryType):
action_type = 'pretix.event.order.consent'
def display(self, logentry: LogEntry, data):
return _('The user confirmed the following message: "{}"').format(
bleach.clean(data.get('msg'), tags=set(), strip=True)
)
@log_entry_types.new()
class OrderCanceledLogEntryType(OrderLogEntryType):
action_type = 'pretix.event.order.canceled'
def display(self, logentry: LogEntry, data):
comment = data.get('comment')
if comment:
return _('The order has been canceled (comment: "{comment}").').format(comment=comment)
if logentry.action_type == 'pretix.event.checkin.revoked':
if show_dt:
return _(
'Scan scan of revoked code "{barcode}" at {datetime} for list "{list}", type "{type}", was uploaded.'
).format(
posid=data.get('positionid'),
type=data.get('type'),
barcode=data.get('barcode')[:16],
datetime=dt_formatted,
list=checkin_list
)
else:
return _('The order has been canceled.')
return _(
'Scan of revoked code "{barcode}" for list "{list}", type "{type}", was uploaded.'
).format(
posid=data.get('positionid'),
type=data.get('type'),
barcode=data.get('barcode')[:16],
list=checkin_list
)
if logentry.action_type == 'pretix.event.checkin.denied':
if show_dt:
return _(
'Denied scan of position #{posid} at {datetime} for list "{list}", type "{type}", '
'error code "{errorcode}".'
).format(
posid=data.get('positionid'),
type=data.get('type'),
errorcode=data.get('errorcode'),
datetime=dt_formatted,
list=checkin_list
)
else:
return _(
'Denied scan of position #{posid} for list "{list}", type "{type}", error code "{errorcode}".'
).format(
posid=data.get('positionid'),
type=data.get('type'),
errorcode=data.get('errorcode'),
list=checkin_list
)
@log_entry_types.new()
class OrderPrintLogEntryType(OrderLogEntryType):
action_type = 'pretix.event.order.print'
def display(self, logentry: LogEntry, data):
return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
posid=data.get('positionid'),
datetime=date_format(
dateutil.parser.parse(data["datetime"]).astimezone(logentry.event.timezone),
"SHORT_DATETIME_FORMAT"
) if logentry.event else data["datetime"],
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
if data.get('type') == Checkin.TYPE_EXIT:
if show_dt:
return _('Position #{posid} has been checked out at {datetime} for list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)
else:
return _('Position #{posid} has been checked out for list "{list}".').format(
posid=data.get('positionid'),
list=checkin_list
)
if data.get('first'):
if show_dt:
return _('Position #{posid} has been checked in at {datetime} for list "{list}".').format(
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)
else:
return _('Position #{posid} has been checked in for list "{list}".').format(
posid=data.get('positionid'),
list=checkin_list
)
else:
if data.get('forced'):
return _(
'A scan for position #{posid} at {datetime} for list "{list}" has been uploaded even though it has '
'been scanned already.'.format(
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)
)
return _(
'Position #{posid} has been scanned and rejected because it has already been scanned before '
'on list "{list}".'.format(
posid=data.get('positionid'),
list=checkin_list
)
)
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if logentry.action_type.startswith('pretix.event.order.changed'):
return _display_order_changed(sender, logentry)
if logentry.action_type.startswith('pretix.event.payment.provider.'):
return _('The settings of a payment provider have been changed.')
if logentry.action_type.startswith('pretix.event.tickets.provider.'):
return _('The settings of a ticket output provider have been changed.')
if logentry.action_type == 'pretix.event.order.consent':
return _('The user confirmed the following message: "{}"').format(
bleach.clean(logentry.parsed_data.get('msg'), tags=set(), strip=True)
)
if logentry.action_type == 'pretix.event.order.canceled':
comment = logentry.parsed_data.get('comment')
if comment:
return _('The order has been canceled (comment: "{comment}").').format(comment=comment)
else:
return _('The order has been canceled.')
if logentry.action_type in ('pretix.control.views.checkin.reverted', 'pretix.event.checkin.reverted'):
if 'list' in logentry.parsed_data:
try:
checkin_list = sender.checkin_lists.get(pk=logentry.parsed_data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
return _('The check-in of position #{posid} on list "{list}" has been reverted.').format(
posid=logentry.parsed_data.get('positionid'),
list=checkin_list,
)
if sender and logentry.action_type.startswith('pretix.event.checkin'):
return _display_checkin(sender, logentry)
if logentry.action_type == 'pretix.event.order.print':
return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
posid=logentry.parsed_data.get('positionid'),
datetime=date_format(
dateutil.parser.parse(logentry.parsed_data["datetime"]).astimezone(sender.timezone),
"SHORT_DATETIME_FORMAT"
),
type=dict(PrintLog.PRINT_TYPES)[logentry.parsed_data["type"]],
)
@receiver(signal=orderposition_blocked_display, dispatch_uid="pretixcontrol_orderposition_blocked_display")
def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, block_name, **kwargs):
@@ -440,7 +396,6 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
@log_entry_types.new_from_dict({
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
'pretix.event.order.modified': _('The order details have been changed.'),
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
'pretix.event.order.secret.changed': _('The order\'s secret has been changed.'),
@@ -531,16 +486,17 @@ class VoucherRedeemedLogEntryType(VoucherLogEntryType):
action_type = 'pretix.voucher.redeemed'
plain = _('The voucher has been redeemed in order {order_code}.')
def display(self, logentry, data):
def display(self, logentry):
data = json.loads(logentry.data)
data = defaultdict(lambda: '?', data)
url = reverse('control:event.order', kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
'code': data.get('order_code', '?')
'code': data['order_code']
})
return format_html(
self.plain,
order_code=format_html('<a href="{}">{}</a>', url, data.get('order_code', '?')),
)
return mark_safe(self.plain.format(
order_code='<a href="{}">{}</a>'.format(url, data['order_code']),
))
@log_entry_types.new_from_dict({
@@ -563,8 +519,8 @@ class CoreTaxRuleLogEntryType(TaxRuleLogEntryType):
class TeamMembershipLogEntryType(LogEntryType):
def display(self, logentry, data):
return self.plain.format(user=data.get('email'))
def display(self, logentry):
return self.plain.format(user=logentry.parsed_data.get('email'))
@log_entry_types.new_from_dict({
@@ -581,9 +537,9 @@ class CoreTeamMembershipLogEntryType(TeamMembershipLogEntryType):
class TeamMemberJoinedLogEntryType(LogEntryType):
action_type = 'pretix.team.member.joined'
def display(self, logentry, data):
def display(self, logentry):
return _('{user} has joined the team using the invite sent to {email}.').format(
user=data.get('email'), email=data.get('invite_email')
user=logentry.parsed_data.get('email'), email=logentry.parsed_data.get('invite_email')
)
@@ -591,23 +547,23 @@ class TeamMemberJoinedLogEntryType(LogEntryType):
class UserSettingsChangedLogEntryType(LogEntryType):
action_type = 'pretix.user.settings.changed'
def display(self, logentry, data):
def display(self, logentry):
text = str(_('Your account settings have been changed.'))
if 'email' in data:
if 'email' in logentry.parsed_data:
text = text + ' ' + str(
_('Your email address has been changed to {email}.').format(email=data['email']))
if 'new_pw' in data:
_('Your email address has been changed to {email}.').format(email=logentry.parsed_data['email']))
if 'new_pw' in logentry.parsed_data:
text = text + ' ' + str(_('Your password has been changed.'))
if data.get('is_active') is True:
if logentry.parsed_data.get('is_active') is True:
text = text + ' ' + str(_('Your account has been enabled.'))
elif data.get('is_active') is False:
elif logentry.parsed_data.get('is_active') is False:
text = text + ' ' + str(_('Your account has been disabled.'))
return text
class UserImpersonatedLogEntryType(LogEntryType):
def display(self, logentry, data):
return self.plain.format(data['other_email'])
def display(self, logentry):
return self.plain.format(logentry.parsed_data['other_email'])
@log_entry_types.new_from_dict({
@@ -732,13 +688,16 @@ class CoreLogEntryType(LogEntryType):
@log_entry_types.new_from_dict({
'pretix.event.order.deleted': _('The test mode order {code} has been deleted.'),
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'),
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
'pretix.event.tickets.provider': _('The settings of a ticket output provider have been changed.'),
'pretix.event.payment.provider': _('The settings of a payment provider have been changed.'),
'pretix.event.live.activated': _('The shop has been taken live.'),
'pretix.event.live.deactivated': _('The shop has been taken offline.'),
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
@@ -758,19 +717,6 @@ class CoreEventLogEntryType(EventLogEntryType):
pass
@log_entry_types.new_from_dict({
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
})
class CheckinlistLogEntryType(EventLogEntryType):
object_link_wrapper = _('Check-in list {val}')
object_link_viewname = 'control:event.orders.checkinlists.edit'
object_link_argname = 'list'
content_type = CheckinList
@log_entry_types.new_from_dict({
'pretix.event.plugins.enabled': _('The plugin has been enabled.'),
'pretix.event.plugins.disabled': _('The plugin has been disabled.'),
@@ -778,7 +724,7 @@ class CheckinlistLogEntryType(EventLogEntryType):
class EventPluginStateLogEntryType(EventLogEntryType):
object_link_wrapper = _('Plugin {val}')
def get_object_link_info(self, logentry) -> Optional[dict]:
def get_object_link_info(self, logentry) -> dict:
if 'plugin' in logentry.parsed_data:
app = app_cache.get(logentry.parsed_data['plugin'])
if app and hasattr(app, 'PretixPluginMeta'):
@@ -813,17 +759,17 @@ class CoreItemLogEntryType(ItemLogEntryType):
'pretix.event.item.variation.changed': _('The variation "{value}" has been changed.'),
})
class VariationLogEntryType(ItemLogEntryType):
def display(self, logentry, data):
if 'value' not in data:
def display(self, logentry):
if 'value' not in logentry.parsed_data:
# Backwards compatibility
var = ItemVariation.objects.filter(id=data['id']).first()
var = ItemVariation.objects.filter(id=logentry.parsed_data['id']).first()
if var:
data['value'] = str(var.value)
logentry.parsed_data['value'] = str(var.value)
else:
data['value'] = '?'
logentry.parsed_data['value'] = '?'
else:
data['value'] = LazyI18nString(data['value'])
return super().display(logentry, data)
logentry.parsed_data['value'] = LazyI18nString(logentry.parsed_data['value'])
return super().display(logentry)
@log_entry_types.new_from_dict({
@@ -879,27 +825,27 @@ class CoreDiscountLogEntryType(DiscountLogEntryType):
class LegacyCheckinLogEntryType(OrderLogEntryType):
action_type = 'pretix.control.views.checkin'
def display(self, logentry, data):
def display(self, logentry):
# deprecated
dt = dateutil.parser.parse(data.get('datetime'))
dt = dateutil.parser.parse(logentry.parsed_data.get('datetime'))
tz = logentry.event.timezone
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in data:
if 'list' in logentry.parsed_data:
try:
checkin_list = logentry.event.checkin_lists.get(pk=data.get('list')).name
checkin_list = logentry.event.checkin_lists.get(pk=logentry.parsed_data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
if data.get('first'):
if logentry.parsed_data.get('first'):
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
posid=data.get('positionid'),
posid=logentry.parsed_data.get('positionid'),
datetime=dt_formatted,
list=checkin_list,
)
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
posid=data.get('positionid'),
posid=logentry.parsed_data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)
+1 -1
View File
@@ -59,7 +59,7 @@ def get_event_navigation(request: HttpRequest):
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'),
'active': url.url_name == 'event.settings.payment',
},
{
'label': _('Plugins'),
@@ -14,7 +14,7 @@
<input class="form-control" name="token" placeholder="{% trans "Token" %}" autocomplete="one-time-code"
type="text" required="required" autofocus="autofocus" id="webauthn-response">
</div>
<div class="alert alert-danger hidden" id="webauthn-error">
<div class="sr-only alert alert-danger" id="webauthn-error">
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
</div>
{% if jsondata %}
@@ -4,8 +4,6 @@
{% load statici18n %}
{% load eventsignal %}
{% load eventurl %}
{% load dialog %}
{% load icon %}
<!DOCTYPE html>
<html{% if rtl %} dir="rtl" class="rtl"{% endif %}>
<head>
@@ -35,7 +33,6 @@
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
<script type="text/javascript" src="{% static "cropper/cropper.js" %}"></script>
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/gettextstub.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
@@ -466,16 +463,25 @@
</div>
</div>
</div>
<div id="ajaxerr" class="modal-wrapper" hidden>
<div id="ajaxerr">
</div>
{% dialog "loadingmodal" "" "" icon="cog rotating" %}
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
<div class="progress">
<div class="progress-bar progress-bar-success">
<div id="loadingmodal">
<div class="modal-card">
<div class="modal-card-icon">
<i class="fa fa-cog big-rotating-icon"></i>
</div>
<div class="modal-card-content">
<h3></h3>
<p class="text"></p>
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
<div class="progress">
<div class="progress-bar progress-bar-success">
</div>
</div>
<div class="steps">
</div>
</div>
</div>
<div class="steps">
</div>
{% enddialog %}
</div>
</body>
</html>
@@ -13,7 +13,7 @@
{% endfor %}
<p>
{% blocktrans trimmed count count=cnt %}
Are you sure you want to permanently delete the check-ins of <strong>one ticket</strong>?
Are you sure you want to permanently delete the check-ins of <strong>one ticket</strong>.
{% plural %}
Are you sure you want to permanently delete the check-ins of <strong>{{ count }} tickets</strong>?
{% endblocktrans %}
@@ -148,7 +148,7 @@
<td>{{ e.item }}{% if e.variation %} {{ e.variation }}{% endif %}</td>
{% if request.event.has_subevents and not checkinlist.subevent %}
<td>
{{ e.subevent.name }} {{ e.subevent.get_date_range_display_with_times }}
{{ e.subevent.name }} {{ e.subevent.get_date_range_display }} {{ e.subevent.date_from|date:"TIME_FORMAT" }}
</td>
{% endif %}
{% if seats %}
@@ -127,7 +127,8 @@
{% if request.event.has_subevents %}
{% if cl.subevent %}
<td>
{{ cl.subevent.name }} {{ cl.subevent.get_date_range_display_with_times }}
{{ cl.subevent.name }} {{ cl.subevent.get_date_range_display }}
{{ cl.subevent.date_from|date:"TIME_FORMAT" }}
</td>
{% else %}
<td>
@@ -7,7 +7,7 @@
{% endblocktrans %}</p>
{% endif %}
{% endif %}
<p class="plugin-description">{{ plugin.description|safe }}</p>
<p>{{ plugin.description|safe }}</p>
{% if plugin.restricted and plugin.module not in request.event.settings.allowed_restricted_plugins %}
<p class="text-muted">
<span class="fa fa-info-circle" aria-hidden="true"></span>
@@ -85,7 +85,7 @@
<div class="checkbox">
<label>
<input type="checkbox" name="delete" value="yes" />
<strong>{% trans "Permanently delete all orders created in test mode" %}</strong>
<b>{% trans "Permanently delete all orders created in test mode" %}</b>
</label>
</div>
<div class="text-right">
@@ -48,19 +48,19 @@
</a>
</td>
</tr>
{% endfor %}
{% empty %}
<tr>
<td colspan="4">
<br>
<td colspan="3">
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
<a href="{{ plugin_settings_url }}#tab-0-1-open" class="btn btn-default">
<i class="fa fa-plus"></i> {% trans "Enable additional payment plugins" %}
</a>
{% blocktrans trimmed with plugin_settings_href='href="'|add:plugin_settings_url|add:'"'|safe %}
There are no payment providers available. Please go to the
<a {{ plugin_settings_href }}>plugin settings</a> and activate one or more payment plugins.
{% endblocktrans %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
<fieldset>
<legend>{% trans "Deadlines" %}</legend>
@@ -10,40 +10,20 @@
software functionality, connect your event to third-party services, or apply other forms of customizations.
{% endblocktrans %}
</p>
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<div class="row">
<div class="col-lg-10">
<p><input type="search" id="plugin_search_input" class="form-control" placeholder="{% trans "Search" %}"></p>
</div>
<div class="col-lg-2 text-right">
<p class="btn-group btn-group-flex" data-toggle="buttons">
<label class="btn btn-primary-if-active active"><input type="radio" name="plugin_state_filter" value="all" checked> {% trans "All" %}</label>
<label class="btn btn-primary-if-active"><input type="radio" name="plugin_state_filter" value="active"> {% trans "Active" %}</label>
</p>
</div>
</div>
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<div id="plugin_search_results" class="panel panel-default collapse">
<div class="panel-heading">
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>
{% trans "Search results" %}
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
<div class="panel-body">
<div class="plugin-list"></div>
</div>
</div>
<div id="plugin_tabs"><div class="tabbed-form">
{% endif %}
<div class="tabbed-form">
{% for cat, catlabel, plist, has_pictures in plugins %}
<fieldset data-plugin-category="{{ cat }}" data-plugin-category-label="{{ catlabel }}">
<fieldset>
<legend>{{ catlabel }}</legend>
<div class="plugin-list">
{% for plugin, is_active, settings_links, navigation_links in plist %}
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}" id="plugin_{{ plugin.module }}" data-plugin-module="{{ plugin.module }}" data-plugin-name="{{ plugin.name }}">
{% for plugin in plist %}
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}" id="plugin_{{ plugin.module }}">
{% if plugin.featured %}
<div class="panel panel-default">
<div class="panel-body">
@@ -69,8 +49,8 @@
{% if show_meta %}
<span class="text-muted text-sm">{{ plugin.version }}</span>
{% endif %}
{% if is_active %}
<span class="label label-success" data-is-active>
{% if plugin.module in plugins_active %}
<span class="label label-success">
<span class="fa fa-check" aria-hidden="true"></span>
{% trans "Active" %}
</span>
@@ -86,32 +66,8 @@
<div class="plugin-action">
<span class="text-muted">{% trans "Not available" %}</span>
</div>
{% elif is_active %}
{% elif plugin.module in plugins_active %}
<div class="plugin-action flip">
{% if navigation_links %}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle{% if plugin.featured %} btn-lg{% endif %}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans "Open plugin settings" %}">
<span class="fa fa-compass"></span> {% trans "Go to" %} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% for link in navigation_links %}
<li><a href="{{ link.0 }}">{{ link.1 }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if settings_links %}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle{% if plugin.featured %} btn-lg{% endif %}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans "Open plugin settings" %}">
<span class="fa fa-cog"></span> {% trans "Settings" %} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% for link in settings_links %}
<li><a href="{{ link.0 }}">{{ link.1 }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<button class="btn btn-default{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
value="disable">{% trans "Disable" %}</button>
</div>
@@ -130,7 +86,6 @@
</div>
</fieldset>
{% endfor %}
</div></div>
</div>
</form>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/plugins.js" %}"></script>
{% endblock %}
@@ -61,7 +61,6 @@
{% bootstrap_field sform.locale layout="control" %}
{% bootstrap_field sform.timezone layout="control" %}
{% bootstrap_field sform.region layout="control" %}
{% bootstrap_field form.is_remote layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Customer and attendee data" %}</legend>
@@ -19,8 +19,8 @@
section of your website:
{% endblocktrans %}
</p>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" version=widget_version_default %}" crossorigin&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language version=widget_version_default %}" async crossorigin&gt;&lt;/script&gt;</pre>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}" crossorigin&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async crossorigin&gt;&lt;/script&gt;</pre>
<p>
{% blocktrans trimmed %}
Then, copy the following code to the place of your website where you want the widget to show up:
@@ -32,7 +32,7 @@
{% abseventurl request.event "presale:event.index" as indexurl %}
{% endif %}
{% if form.cleaned_data.compatibility_mode %}
<pre>&lt;div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}&gt;&lt;/div&gt;
<pre>&lt;div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %} single-item-select="button"&gt;&lt;/div&gt;
&lt;noscript&gt;
&lt;div class="pretix-widget"&gt;
&lt;div class="pretix-widget-info-message"&gt;
@@ -45,7 +45,7 @@
&lt;/noscript&gt;
</pre>
{% else %}
<pre>&lt;pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}&gt;&lt;/pretix-widget&gt;
<pre>&lt;pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %} single-item-select="button"&gt;&lt;/pretix-widget&gt;
&lt;noscript&gt;
&lt;div class="pretix-widget"&gt;
&lt;div class="pretix-widget-info-message"&gt;
@@ -110,26 +110,23 @@
{% if not hide_orga %}<td>{{ e.organizer }}</td>{% endif %}
<td class="event-date-col">
{% if e.has_subevents %}
<span class="fa fa-fw- fa-calendar"></span>
{% trans "Event series" %}
<br>
<span class="text-muted">
{% if e.min_from %}
{{ e.min_from|date:"SHORT_DATETIME_FORMAT" }} <br>
{{ e.max_fromto|default_if_none:e.max_to|default_if_none:e.max_from|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
{% trans "No dates" context "subevent" %}
{% endif %}
</span>
{{ e.min_from|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
{{ e.get_short_date_from_display }}
{% if e.settings.show_date_to and e.date_to %}
<br>
{% endif %}
{% if e.has_subevents %}
<span class="label label-default">{% trans "Series" %}</span>
{% endif %}
{% if e.settings.show_date_to and e.date_to %}
<br>
{% if e.has_subevents %}
{{ e.max_fromto|default_if_none:e.max_from|default_if_none:e.max_to|default_if_none:""|date:"SHORT_DATETIME_FORMAT" }}
{% else %}
{{ e.get_short_date_to_display }}
{% endif %}
{% endif %}
{% if e.settings.timezone != request.timezone %}
<span class="fa fa-globe text-muted" data-toggle="tooltip" title="{{ e.tzname }}"></span>
<span class="fa fa-globe text-muted" data-toggle="tooltip" title="{{ e.timezone }}"></span>
{% endif %}
</td>
<td>
@@ -1,7 +1,5 @@
{% load i18n %}
{% load bootstrap3 %}
{% load rich_text %}
{{ request.event.settings.payment_giftcard_public_description|rich_text }}
{% bootstrap_form form layout='checkout' %}
<input name="giftcard" class="form-control" placeholder="{% trans "Gift card code" %}">
@@ -172,7 +172,7 @@
{% if form.hidden_if_available %}
{% bootstrap_field form.hidden_if_available layout="control" horizontal_field_class="col-md-7" %}
{% endif %}
{% bootstrap_field form.hidden_if_item_available visibility_field=form.hidden_if_item_available_mode layout="control_with_visibility" %}
{% bootstrap_field form.hidden_if_item_available layout="control" horizontal_field_class="col-md-7" %}
</fieldset>
{% for v in formsets.values %}
<fieldset>
@@ -40,10 +40,6 @@
<tr data-dnd-id="{{ c.id }}">
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
<br>
<small class="text-muted">
#{{ c.pk }}
</small>
</td>
<td>
{{ c.get_category_type_display }}
@@ -26,8 +26,6 @@
{% bootstrap_field form.condition_ignore_voucher_discounted layout="control" %}
{% if form.subevent_mode %}
{% bootstrap_field form.subevent_mode layout="control" %}
{% bootstrap_field form.subevent_date_from layout="control" %}
{% bootstrap_field form.subevent_date_until layout="control" %}
{% endif %}
<div class="form-group form-alternatives">
<label class="col-md-3 control-label">

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