Compare commits

...

59 Commits

Author SHA1 Message Date
Martin Gross 39115152a6 Update src/pretix/base/customersso/oidc.py
Co-authored-by: Mira <weller@rami.io>
2025-01-30 13:39:22 +01:00
Martin Gross 7ce790dbb9 Actually fix indentation... 2025-01-30 13:02:49 +01:00
Martin Gross 9a4a0df625 isort 2025-01-30 12:39:46 +01:00
Martin Gross a8376b9a79 Add missing imports; fix indentation 2025-01-30 12:38:12 +01:00
Martin Gross b21041a833 Apply suggestions from code review
Co-authored-by: Mira <weller@rami.io>
2025-01-30 12:35:58 +01:00
Martin Gross e8a716273e OIDC: Allow to add query parameters to Authorization UR 2025-01-29 11:55:12 +01:00
Richard Schreiber 59a7845ac4 [A11y] Improve focus handling for widget overlay
* move iframe after close-button to follow tab-order

* add missing prevActiveElement

* prepare focus-handling for error_message

* iframe.src through prop instead of directly accessing it

* do not change close button HTML-element for compatability

* make all overlay elements role=dialog and modals

* fix close button

* fix re-opening of iframe

* make error-message read out when shown

* Improve handling of frame_src with frame_loading

* manually focus continue or close button in alert-box

* fix btn-focus in transition

* Improve quantity group
2025-01-28 11:04:38 +01:00
Cornelius Kibelka ef1024d231 Translations: Update Portuguese (Brazil)
Currently translated at 14.8% (870 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2025-01-28 10:13:32 +01:00
Cornelius Kibelka 71d0d72425 Translations: Update Portuguese (Brazil)
Currently translated at 14.1% (825 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_BR/

powered by weblate
2025-01-27 17:40:37 +01:00
Hijiri Umemoto 6252e526bf Translations: Update Japanese
Currently translated at 100.0% (5844 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2025-01-27 17:40:37 +01:00
Rosariocastellana f3d8a1c2e1 Translations: Update Italian
Currently translated at 24.2% (1419 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/it/

powered by weblate
2025-01-27 17:40:37 +01:00
Martin Gross 148a3b2933 MetricsMiddleware: Do not record pretix_view_duration_seconds for urls with no url.url_name (#4784) 2025-01-27 16:46:58 +01:00
Mira 709633d6fb Register missing LogEntryTypes (#4779) 2025-01-27 15:35:29 +01:00
Mira af3418db54 Fix company & vat_id dependencies on is_business (#4777) 2025-01-27 11:55:39 +01:00
Mira ca8d253114 Bugfixes for LogEntryTypes refactoring (#4778) 2025-01-24 16:19:55 +01:00
Mira Weller f014a9bbd3 Reapply "Implement hidden_if_item_available_mode option (Z#23177008) (#4776)"
This reverts commit 5cd7959e86.
2025-01-24 14:48:28 +01:00
Mira Weller 3e5bfb44d2 Revert "Preliminary migration"
This reverts commit 1736efbdc3.
2025-01-24 14:48:28 +01:00
Mira Weller 1736efbdc3 Preliminary migration 2025-01-24 14:08:13 +01:00
Mira Weller 5cd7959e86 Revert "Implement hidden_if_item_available_mode option (Z#23177008) (#4776)"
This reverts commit b847612e1a.
2025-01-24 14:07:55 +01:00
Mira b847612e1a Implement hidden_if_item_available_mode option (Z#23177008) (#4776) 2025-01-24 11:24:50 +01:00
Mira 832f4e4d68 Define LogEntryTypes for all actions in pretix core, improve content_object handling (#4768)
Create LogEntryType definitions for all missing action_types (order changes, check-in events, settings changes of PaymentProviders and TicketOutputs).

Check whether the stored content_object is of the expected model type, preventing incorrect links.

Refactoring:
-    Move the base LogEntryType definitions for our models to their own file
-    Move HTML escaping into make_link to make it less likely to oversee in the LogEntryType definitions
-    Log pretix.event.order.deleted with the deleted Order model as content_object, matching the other *.deleted action_types
2025-01-24 10:05:19 +01:00
Mira 0a23aeece4 Allow 0% tax rate on event creation (#4756)
(but still warn if tax rate is not filled at all)
2025-01-23 12:59:39 +01:00
Martin Gross 9622bf41a1 InvoiceForm: Display vat_id especially if company_required (Z#23180046) (#4775) 2025-01-23 12:09:45 +01:00
Hijiri Umemoto dd4bac70be Translations: Update Japanese
Currently translated at 99.8% (5837 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2025-01-22 18:36:36 +01:00
Johanna Ketola 1187757b56 Translations: Update Finnish
Currently translated at 25.3% (1484 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-01-22 18:36:36 +01:00
CVZ-es abe5b4ef53 Translations: Update Spanish
Currently translated at 100.0% (5844 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2025-01-22 18:36:36 +01:00
CVZ-es b1fb391d08 Translations: Update French
Currently translated at 100.0% (5844 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fr/

powered by weblate
2025-01-22 18:36:36 +01:00
Raphael Michel a95c6d94ee Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (235 of 235 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/de_Informal/

powered by weblate
2025-01-22 18:36:36 +01:00
Raphael Michel 4809558343 Translations: Update German
Currently translated at 100.0% (235 of 235 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/de/

powered by weblate
2025-01-22 18:36:36 +01:00
Raphael Michel a6d1af01d2 Fix missing language info for nb-NO (default is just nb) (Z#23177041) 2025-01-22 15:57:47 +01:00
Raphael Michel 23e58996bc Invoice preview: Fix missing delivery date 2025-01-22 11:49:20 +01:00
Raphael Michel 15e05dae2f Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5844 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2025-01-21 18:25:40 +01:00
Raphael Michel ce40524ae8 Translations: Update German
Currently translated at 100.0% (5844 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2025-01-21 18:25:40 +01:00
Raphael Michel 46aefc10f3 Order change form: No default fee type, use most generic fee type first (Z#23179634) (#4771) 2025-01-21 17:48:31 +01:00
Raphael Michel 1f49b577f0 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2025-01-21 17:43:41 +01:00
Hijiri Umemoto b3aa405bcc Translations: Update Japanese
Currently translated at 100.0% (5840 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2025-01-21 17:42:33 +01:00
Hector f29b60b3db Translations: Update Spanish
Currently translated at 100.0% (235 of 235 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/es/

powered by weblate
2025-01-21 17:42:33 +01:00
Hector 603e7821cc Translations: Update Spanish
Currently translated at 100.0% (5844 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2025-01-21 17:42:33 +01:00
Raphael Michel ffdc73e0a3 Show percentage of discount in cart (Z#23176955) (#4719)
* Show percentage of discount in cart (Z#23176955)

* Fix computation
2025-01-21 16:35:15 +01:00
Raphael Michel 00b4622afa Address form logic: Respect dependency of VAT ID on is_business (#4713) 2025-01-21 15:05:16 +01:00
Raphael Michel 045edc7cec Invoice rendering: Remove transparency from logos (Z#23179391) (#4762)
* Invoice rendering: Remove logos from transparency

* Add comment
2025-01-21 15:03:43 +01:00
Raphael Michel 1635118772 Copy ItemVariation.limit_sales_channels when copying items (#4752) 2025-01-21 15:01:28 +01:00
Mira 87c987fee5 Only display tax rates with non-zero gross amount (#4760) 2025-01-20 14:49:04 +01:00
dependabot[bot] 1267bf8ba8 Update webauthn requirement from ==2.4.* to ==2.5.* (#4766)
Updates the requirements on [webauthn](https://github.com/duo-labs/py_webauthn) to permit the latest version.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.4.0...v2.5.0)

---
updated-dependencies:
- dependency-name: webauthn
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 12:49:03 +01:00
Serge Bazanski a8d1ed8ee1 Translations: Update Polish (informal) (pl_Informal)
Currently translated at 13.3% (782 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl_Informal/

powered by weblate
2025-01-20 12:45:01 +01:00
Serge Bazanski b7736d5e82 Translations: Update Polish (informal) (pl_Informal)
Currently translated at 12.0% (701 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl_Informal/

powered by weblate
2025-01-20 12:45:01 +01:00
Serge Bazanski cfefe5bfc3 Translations: Update Polish (informal) (pl_Informal)
Currently translated at 100.0% (235 of 235 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/pl_Informal/

powered by weblate
2025-01-20 12:45:01 +01:00
Serge Bazanski f0f272b304 Translations: Update Polish (informal) (pl_Informal)
Currently translated at 6.8% (398 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl_Informal/

powered by weblate
2025-01-20 12:45:01 +01:00
Wiktor Przybylski e8b159e6d4 Translations: Update Polish
Currently translated at 98.7% (5769 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl/

powered by weblate
2025-01-20 12:45:01 +01:00
Hijiri Umemoto b0de6815db Translations: Update Japanese
Currently translated at 100.0% (235 of 235 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ja/

powered by weblate
2025-01-20 12:45:01 +01:00
Hijiri Umemoto 92ceea2680 Translations: Update Japanese
Currently translated at 100.0% (5840 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2025-01-20 12:45:01 +01:00
Wiktor Przybylski c2a9f9f76a Translations: Update Polish
Currently translated at 98.4% (5751 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl/

powered by weblate
2025-01-20 12:45:01 +01:00
Wiktor Przybylski 1e1f0e5d86 Translations: Update Polish
Currently translated at 98.3% (5746 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl/

powered by weblate
2025-01-20 12:45:01 +01:00
Richard Schreiber 9634907539 [a11y] Remove h1 aria-label in presale 2025-01-20 08:57:21 +01:00
Raphael Michel 70dd688ec1 Translations: Add Flemish (West) 2025-01-17 17:49:14 +01:00
Raphael Michel 5ad0213195 Translations: Add Albanian 2025-01-17 17:49:14 +01:00
Raphael Michel c40cf45179 Translations: Add Faroese 2025-01-17 17:49:14 +01:00
CVZ-es a72839fd0e Translations: Update Spanish
Currently translated at 100.0% (5844 of 5844 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2025-01-17 17:49:14 +01:00
CVZ-es 5071db0a8b Translations: Update French
Currently translated at 100.0% (5840 of 5840 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fr/

powered by weblate
2025-01-17 17:49:14 +01:00
94 changed files with 149315 additions and 51648 deletions
+13
View File
@@ -69,6 +69,10 @@ 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
@@ -239,6 +243,10 @@ 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
-----
@@ -308,6 +316,7 @@ 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,
@@ -459,6 +468,7 @@ 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,
@@ -589,6 +599,7 @@ 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,
@@ -705,6 +716,7 @@ 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,
@@ -855,6 +867,7 @@ 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,
+1 -1
View File
@@ -100,7 +100,7 @@ dependencies = [
"ua-parser==1.0.*",
"vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*",
"webauthn==2.4.*",
"webauthn==2.5.*",
"zeep==4.3.*"
]
+6
View File
@@ -163,6 +163,12 @@ EXTRA_LANG_INFO = {
'name': 'Portuguese',
'name_local': 'Português',
},
'nb-no': {
'bidi': False,
'code': 'nb-no',
'name': 'Norwegian Bokmal',
'name_local': 'norsk (bokmål)',
},
}
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
+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', 'allow_waitinglist',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', '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',
+10 -1
View File
@@ -24,7 +24,7 @@ import hashlib
import logging
import time
from datetime import datetime
from urllib.parse import urlencode, urljoin
from urllib.parse import parse_qsl, urlencode, urljoin
import jwt
import requests
@@ -139,6 +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
@@ -154,6 +159,10 @@ def oidc_authorize_url(provider, state, redirect_uri):
'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"]))
return endpoint + '?' + urlencode(params)
+5 -6
View File
@@ -1025,10 +1025,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'autocomplete': 'address-level2',
}),
'company': forms.TextInput(attrs={
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'vat_id': forms.TextInput(),
'internal_reference': forms.TextInput,
}
labels = {
@@ -1059,8 +1058,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs)
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'
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"]'
if not self.ask_vat_id:
del self.fields['vat_id']
@@ -1141,9 +1140,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'#id_{self.add_prefix("is_business")}_0'
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-no-required-attr'] = '1'
self.fields['company'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_1'
self.fields['company'].widget.attrs['data-required-if'] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
if not event.settings.invoice_address_beneficiary:
del self.fields['beneficiary']
+11
View File
@@ -388,6 +388,15 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
except:
logger.exception("Can not resize image")
pass
try:
# Valid ZUGFeRD invoices must be compliant with PDF/A-3. pretix-zugferd ensures this by passing them
# through ghost script. Unfortunately, if the logo contains transparency, this will still fail.
# I was unable to figure out a way to fix this in GhostScript, so the easy fix is to remove the
# transparency, as our invoices always have a white background anyways.
ir.remove_transparency()
except:
logger.exception("Can not remove transparency from logo")
pass
canvas.drawImage(ir,
self.logo_left,
self.pagesize[1] - self.logo_height - self.logo_top,
@@ -775,6 +784,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
for idx, gross in grossvalue_map.items():
rate, name = idx
if rate == 0 and gross == 0:
continue
tax = taxvalue_map[idx]
tdata.append([
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
+165
View File
@@ -0,0 +1,165 @@
#
# 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
+34 -140
View File
@@ -19,137 +19,20 @@
# 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 escape
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.signals import EventPluginRegistry
from pretix.base.models import (
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
Voucher,
)
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
from .logentrytype_registry import ( # noqa
ClearDataShredderMixin, LogEntryType, NoOpShredderMixin, log_entry_types,
make_link, LogEntryTypeRegistry,
)
class EventLogEntryType(LogEntryType):
@@ -157,15 +40,27 @@ 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) -> dict:
if hasattr(self, 'object_link_viewname') and logentry.content_object:
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
return {
'href': reverse(self.object_link_viewname, kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
**self.object_link_args(logentry.content_object),
**self.object_link_args(content),
}),
'val': escape(self.object_link_display_name(logentry.content_object)),
'val': self.object_link_display_name(logentry.content_object),
}
def object_link_args(self, content_object):
@@ -182,6 +77,7 @@ 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}
@@ -194,6 +90,7 @@ 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:
@@ -205,49 +102,46 @@ 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'
class NoOpShredderMixin:
def shred_pii(self, logentry):
pass
class ClearDataShredderMixin:
def shred_pii(self, logentry):
logentry.data = None
content_type = TaxRule
@@ -0,0 +1,18 @@
# 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),
),
]
+12 -1
View File
@@ -442,8 +442,12 @@ class Item(LoggedModel):
UNAVAIL_MODE_INFO = "info"
UNAVAIL_MODES = (
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
(UNAVAIL_MODE_INFO, _("Show product with info on why its unavailable")),
)
UNAVAIL_MODE_ICONS = {
UNAVAIL_MODE_HIDDEN: 'eye-slash',
UNAVAIL_MODE_INFO: 'info'
}
MEDIA_POLICY_REUSE = 'reuse'
MEDIA_POLICY_NEW = 'new'
@@ -596,6 +600,11 @@ 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,
@@ -885,6 +894,8 @@ 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
+2 -2
View File
@@ -40,7 +40,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.functional import cached_property
from pretix.base.logentrytypes import log_entry_types, make_link
from pretix.base.logentrytype_registry 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)
return log_entry_type.display(self, self.parsed_data)
from ..signals import logentry_display
+8 -2
View File
@@ -355,7 +355,7 @@ class Order(LockModel, LoggedModel):
if not self.testmode:
raise TypeError("Only test mode orders can be deleted.")
self.event.log_action(
self.log_action(
'pretix.event.order.deleted', user=user, auth=auth,
data={
'code': self.code,
@@ -2281,9 +2281,9 @@ class OrderFee(models.Model):
FEE_TYPE_OTHER = "other"
FEE_TYPE_GIFTCARD = "giftcard"
FEE_TYPES = (
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_PAYMENT, _("Payment fee")),
(FEE_TYPE_SHIPPING, _("Shipping fee")),
(FEE_TYPE_SERVICE, _("Service fee")),
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
(FEE_TYPE_INSURANCE, _("Insurance fee")),
(FEE_TYPE_LATE, _("Late fee")),
@@ -3218,6 +3218,12 @@ class CartPosition(AbstractPosition):
self.tax_code = line_price.code
self.save(update_fields=['line_price_gross', 'tax_rate'])
@property
def discount_percentage(self):
if not self.line_price_gross:
return 0
return (self.line_price_gross - self.price) / self.line_price_gross * 100
@property
def addons_without_bundled(self):
addons = [op for op in self.addons.all() if not op.is_bundled]
+6
View File
@@ -495,12 +495,18 @@ def build_preview_invoice_pdf(event):
invoice=invoice, description=_("Sample product {}").format(i + 1),
gross_value=tax.gross, tax_value=tax.tax,
tax_rate=tax.rate, tax_name=tax.name, tax_code=tax.code,
event_date_from=event.date_from,
event_date_to=event.date_to,
event_location=event.settings.invoice_event_location,
)
else:
for i in range(5):
InvoiceLine.objects.create(
invoice=invoice, description=_("Sample product A"),
gross_value=100, tax_value=0, tax_rate=0, tax_code=None,
event_date_from=event.date_from,
event_date_to=event.date_to,
event_location=event.settings.invoice_event_location,
)
return event.invoice_renderer.generate(invoice)
+1 -1
View File
@@ -231,7 +231,7 @@ class EventWizardBasicsForm(I18nModelForm):
raise ValidationError({
'timezone': _('Your default locale must be specified.')
})
if not data.get("no_taxes") and not data.get("tax_rate"):
if not data.get("no_taxes") and data.get("tax_rate") is None:
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)
+13 -16
View File
@@ -476,6 +476,7 @@ 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',
@@ -539,6 +540,8 @@ class ItemCreateForm(I18nModelForm):
v.pk = None
v.item = instance
v.save()
if not variation.all_sales_channels:
v.limit_sales_channels.set(variation.limit_sales_channels.all())
for mv in variation.meta_values.all():
mv.pk = None
mv.variation = v
@@ -644,18 +647,12 @@ class ItemUpdateForm(I18nModelForm):
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
option_icons=Item.UNAVAIL_MODE_ICONS
)
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
option_icons=Item.UNAVAIL_MODE_ICONS
)
self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect(
@@ -670,6 +667,11 @@ 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(
@@ -851,6 +853,7 @@ 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',
@@ -968,18 +971,12 @@ class ItemVariationForm(I18nModelForm):
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_from_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
option_icons=Item.UNAVAIL_MODE_ICONS
)
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
choices=self.fields['available_until_mode'].choices,
option_icons={
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
Item.UNAVAIL_MODE_INFO: 'info'
}
option_icons=Item.UNAVAIL_MODE_ICONS
)
self.meta_fields = []
+7 -1
View File
@@ -612,7 +612,13 @@ class OrderFeeChangeForm(forms.Form):
class OrderFeeAddForm(forms.Form):
fee_type = forms.ChoiceField(choices=OrderFee.FEE_TYPES)
fee_type = forms.ChoiceField(
choices=[("", ""), *OrderFee.FEE_TYPES],
help_text=_(
"Note that payment fees have a special semantic and might automatically be changed if the "
"payment method of the order is changed."
)
)
value = forms.DecimalField(
max_digits=13, decimal_places=2,
localize=True,
+9
View File
@@ -1043,6 +1043,15 @@ 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
+301 -253
View File
@@ -33,16 +33,16 @@
# 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
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.strings import LazyI18nString
@@ -68,128 +68,212 @@ OVERVIEW_BANLIST = [
]
def _display_order_changed(event: Event, logentry: LogEntry):
data = json.loads(logentry.data)
class OrderChangeLogEntryType(OrderLogEntryType):
prefix = _('The order has been changed:')
text = _('The order has been changed:')
if logentry.action_type == 'pretix.event.order.changed.item':
def display(self, logentry, data):
return 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):
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 text + ' ' + _('Position #{posid}: {old_item} ({old_price}) changed '
'to {new_item} ({new_price}).').format(
return _('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),
)
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':
@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):
old_se = str(event.subevents.get(pk=data['old_subevent']))
new_se = str(event.subevents.get(pk=data['new_subevent']))
return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price}) changed '
'to "{new_event}" ({new_price}).').format(
return _('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),
)
elif logentry.action_type == 'pretix.event.order.changed.price':
return text + ' ' + _('Price of position #{posid} changed from {old_price} '
'to {new_price}.').format(
@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(
posid=data.get('positionid', '?'),
old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency),
)
elif logentry.action_type == 'pretix.event.order.changed.tax_rule':
@log_entry_types.new()
class OrderTaxRuleChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.tax_rule'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
if 'positionid' in data:
return text + ' ' + _('Tax rule of position #{posid} changed from {old_rule} '
'to {new_rule}.').format(
return _('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 text + ' ' + _('Tax rule of fee #{fee} changed from {old_rule} '
'to {new_rule}.').format(
return _('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']),
)
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(
@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(
old_price=money_filter(Decimal(data['old_price']), event.currency),
new_price=money_filter(Decimal(data['new_price']), event.currency),
)
elif logentry.action_type == 'pretix.event.order.changed.cancelfee':
return text + ' ' + _('A fee of {old_price} was removed.').format(
@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(
old_price=money_filter(Decimal(data['old_price']), event.currency),
)
elif logentry.action_type == 'pretix.event.order.changed.cancel':
@log_entry_types.new()
class OrderCanceled(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.cancel'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
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 text + ' ' + _('Position #{posid} ({old_item}, {old_price}) canceled.').format(
return _('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),
)
elif logentry.action_type == 'pretix.event.order.changed.add':
@log_entry_types.new()
class OrderPositionAdded(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.add'
def display_prefixed(self, event: Event, logentry: LogEntry, data):
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 text + ' ' + _('Position #{posid} created: {item} ({price}) as an add-on to '
'position #{addon_to}.').format(
return _('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 text + ' ' + _('Position #{posid} created: {item} ({price}).').format(
return _('Position #{posid} created: {item} ({price}).').format(
posid=data.get('positionid', '?'),
item=item,
price=money_filter(Decimal(data['price']), event.currency),
)
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(
@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(
posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get(
'new_value') else ''
)
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(
@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(
posid=data.get('positionid', '?'),
value=date_format(dateutil.parser.parse(data.get('new_value')), 'SHORT_DATETIME_FORMAT') if data.get('new_value') else ''
)
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':
@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):
old_item = str(event.items.get(pk=data['old_item']))
if data['old_variation']:
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
@@ -198,194 +282,148 @@ def _display_order_changed(event: Event, logentry: LogEntry):
'organizer': event.organizer.slug,
'code': data['new_order']
})
return mark_safe(escape(text) + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
return mark_safe(self.prefix + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
old_item=escape(old_item),
posid=data.get('positionid', '?'),
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':
@log_entry_types.new()
class OrderChangedSplitFrom(OrderLogEntryType):
action_type = 'pretix.event.order.changed.split_from'
def display(self, logentry: LogEntry, data):
return _('This order has been created by splitting the order {order}').format(
order=data['original_order'],
)
def _display_checkin(event, logentry):
data = logentry.parsed_data
@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):
self.display_plain(self.plain, logentry, data)
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")
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
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)")
data = defaultdict(lambda: '?', 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
event = logentry.event
if 'list' in data:
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 'forced' in data:
tz = event.timezone
data['datetime'] = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
return str(plain_with_dt).format_map(data)
else:
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
)
else:
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
return self.display_plain(
_('Position #{posid} has been scanned and rejected because it has already been scanned before '
'on list "{list}".'),
logentry, data
)
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 _(
'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 OrderConsentLogEntryType(OrderLogEntryType):
action_type = 'pretix.event.order.consent'
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
)
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)
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
)
return _('The order has been canceled.')
@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"
),
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
)
@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):
@@ -396,6 +434,7 @@ 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.'),
@@ -486,17 +525,16 @@ class VoucherRedeemedLogEntryType(VoucherLogEntryType):
action_type = 'pretix.voucher.redeemed'
plain = _('The voucher has been redeemed in order {order_code}.')
def display(self, logentry):
data = json.loads(logentry.data)
data = defaultdict(lambda: '?', data)
def display(self, logentry, data):
url = reverse('control:event.order', kwargs={
'event': logentry.event.slug,
'organizer': logentry.event.organizer.slug,
'code': data['order_code']
'code': data.get('order_code', '?')
})
return mark_safe(self.plain.format(
order_code='<a href="{}">{}</a>'.format(url, data['order_code']),
))
return format_html(
self.plain,
order_code=format_html('<a href="{}">{}</a>', url, data.get('order_code', '?')),
)
@log_entry_types.new_from_dict({
@@ -519,8 +557,8 @@ class CoreTaxRuleLogEntryType(TaxRuleLogEntryType):
class TeamMembershipLogEntryType(LogEntryType):
def display(self, logentry):
return self.plain.format(user=logentry.parsed_data.get('email'))
def display(self, logentry, data):
return self.plain.format(user=data.get('email'))
@log_entry_types.new_from_dict({
@@ -537,9 +575,9 @@ class CoreTeamMembershipLogEntryType(TeamMembershipLogEntryType):
class TeamMemberJoinedLogEntryType(LogEntryType):
action_type = 'pretix.team.member.joined'
def display(self, logentry):
def display(self, logentry, data):
return _('{user} has joined the team using the invite sent to {email}.').format(
user=logentry.parsed_data.get('email'), email=logentry.parsed_data.get('invite_email')
user=data.get('email'), email=data.get('invite_email')
)
@@ -547,23 +585,23 @@ class TeamMemberJoinedLogEntryType(LogEntryType):
class UserSettingsChangedLogEntryType(LogEntryType):
action_type = 'pretix.user.settings.changed'
def display(self, logentry):
def display(self, logentry, data):
text = str(_('Your account settings have been changed.'))
if 'email' in logentry.parsed_data:
if 'email' in data:
text = text + ' ' + str(
_('Your email address has been changed to {email}.').format(email=logentry.parsed_data['email']))
if 'new_pw' in logentry.parsed_data:
_('Your email address has been changed to {email}.').format(email=data['email']))
if 'new_pw' in data:
text = text + ' ' + str(_('Your password has been changed.'))
if logentry.parsed_data.get('is_active') is True:
if data.get('is_active') is True:
text = text + ' ' + str(_('Your account has been enabled.'))
elif logentry.parsed_data.get('is_active') is False:
elif data.get('is_active') is False:
text = text + ' ' + str(_('Your account has been disabled.'))
return text
class UserImpersonatedLogEntryType(LogEntryType):
def display(self, logentry):
return self.plain.format(logentry.parsed_data['other_email'])
def display(self, logentry, data):
return self.plain.format(data['other_email'])
@log_entry_types.new_from_dict({
@@ -688,16 +726,13 @@ 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.'),
@@ -717,6 +752,19 @@ 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.'),
@@ -724,7 +772,7 @@ class CoreEventLogEntryType(EventLogEntryType):
class EventPluginStateLogEntryType(EventLogEntryType):
object_link_wrapper = _('Plugin {val}')
def get_object_link_info(self, logentry) -> dict:
def get_object_link_info(self, logentry) -> Optional[dict]:
if 'plugin' in logentry.parsed_data:
app = app_cache.get(logentry.parsed_data['plugin'])
if app and hasattr(app, 'PretixPluginMeta'):
@@ -759,17 +807,17 @@ class CoreItemLogEntryType(ItemLogEntryType):
'pretix.event.item.variation.changed': _('The variation "{value}" has been changed.'),
})
class VariationLogEntryType(ItemLogEntryType):
def display(self, logentry):
if 'value' not in logentry.parsed_data:
def display(self, logentry, data):
if 'value' not in data:
# Backwards compatibility
var = ItemVariation.objects.filter(id=logentry.parsed_data['id']).first()
var = ItemVariation.objects.filter(id=data['id']).first()
if var:
logentry.parsed_data['value'] = str(var.value)
data['value'] = str(var.value)
else:
logentry.parsed_data['value'] = '?'
data['value'] = '?'
else:
logentry.parsed_data['value'] = LazyI18nString(logentry.parsed_data['value'])
return super().display(logentry)
data['value'] = LazyI18nString(data['value'])
return super().display(logentry, data)
@log_entry_types.new_from_dict({
@@ -825,27 +873,27 @@ class CoreDiscountLogEntryType(DiscountLogEntryType):
class LegacyCheckinLogEntryType(OrderLogEntryType):
action_type = 'pretix.control.views.checkin'
def display(self, logentry):
def display(self, logentry, data):
# deprecated
dt = dateutil.parser.parse(logentry.parsed_data.get('datetime'))
dt = dateutil.parser.parse(data.get('datetime'))
tz = logentry.event.timezone
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
if 'list' in logentry.parsed_data:
if 'list' in data:
try:
checkin_list = logentry.event.checkin_lists.get(pk=logentry.parsed_data.get('list')).name
checkin_list = logentry.event.checkin_lists.get(pk=data.get('list')).name
except CheckinList.DoesNotExist:
checkin_list = _("(unknown)")
else:
checkin_list = _("(unknown)")
if logentry.parsed_data.get('first'):
if data.get('first'):
return _('Position #{posid} has been checked in manually at {datetime} on list "{list}".').format(
posid=logentry.parsed_data.get('positionid'),
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list,
)
return _('Position #{posid} has been checked in again at {datetime} on list "{list}".').format(
posid=logentry.parsed_data.get('positionid'),
posid=data.get('positionid'),
datetime=dt_formatted,
list=checkin_list
)
@@ -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 layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.hidden_if_item_available visibility_field=form.hidden_if_item_available_mode layout="control_with_visibility" %}
</fieldset>
{% for v in formsets.values %}
<fieldset>
@@ -2,5 +2,5 @@
{% if mode == "hide" %}
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Hide product if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-eye-slash"></span></span>
{% else %}
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Show info text if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-info-circle"></span></span>
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Show product with info on why its unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-info-circle"></span></span>
{% endif %}
+13 -7
View File
@@ -491,8 +491,11 @@ class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMix
if self.form.is_valid():
if self.form.has_changed():
self.request.event.log_action(
'pretix.event.payment.provider.' + self.provider.identifier, user=self.request.user, data={
k: self.form.cleaned_data.get(k) for k in self.form.changed_data
'pretix.event.payment.provider', user=self.request.user, data={
'provider': self.provider.identifier,
'new_values': {
k: self.form.cleaned_data.get(k) for k in self.form.changed_data
}
}
)
self.form.save()
@@ -888,11 +891,14 @@ class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
provider.form.save()
if provider.form.has_changed():
self.request.event.log_action(
'pretix.event.tickets.provider.' + provider.identifier, user=self.request.user, data={
k: (provider.form.cleaned_data.get(k).name
if isinstance(provider.form.cleaned_data.get(k), File)
else provider.form.cleaned_data.get(k))
for k in provider.form.changed_data
'pretix.event.tickets.provider', user=self.request.user, data={
'provider': provider.identifier,
'new_values': {
k: (provider.form.cleaned_data.get(k).name
if isinstance(provider.form.cleaned_data.get(k), File)
else provider.form.cleaned_data.get(k))
for k in provider.form.changed_data
}
}
)
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'provider': provider.identifier})
+1 -1
View File
@@ -313,7 +313,7 @@ class EventWizard(SafeSessionWizardView):
)
event.set_defaults()
if basics_data['tax_rate']:
if basics_data['tax_rate'] is not None:
if not event.settings.tax_rate_default or event.settings.tax_rate_default.rate != basics_data['tax_rate']:
event.settings.tax_rate_default = event.tax_rules.create(
name=LazyI18nString.from_gettext(gettext('VAT')),
+3 -2
View File
@@ -62,7 +62,8 @@ class MetricsMiddleware(object):
t0 = time.perf_counter()
resp = self.get_response(request)
tdiff = time.perf_counter() - t0
pretix_view_duration_seconds.observe(tdiff, status_code=resp.status_code, method=request.method,
url_name=url.namespace + ':' + url.url_name)
if url.url_name:
pretix_view_duration_seconds.observe(tdiff, status_code=resp.status_code, method=request.method,
url_name=url.namespace + ':' + url.url_name)
return resp
+6
View File
@@ -39,6 +39,12 @@ class ThumbnailingImageReader(ImageReader):
self._data = None
return width, height
def remove_transparency(self, background_color="WHITE"):
if "A" in self._image.mode:
new_image = Image.new("RGBA", self._image.size, background_color)
new_image.paste(self._image, mask=self._image)
self._image = new_image.convert("RGB")
def _jpeg_fh(self):
# Bypass a reportlab-internal optimization that falls back to the original
# file handle if the file is a JPEG, and therefore does not respect the
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 16:46+0000\n"
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
"PO-Revision-Date: 2025-01-22 16:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
"de/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.7\n"
"X-Generator: Weblate 5.9.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -777,13 +777,13 @@ msgstr "Preis"
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
msgstr "Originalpreis: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
msgstr "Neuer Preis: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
@@ -836,7 +836,7 @@ msgstr "ab %(currency)s %(price)s"
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
msgstr "Bild von %s"
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 16:46+0000\n"
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
"PO-Revision-Date: 2025-01-22 16:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.7\n"
"X-Generator: Weblate 5.9.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -776,13 +776,13 @@ msgstr "Preis"
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
msgstr "Originalpreis: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
msgstr "Neuer Preis: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
@@ -835,7 +835,7 @@ msgstr "ab %(currency)s %(price)s"
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
msgstr "Bild von %s"
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 16:46+0000\n"
"POT-Creation-Date: 2025-01-21 16:43+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 16:46+0000\n"
"PO-Revision-Date: 2025-01-16 10:32+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"PO-Revision-Date: 2025-01-21 00:00+0000\n"
"Last-Translator: Hector <hector@demandaeventos.es>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/"
"pretix-js/es/>\n"
"Language: es\n"
@@ -775,7 +775,7 @@ msgstr "Precio"
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr "Prix initial : %s"
msgstr "Precio original: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+20 -26
View File
@@ -8,10 +8,10 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 16:46+0000\n"
"PO-Revision-Date: 2024-12-27 11:45+0000\n"
"PO-Revision-Date: 2025-01-18 18:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/"
"pretix-js/ja/>\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -31,7 +31,7 @@ msgstr "注釈:"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
msgid "PayPal"
msgstr ""
msgstr "PayPal"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
msgid "Venmo"
@@ -60,19 +60,19 @@ msgstr "PayPal後払い"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL"
msgstr ""
msgstr "iDEAL"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"
msgstr ""
msgstr "SEPA Direct Debit"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
msgid "Bancontact"
msgstr ""
msgstr "Bancontact"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:44
msgid "giropay"
msgstr ""
msgstr "giropay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:45
msgid "SOFORT"
@@ -88,7 +88,7 @@ msgstr "MyBank"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:48
msgid "Przelewy24"
msgstr ""
msgstr "Przelewy24"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:49
msgid "Verkkopankki"
@@ -124,7 +124,7 @@ msgstr "Boleto"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:57
msgid "WeChat Pay"
msgstr ""
msgstr "WeChat Pay"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:58
msgid "Mercado Pago"
@@ -241,7 +241,7 @@ msgstr "確認済み"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Approval pending"
msgstr ""
msgstr "承認保留中"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed"
@@ -297,16 +297,12 @@ msgid "Ticket code revoked/changed"
msgstr "チケットコードのブロック/変更"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket blocked"
msgstr "チケット未払い"
msgstr "チケットブロック中"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket not valid at this time"
msgstr "チケット未払い"
msgstr "現時点で無効なチケット"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
msgid "Order canceled"
@@ -314,11 +310,11 @@ msgstr "注文がキャンセルされました"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
msgid "Ticket code is ambiguous on list"
msgstr ""
msgstr "リストのチケットコードは曖昧です"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
msgid "Order not approved"
msgstr ""
msgstr "承認されない注文"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets"
@@ -455,7 +451,7 @@ msgstr "商品の種類"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
msgid "Gate"
msgstr ""
msgstr "ゲート"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:111
msgid "Current date and time"
@@ -576,10 +572,8 @@ msgid "Text object (deprecated)"
msgstr "テキストオブジェクト (廃止済)"
#: pretix/static/pretixcontrol/js/ui/editor.js:901
#, fuzzy
#| msgid "Text object"
msgid "Text box"
msgstr "テキストオブジェクト"
msgstr "テキストボックス"
#: pretix/static/pretixcontrol/js/ui/editor.js:903
msgid "Barcode area"
@@ -770,13 +764,13 @@ msgstr "価格"
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
msgstr "元の価格: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
msgstr "新しい価格: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
@@ -829,7 +823,7 @@ msgstr "%(currency)s %(price)sから"
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
msgstr "%sのイメージ"
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -51,8 +51,8 @@ def register_payment_provider(sender, **kwargs):
class PaypalEventLogEntryType(EventLogEntryType):
action_type = 'pretix.plugins.paypal.event'
def display(self, logentry):
event_type = logentry.parsed_data.get('event_type')
def display(self, logentry, data):
event_type = data.get('event_type')
text = None
plains = {
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
@@ -82,17 +82,17 @@
{% endif %}
{% if event_logo and event_logo_image_large %}
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}"
aria-label="{% trans 'Homepage' %}" title="{% trans 'Homepage' %}">
title="{% trans 'Homepage' %}">
<img src="{{ event_logo|thumb:'1170x5000' }}" alt="{{ event.name }}" class="event-logo" />
</a>
{% elif event_logo %}
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}"
aria-label="{% trans 'Homepage' %}" title="{% trans 'Homepage' %}">
title="{% trans 'Homepage' %}">
<img src="{{ event_logo|thumb:'5000x120' }}" alt="{{ event.name }}" class="event-logo" />
</a>
{% else %}
<h1>
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}" aria-label="{% trans 'Homepage' %}">{{ event.name }}</a>
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}">{{ event.name }}</a>
{% if request.event.settings.show_dates_on_frontpage and not event.has_subevents %}
<small>{{ event.get_date_range_display_as_html }}</small>
{% endif %}
@@ -5,6 +5,10 @@
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
<p><small><a href="#voucher">{% trans "Enter a voucher code below to buy this product." %}</a></small></p>
</div>
{% elif item.current_unavailability_reason == 'hidden_if_item_available' %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
<p><small>{% trans "Not available yet." %}</small></p>
</div>
{% elif item.current_unavailability_reason == 'available_from' or var.current_unavailability_reason == 'available_from' %}
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
<p><small>{% trans "Not available yet." %}</small></p>
@@ -268,8 +268,14 @@
{% if line.discount and line.line_price_gross != line.price %}
<span class="text-success discounted" data-toggle="tooltip" title="{% trans "The price of this product was reduced because of an automatic discount." %}">
<br>
<span class="fa fa-percent fa-fw" aria-hidden="true"></span>
{% trans "Discounted" %}
<span class="fa fa-star fa-fw" aria-hidden="true"></span>
{% if line.price < line.line_price_gross %}
{% blocktranslate trimmed with percent=line.discount_percentage|floatformat:0 %}
{{ percent }} % Discount
{% endblocktranslate %}
{% else %}
{% trans "Discounted" %}
{% endif %}
</span>
{% endif %}
</div>
@@ -335,8 +341,14 @@
{% if line.discount and line.line_price_gross != line.price %}
<span class="text-success discounted" data-toggle="tooltip" title="{% trans "The price of this product was reduced because of an automatic discount." %}">
<br>
<span class="fa fa-percent fa-fw" aria-hidden="true"></span>
{% trans "Discounted" %}
<span class="fa fa-star fa-fw" aria-hidden="true"></span>
{% if line.price < line.line_price_gross %}
{% blocktranslate trimmed with percent=line.discount_percentage|floatformat:0 %}
{{ percent }} % Discount
{% endblocktranslate %}
{% else %}
{% trans "Discounted" %}
{% endif %}
</span>
{% endif %}
</div>
+4 -4
View File
@@ -70,7 +70,7 @@ from pretix.base.models import (
)
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.items import (
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
)
from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.services.quotas import QuotaAvailability
@@ -302,14 +302,14 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
if item.hidden_if_item_available:
if item.hidden_if_item_available.has_variations:
dependency_available = any(
item._dependency_available = any(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
for var in item.hidden_if_item_available.available_variations
)
else:
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
dependency_available = q[0] == Quota.AVAILABILITY_OK
if dependency_available:
item._dependency_available = q[0] == Quota.AVAILABILITY_OK
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
item._remove = True
continue
+10 -3
View File
@@ -34,10 +34,17 @@ $(function () {
}
for(var k in dependents) {
const options = data[k],
dependent = dependents[k],
visible = 'visible' in options ? options.visible : true,
required = 'required' in options && options.required && isRequired && visible;
dependent = dependents[k];
let visible = 'visible' in options ? options.visible : true;
if (dependent.is("[data-display-dependency]")) {
const dependency = $(dependent.attr("data-display-dependency"));
visible = visible && (
(dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val()
);
}
const required = 'required' in options && options.required && isRequired && visible;
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required);
}
@@ -38,6 +38,7 @@ var strings = {
'unavailable_available_from': django.pgettext('widget', 'Not yet available'),
'unavailable_available_until': django.pgettext('widget', 'Not available anymore'),
'unavailable_active': django.pgettext('widget', 'Currently not available'),
'unavailable_hidden_if_item_available': django.pgettext('widget', 'Not yet available'),
'order_min': django.pgettext('widget', 'minimum amount to order: %s'),
'exit': django.pgettext('widget', 'Close ticket shop'),
'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'),
@@ -229,7 +230,7 @@ Vue.component('availbox', {
+ ' v-bind:aria-label="label_select_item"'
+ '>'
+ '</label>'
+ '<div :class="count_group_classes" v-else>'
+ '<div :class="count_group_classes" v-else role="group" v-bind:aria-label="item.name">'
+ '<button v-if="!$root.use_native_spinners" type="button" @click.prevent.stop="on_step" data-step="-1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-dec" aria-label="' + strings.quantity_dec + '"><span>-</span></button>'
+ '<input type="number" inputmode="numeric" pattern="\d*" class="pretix-widget-item-count-multiple" placeholder="0" min="0"'
+ ' v-model="amount_selected" :max="order_max" :name="input_name" :id="\'input_\' + input_name"'
@@ -722,7 +723,6 @@ var shared_methods = {
},
buy_callback: function (data) {
if (data.redirect) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
if (data.cart_id) {
this.$root.cart_id = data.cart_id;
setCookie(this.$root.cookieName, data.cart_id, 30);
@@ -747,7 +747,7 @@ var shared_methods = {
}
this.$root.overlay.frame_loading = false;
} else {
iframe.src = url;
this.$root.overlay.frame_src = url;
}
} else {
this.async_task_id = data.async_id;
@@ -786,9 +786,7 @@ var shared_methods = {
if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams;
}
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
this.$root.overlay.frame_src = redirect_url;
},
voucher_open: function (voucher) {
var redirect_url;
@@ -800,9 +798,7 @@ var shared_methods = {
redirect_url += '&' + this.$root.additionalURLParams;
}
if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
this.$root.overlay.frame_src = redirect_url;
} else {
window.open(redirect_url);
}
@@ -825,9 +821,7 @@ var shared_methods = {
redirect_url += '&' + this.$root.additionalURLParams;
}
if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
this.$root.overlay.frame_src = redirect_url;
} else {
window.open(redirect_url);
}
@@ -852,27 +846,27 @@ var shared_loading_fragment = (
);
var shared_iframe_fragment = (
'<div :class="frameClasses">'
'<div :class="frameClasses" role="dialog" aria-modal="true" >'
+ '<div class="pretix-widget-frame-loading" v-show="$root.frame_loading">'
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
+ '</div>'
+ '<div class="pretix-widget-frame-inner" ref="frame-container" v-show="$root.frame_shown">'
+ '<div class="pretix-widget-frame-close"><a href="#" @click.prevent.stop="close" role="button" aria-label="'+strings.close+'">'
+ '<svg height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</a></div>'
+ '<iframe frameborder="0" width="650" height="650" @load="iframeLoaded" '
+ ' :name="$root.parent.widget_id" src="about:blank" v-once'
+ ' allow="autoplay *; camera *; fullscreen *; payment *"'
+ ' referrerpolicy="origin">'
+ 'Please enable frames in your browser!'
+ '</iframe>'
+ '<div class="pretix-widget-frame-close"><a href="#" @click.prevent.stop="close" role="button" aria-label="'+strings.close+'">'
+ '<svg height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</a></div>'
+ '</div>'
+ '</div>'
);
var shared_alert_fragment = (
'<div :class="alertClasses">'
+ '<transition name="bounce">'
'<div :class="alertClasses" role="dialog" aria-modal="true" aria-live="polite">'
+ '<transition name="bounce" @after-enter="focusButton">'
+ '<div class="pretix-widget-alert-box" v-if="$root.error_message">'
+ '<p>{{ $root.error_message }}</p>'
+ '<p><button v-if="$root.error_url_after" @click.prevent.stop="errorContinue">' + strings.continue + '</button>'
@@ -965,8 +959,7 @@ Vue.component('pretix-overlay', {
window.open(this.$root.error_url_after);
return;
}
var iframe = this.$refs['frame-container'].children[0];
iframe.src = this.$root.error_url_after;
this.$root.overlay.frame_src = this.$root.error_url_after;
this.$root.frame_loading = true;
this.$root.error_message = null;
this.$root.error_url_after = null;
@@ -974,15 +967,22 @@ Vue.component('pretix-overlay', {
close: function () {
this.$root.frame_shown = false;
this.$root.parent.frame_dismissed = true;
this.$root.frame_src = "";
this.$root.parent.reload();
this.$root.parent.trigger_close_callback();
},
iframeLoaded: function () {
if (this.$root.frame_loading) {
this.$root.frame_loading = false;
this.$root.frame_shown = true;
if (this.$root.frame_src) {
this.$root.frame_shown = true;
}
}
}
},
focusButton: function () {
this.$el.querySelector(".pretix-widget-alert-box button").focus();
},
}
});
@@ -1737,8 +1737,7 @@ var shared_root_methods = {
} else {
url += '?iframe=1';
}
this.$root.overlay.$children[0].$refs['frame-container'].children[0].src = url;
this.$root.overlay.frame_loading = true;
this.$root.overlay.frame_src = url;
} else {
event.target.href = url;
return;
@@ -1901,9 +1900,7 @@ var shared_root_methods = {
redirect_url += '&' + this.$root.additionalURLParams;
}
if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
this.$root.overlay.frame_src = redirect_url;
} else {
window.open(redirect_url);
}
@@ -1927,9 +1924,7 @@ var shared_root_methods = {
redirect_url += '&' + this.$root.additionalURLParams;
}
if (this.$root.useIframe) {
var iframe = this.$root.overlay.$children[0].$refs['frame-container'].children[0];
this.$root.overlay.frame_loading = true;
iframe.src = redirect_url;
this.$root.overlay.frame_src = redirect_url;
} else {
window.open(redirect_url);
}
@@ -2066,9 +2061,41 @@ var create_overlay = function (app) {
error_url_after_new_tab: true,
error_message: null,
lightbox: null,
prevActiveElement: null,
}
},
props: {
frame_src: String,
},
methods: {
},
watch: {
frame_src: function (newValue, oldValue) {
// show loading spinner only when previously no frame_src was set
if (newValue && !oldValue) {
this.frame_loading = true;
}
// to close and unload the iframe, frame_src can be empty -> make it valid HTML with about:blank
this.$el.querySelector("iframe").src = newValue || "about:blank";
},
frame_shown: function (newValue) {
if (newValue) {
this.prevActiveElement = document.activeElement;
var btn = this.$el?.querySelector(".pretix-widget-frame-close a");
this.$nextTick(function () {
btn?.focus();
});
} else {
this.prevActiveElement?.focus();
}
},
error_message: function (newValue) {
if (newValue) {
this.prevActiveElement = document.activeElement;
} else {
this.prevActiveElement?.focus();
}
},
}
});
app.$root.overlay = framechild;
+1
View File
@@ -323,6 +323,7 @@ TEST_ITEM_RES = {
"max_per_order": None,
"hidden_if_available": None,
"hidden_if_item_available": None,
"hidden_if_item_available_mode": "hide",
"checkin_attention": False,
"checkin_text": None,
"has_variations": False,
+9 -2
View File
@@ -673,7 +673,12 @@ class ItemsTest(ItemFormTest):
with scopes_disabled():
q = Question.objects.create(event=self.event1, question="Size", type="N")
q.items.add(self.item2)
self.item2.sales_channels = ["web", "bar"]
self.item2.limit_sales_channels.set(self.orga1.sales_channels.filter(identifier__in=["web", "bar"]))
self.item2.all_sales_channels = False
self.item2.save()
self.var2.limit_sales_channels.set(self.orga1.sales_channels.filter(identifier__in=["web"]))
self.var2.all_sales_channels = False
self.var2.save()
prop = self.event1.item_meta_properties.create(name="Foo")
self.item2.meta_values.create(property=prop, value="Bar")
@@ -690,6 +695,7 @@ class ItemsTest(ItemFormTest):
with scopes_disabled():
i_old = Item.objects.get(name__icontains='Business')
i_new = Item.objects.get(name__icontains='Intermediate')
v2_new = i_new.variations.get(value__icontains='Gold')
assert i_new.category == i_old.category
assert i_new.description == i_old.description
assert i_new.active == i_old.active
@@ -698,7 +704,8 @@ class ItemsTest(ItemFormTest):
assert i_new.require_voucher == i_old.require_voucher
assert i_new.hide_without_voucher == i_old.hide_without_voucher
assert i_new.allow_cancel == i_old.allow_cancel
assert set(i_new.limit_sales_channels.all()) == set(i_old.limit_sales_channels.all())
assert set(i_new.limit_sales_channels.values_list("identifier", flat=True)) == {"web", "bar"}
assert set(v2_new.limit_sales_channels.values_list("identifier", flat=True)) == {"web"}
assert i_new.meta_data == i_old.meta_data == {"Foo": "Bar"}
assert set(i_new.questions.all()) == set(i_old.questions.all())
assert set([str(v.value) for v in i_new.variations.all()]) == set([str(v.value) for v in i_old.variations.all()])