Compare commits

..
Author SHA1 Message Date
Richard Schreiber 825e932ea6 fix flake8 2026-09-22 13:44:06 +02:00
Richard Schreiber b363c71ae4 undo test changes in events test 2026-09-22 13:41:44 +02:00
Richard Schreiber 8eff96557c make label_child configurable if MetaPropertyDictField should contain non-localized stuff 2026-09-22 11:54:46 +02:00
Richard Schreiber d3090a7499 fix docs for i18n strings 2026-09-22 11:50:54 +02:00
Richard Schreiber 1e6c167bd7 update MetaPropertyDictField 2026-09-22 11:49:55 +02:00
Richard Schreiber ab248d0d97 Change to I18nField for validation 2026-09-22 11:38:51 +02:00
Richard Schreiber 0e38518e9f Improve validation 2026-09-22 10:50:49 +02:00
Richard Schreiber 4ea5a6128b fix validation result 2026-09-21 12:47:35 +02:00
Richard Schreiber 3fe39d77cd Make ObjectListField more flexibel for re-use 2026-09-21 12:27:47 +02:00
Richard Schreiber 59216c0bd5 fix permission tests 2026-09-21 11:15:43 +02:00
Richard Schreiber 6aff1cf55c fix flake8 2026-09-21 09:21:56 +02:00
Richard Schreiber 3fc0ddd1d5 update tests to check for error-messages as well 2026-09-21 09:20:13 +02:00
Richard SchreiberandRaphael Michel e47fa4ceb8 Apply batched suggestions from code review
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-09-21 09:01:20 +02:00
Richard Schreiber 27d66349bc add safe-guard normalization to None to to_representation 2026-09-21 08:59:59 +02:00
Richard SchreiberandRichard Schreiber 09b10127b4 Apply batched suggestions from code review
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
2026-09-18 13:45:40 +02:00
Richard Schreiber 08f66fae3a filter unknown keys from choices due to django-formsets 2026-09-18 13:40:13 +02:00
Richard Schreiber cde6c301b2 fix choices validation 2026-09-18 13:02:28 +02:00
Richard Schreiber ff3da7c8f6 add meta_properties from organizer only 2026-09-18 12:35:58 +02:00
Richard Schreiber 403efcf38c validate and add tests 2026-09-18 12:35:41 +02:00
Phin Wolkwitz 71153ccdf4 Fix logentry again 2026-09-09 14:20:10 +02:00
Phin Wolkwitz 4f343aac1f Fix logentry 2026-09-09 14:13:56 +02:00
Phin Wolkwitz b4b5dd2ff3 Add new doc-file to index, fix spelling and description 2026-09-09 14:08:45 +02:00
Phin Wolkwitz 4f0d5f0a64 Add API-endpoint for event-meta-properties 2026-09-07 16:07:10 +02:00
124 changed files with 11136 additions and 11199 deletions
-2
View File
@@ -1,2 +0,0 @@
# Format pre-vue code with eslint where possible (2026-09-10)
d85e52c83ed3639e040372fd0052e842e4899d90
+241
View File
@@ -0,0 +1,241 @@
Event Meta Properties
=====================
Resource description
--------------------
An event meta property is used to to define meta information fields for its events.
This information can be re-used, for example, in ticket layouts.
The event meta property resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Unique ID for this property
name string Name of the property
default string Value of the default option
required boolean If ``true``, an event can only be taken live if the
property is set. In event series, it's always optional
to set a value for individual dates
protected boolean If ``true``, the value for an event can only be changed
by organizer-level administrators
filter_public boolean If ``true``, this property will be shown to filter
events in the public event list and calendar
public_label string Public name of the property
filter_allowed boolean If ``true``, this property will be shown to filter
events or reports in the backend, and it can also be
used for hidden filter parameters in the frontend
choices list of objects List of JSON objects representing all permitted values
for this property, or ``null`` for no limitation.
Each choice object has a required internal name named
``key`` and optional public name named ``label``
consisting of a dictionary of i18n string translations
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/
Returns a list of all meta properties for the organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": [
{
"key": "blue",
"label": {
"en": "Blue"
},
}
]
}
]
}
:param organizer: The ``slug`` field of the organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Returns information on one property, identified by its id.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to retrieve
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/event_meta_properties/
Creates a new meta property
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/event_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "ref-code",
"default": "abcde",
"required": true,
"choices": null
}
**Example response**:
.. sourcecode:: http
{
"id": 2,
"name": "reference",
"default": "abcde",
"required": true,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:statuscode 201: no error
:statuscode 400: The meta property could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Update a meta property. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide
all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the
fields that you want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/event_meta_properties/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"required": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 3,
"name": "reference",
"default": "abcde",
"required": false,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to modify
:statuscode 200: no error
:statuscode 400: The property could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Delete a meta property.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
+7 -9
View File
@@ -566,7 +566,7 @@ organizer level.
Content-Type: application/json
{
"region": "DE",
"imprint_url": "https://pretix.eu",
}
@@ -579,14 +579,12 @@ organizer level.
Content-Type: application/json
{
"region":
"imprint_url":
{
"value": "DE",
"label": "Region",
"value": "https://pretix.eu",
"label": "Imprint URL",
"readonly": false,
"help_text": "Will be used to determine date and time formatting as well as default country for customer
addresses and phone numbers. For formatting, this takes less priority than the language and
is therefore mostly relevant for languages used in different regions globally (like English)."
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
}
},
@@ -622,7 +620,7 @@ organizer level.
Content-Type: application/json
{
"region": "DE"
"imprint_url": "https://example.org/imprint/"
}
**Example response**:
@@ -634,7 +632,7 @@ organizer level.
Content-Type: application/json
{
"region": "DE",
"imprint_url": "https://example.org/imprint/",
}
+1
View File
@@ -12,6 +12,7 @@ at :ref:`plugin-docs`.
organizers
events
subevents
event_meta_properties
taxrules
categories
items
+1 -58
View File
@@ -8,64 +8,7 @@ import vuePug from 'eslint-plugin-vue-pug'
const ignores = globalIgnores([
'**/node_modules',
'**/dist',
// Vendored code
'src/pretix/static/leaflet',
'src/pretix/static/clipboard',
'src/pretix/static/cropper',
'src/pretix/static/lightbox',
'src/pretix/static/are-you-sure',
'src/pretix/static/vuejs',
'src/pretix/static/fontawesome',
'src/pretix/static/typeahead',
'src/pretix/static/moment',
'src/pretix/static/pdfjs',
'src/pretix/static/sortable',
'src/pretix/static/iframeresizer',
'src/pretix/static/bootstrap',
'src/pretix/static/d3',
'src/pretix/static/jsi18n',
'src/pretix/static/fabric',
'src/pretix/static/datetimepicker',
'src/pretix/static/charts',
'src/pretix/static/fileupload',
'src/pretix/static/seating',
'src/pretix/static/rest_framework',
'src/pretix/static/select2',
'src/pretix/static/schema',
'src/pretix/static/slider',
'src/pretix/static/jquery',
'src/pretix/static/colorpicker',
'src/pretix/static/rrule',
'src/pretix/static/pretixcontrol/js/jquery.qrcode.min.js',
'src/pretix/static/pretixpresale/js/widget/docready.js',
// Pre-vue JS code
'src/pretix/static/pretixbase/js/addressform.js',
'src/pretix/static/pretixbase/js/asynctask.js',
'src/pretix/static/pretixbase/js/details.js',
'src/pretix/static/pretixbase/js/gettextstub.js',
'src/pretix/static/pretixbase/js/i18nstring.js',
'src/pretix/static/pretixcontrol/js/menu.js',
'src/pretix/static/pretixcontrol/js/ui/editor.js',
'src/pretix/static/pretixcontrol/js/ui/geo.js',
'src/pretix/static/pretixcontrol/js/ui/main.js',
'src/pretix/static/pretixcontrol/js/ui/plugins.js',
'src/pretix/static/pretixcontrol/js/ui/subevent.js',
'src/pretix/static/pretixcontrol/js/ui/variations.js',
'src/pretix/static/pretixcontrol/js/ui/webauthn.js',
'src/pretix/static/pretixpresale/js/ui/cart.js',
'src/pretix/static/pretixpresale/js/ui/main.js',
'src/pretix/static/pretixpresale/js/ui/questions.js',
'src/pretix/static/pretixpresale/js/widget/floatformat.js',
'src/pretix/static/pretixpresale/js/widget/widget.js',
'src/pretix/plugins/banktransfer/static',
'src/pretix/plugins/paypal2/static',
'src/pretix/plugins/statistics/static',
'src/pretix/plugins/stripe/static',
// Plugin checkouts
'local',
// docs
'doc',
'**/dist'
])
export default defineConfig([
+13 -27
View File
@@ -294,43 +294,29 @@
}
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -3394,9 +3380,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz",
"integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4162,9 +4148,9 @@
}
},
"node_modules/smol-toml": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz",
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
+9 -9
View File
@@ -33,14 +33,14 @@ dependencies = [
"bleach==6.4.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=50.0.1",
"cryptography>=50.0.0",
"css-inline==0.21.*",
"defusedcsv>=3.0.0",
"dnspython==2.*",
"Django[argon2]==5.2.*,>=5.2.17",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.2",
"django-compressor==4.6.0",
"django-countries==9.1.*",
"django-countries==9.0.*",
"django-filter==26.1",
"django-formset-js-improved==0.5.0.5",
"django-formtools==2.7",
@@ -56,7 +56,7 @@ dependencies = [
"django-querytagger==0.0.3",
"django-redis==7.0.*",
"django-scopes==2.1.*",
"django-statici18n==2.8.*",
"django-statici18n==2.7.*",
"djangorestframework==3.17.*",
"dnspython==2.8.*",
"drf_ujson2==1.7.*",
@@ -75,7 +75,7 @@ dependencies = [
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.14.*",
"PyJWT==2.13.*",
"phonenumberslite==9.0.*",
"Pillow==12.3.*",
"pretix-plugin-build",
@@ -84,7 +84,7 @@ dependencies = [
"pycountry",
"pycparser==3.0",
"pycryptodome==3.23.*",
"pypdf==6.19.*",
"pypdf==6.5.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
@@ -94,7 +94,7 @@ dependencies = [
"redis==7.4.*",
"reportlab==5.0.*",
"requests==2.34.*",
"sentry-sdk==2.69.*",
"sentry-sdk==2.68.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -112,10 +112,10 @@ dev = [
"aiohttp==3.14.*",
"coverage",
"coveralls",
"fakeredis==2.38.*",
"fakeredis==2.37.*",
"flake8==7.3.*",
"freezegun",
"isort==9.0.*",
"isort==8.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=1.4.0",
+90 -3
View File
@@ -28,6 +28,7 @@ from django.db import transaction
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import gettext, gettext_lazy as _
from i18nfield.rest_framework import I18nField
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -40,9 +41,10 @@ from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region
from pretix.base.models import (
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
Customer, Device, EventMetaProperty, GiftCard, GiftCardAcceptance,
GiftCardTransaction, Membership, MembershipType, OrderPosition, Organizer,
ReusableMedium, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite,
User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.permissions import (
@@ -640,3 +642,88 @@ class OrganizerSettingsSerializer(SettingsSerializer):
)
# TODO: make sure pub is always correct
return 'pub/' + fname
class MetaPropertyListField(serializers.ListField):
def __init__(self, *args, **kwargs):
kwargs["validators"] = kwargs.pop("validators", [])
def validate_keys_unique(choices):
if not choices:
return
keys = [c.get("key") for c in choices]
if len(set(keys)) < len(keys):
raise ValidationError("The key for each meta property must be unique.")
kwargs["validators"].append(
validate_keys_unique
)
super().__init__(*args, **kwargs)
class MetaPropertyDictField(serializers.DictField):
def __init__(self, **kwargs):
self.label_child = kwargs.pop("label_child", I18nField())
super().__init__(**kwargs)
def to_representation(self, value):
# django added unneccessary keys DELETE, ORDER through formsets, filter them here for backwards compat
d = {
"key": value["key"]
}
if "label" in value:
d["label"] = self.label_child.to_representation(value["label"])
return super().to_representation(d)
def to_internal_value(self, data):
if not isinstance(data, dict):
raise ValidationError("Meta properties must be a dict.")
if not isinstance(data.get("key"), str):
raise ValidationError("Meta properties must have a key of type string.")
if any(k not in {"key", "label"} for k in data.keys()):
raise ValidationError("Meta properties may only have a key and optionally a label.")
if "label" in data:
try:
data["label"] = self.label_child.to_internal_value(data["label"])
except ValidationError as e:
raise ValidationError({"label": e.detail})
return super().to_internal_value(data)
class EventMetaPropertiesSerializer(I18nAwareModelSerializer):
choices = MetaPropertyListField(
child=MetaPropertyDictField(
label_child=I18nField()
),
allow_null=True,
)
class Meta:
model = EventMetaProperty
fields = (
'id', 'name', 'default', 'required', 'protected', 'filter_public', 'public_label', 'filter_allowed',
'choices'
)
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
choices = full_data.get("choices")
default = full_data.get("default")
if choices and default:
choice_keys = [c.get("key") for c in choices]
if default not in choice_keys:
raise ValidationError("You cannot set a default value that is not a valid value.")
if not choices and "choices" in data:
# normalize empty dict to None
data["choices"] = None
return data
+1
View File
@@ -68,6 +68,7 @@ orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportVie
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
orga_router.register(r'event_meta_properties', organizer.EventMetaPropertiesViewSet)
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
+51 -4
View File
@@ -44,15 +44,16 @@ from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.organizer import (
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
EventMetaPropertiesSerializer, GiftCardSerializer,
GiftCardTransactionSerializer, MembershipSerializer,
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
)
from pretix.base.models import (
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
TeamAPIToken, TeamInvite, User,
Customer, Device, Event, EventMetaProperty, GiftCard, GiftCardTransaction,
LogEntry, Membership, MembershipType, Organizer, SalesChannel, SeatingPlan,
Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
@@ -846,3 +847,49 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
data={'id': instance.pk}
)
instance.delete()
class EventMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = EventMetaPropertiesSerializer
queryset = EventMetaProperty.objects.none()
write_permission = 'organizer.settings.general:write'
def get_queryset(self):
return self.request.organizer.meta_properties.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.property.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.created',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
+4 -27
View File
@@ -54,7 +54,6 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ..exporter import BaseExporter, MultiSheetListExporter
from ..invoicing.transmission import get_transmission_types
from ..services.export import ExportError
from ..services.invoices import invoice_pdf_task
from ..signals import (
@@ -198,7 +197,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
def iterate_sheet(self, form_data, sheet):
_ = gettext
if sheet == 'invoices':
headers = [
yield [
_('Invoice number'),
_('Date'),
_('Order code'),
@@ -231,18 +230,8 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Total value (without taxes)'),
_('Payment matching IDs'),
_('Payment providers'),
_('Transmission type'),
_('Transmission status'),
_('Transmission date'),
]
transmission_types = get_transmission_types()
for tt in transmission_types:
for c in tt.describe_info_columns():
headers.append(str(tt.verbose_name) + ': ' + str(c))
yield headers
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -253,7 +242,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
'm'
).order_by()
base_qs = self.invoices_queryset(form_data)
base_qs = self.invoices_queryset(form_data)\
qs = base_qs.select_related(
'order', 'refers'
@@ -291,7 +280,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
if mid:
pmis.append(mid)
pmi = '\n'.join(pmis)
line = [
yield [
i.full_invoice_no,
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
@@ -326,20 +315,8 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(',')))
if p and p != 'free'
]),
i.transmission_type_instance.verbose_name,
i.get_transmission_status_display(),
date_format(i.transmission_date, "SHORT_DATETIME_FORMAT") if i.transmission_date else "",
])
]
for tt in transmission_types:
if tt.identifier == i.transmission_type:
described = dict(tt.describe_info(i.invoice_to_transmission_info, i.invoice_to_country, i.invoice_to_is_business))
for c in tt.describe_info_columns():
line.append(described.get(c, ""))
else:
for c in tt.describe_info_columns():
line.append("")
yield line
elif sheet == 'lines':
yield [
_('Invoice number'),
+9 -17
View File
@@ -350,22 +350,16 @@ class WrappedPhonePrefixSelect(Select):
return super().render(name, value or self.initial, *args, **kwargs)
def get_context(self, name, value, attrs):
# self.choices is lazy evaluated, needs to be realized to be modifiable
choices = list(self.choices)
if value and choices[1][0] != value:
matching_choices = len([1 for p, c in choices if p == value])
if value and self.choices[1][0] != value:
matching_choices = len([1 for p, c in self.choices if p == value])
if matching_choices > 1:
# Some countries share a phone prefix, for example +1 is used all over the Americas.
# This causes a UX problem: If the default value or the existing data is +12125552368,
# the widget will just show the first <option> entry with value="+1" as selected,
# which alphabetically is America Samoa, although most numbers statistically are from
# the US. As a workaround, we detect this case and add an additional choice value with
# the US. As a workaround, we detect this case and add an aditional choice value with
# just <option value="+1">+1</option> without an explicit country.
self.choices = [
choices[0],
(value, value),
*choices[1:],
]
self.choices.insert(1, (value, value))
context = super().get_context(name, value, attrs)
return context
@@ -1202,9 +1196,8 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
return field
def clean(self):
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time
validate_address,
)
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
d = super().clean()
@@ -1445,9 +1438,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['transmission_type'].widget.attrs['data-trigger-address-info'] = 'on'
def clean(self):
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time
validate_address,
)
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
data = self.cleaned_data
@@ -1501,7 +1493,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
"vat_id": _("This field is required.")
})
if self.validate_vat_id and self.instance.vat_id_validated and not any(v in self.changed_data for v in ('is_business', 'vat_id', 'country')):
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass # Skip re-validation if it is validated
elif self.validate_vat_id and vat_id_applicable:
try:
@@ -107,9 +107,6 @@ class TransmissionType:
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
return transmission_info
def describe_info_columns(self):
return [f.label for f in self.invoice_address_form_fields.values()]
def describe_info(self, transmission_info: dict, country: Country, is_business: bool):
form_data = self.transmission_info_to_form_data(transmission_info)
data = []
@@ -1,49 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-21 11:30
import django.db.models.deletion
from django.db import migrations, models
def fix_unshredded_invoices(apps, _):
Invoice = apps.get_model("pretixbase", "Invoice")
InvoiceLine = apps.get_model("pretixbase", "InvoiceLine")
ignore_fields = (
# bool/int fields are not listed and skipped automatically
'prefix', 'invoice_no', 'full_invoice_no', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id',
'invoice_from_vat_id', 'locale', 'payment_provider_stamp', 'footer_text', 'foreign_currency_display',
'foreign_currency_source', 'transmission_type', 'transmission_provider', 'transmission_status',
)
for i in Invoice.objects.filter(shredded=True):
for f in Invoice._meta.fields:
if f.name in ignore_fields:
continue
val = getattr(i, f.name, None)
if val and isinstance(val, str):
setattr(i, f.name, "")
elif val and isinstance(val, list): # jsonfield
setattr(i, f.name, [])
elif val and isinstance(val, dict): # jsonfield
setattr(i, f.name, {"_shredded": True})
i.save()
InvoiceLine.objects.filter(
attendee_name__isnull=False,
invoice__shredded=True
).update(attendee_name="")
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0310_question_valid_string_length_min"),
]
operations = [
migrations.RunPython(
fix_unshredded_invoices,
migrations.RunPython.noop,
),
]
-1
View File
@@ -166,7 +166,6 @@ class Device(LoggedModel):
)
security_profile = models.CharField(
max_length=190,
verbose_name=_('Security profile'),
default='full',
null=True,
blank=False
+40 -7
View File
@@ -626,14 +626,47 @@ class Order(LockModel, LoggedModel):
self.save(update_fields=['last_modified'])
def set_expires(self, now_dt=None, subevents=None):
from pretix.base.services.payment import compute_payment_deadline
now_dt = now_dt or now()
tz = ZoneInfo(self.event.settings.timezone)
self.expires = compute_payment_deadline(
event=self.event,
sales_channel=self.sales_channel,
now_dt=now_dt,
subevents=subevents,
)
sales_channel_suffix = "_" + self.sales_channel.identifier.replace(".", "_")
if not (mode := self.event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
mode = self.event.settings.get('payment_term_mode')
sales_channel_suffix = ""
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
self.expires = exp_by_date
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents and subevents:
terms = [
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return
term_last = min(terms)
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < self.expires:
self.expires = term_last
@cached_property
def tax_total(self):
+34 -40
View File
@@ -801,18 +801,6 @@ def generate_compressed_addon_list(op, order, event, only_checked_in=False):
return addonlist
def get_sizebox(page: pypdf.PageObject):
mediabox = page.mediabox
cropbox = page.cropbox
return pypdf.generic.RectangleObject((
max(mediabox[0], cropbox[0]),
max(mediabox[1], cropbox[1]),
min(mediabox[2], cropbox[2]),
min(mediabox[3], cropbox[3]),
))
class Renderer:
def __init__(self, event, layout, background_file):
@@ -1165,10 +1153,11 @@ class Renderer:
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
first_page = self.bg_pdf.pages[0]
sizebox = get_sizebox(first_page)
page_size = (sizebox.width, sizebox.height)
if first_page.rotation in (90, 270):
page_size = (
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
)
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
# swap dimensions due to pdf being rotated
page_size = page_size[::-1]
canvas.setPageSize(page_size)
@@ -1243,7 +1232,9 @@ class Renderer:
for i, page in enumerate(fg_pdf.pages):
bg_page = self.bg_pdf.pages[i]
_merge_with_correct_page_media_box(output, page, bg_page)
_correct_page_media_box(bg_page)
page.merge_page(bg_page, over=False)
output.add_page(page)
# pdf_header is a string like "%pdf-X.X"
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
@@ -1308,36 +1299,39 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
bg_pdf.write(bg_filename)
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
else:
output = PdfWriter()
for i, page in enumerate(fg_pdf.pages):
bg_page = bg_pdf.pages[i]
_merge_with_correct_page_media_box(output, page, bg_page)
_correct_page_media_box(bg_page)
page.merge_page(bg_page, over=False)
# pdf_header is a string like "%pdf-X.X"
output.pdf_header = (
bg_pdf.pdf_header
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:])
else fg_pdf.pdf_header
)
output.write(out_file)
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
fg_pdf.pdf_header = bg_pdf.pdf_header
fg_pdf.write(out_file)
def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject):
"""
Adds fg_page to output, merging bg_page behind it.
If bg_page has a non-zero mergebox/cropbox or is rotated via /Rotate, a transformation is applied to fix this."""
def _correct_page_media_box(page: pypdf.PageObject):
if page.rotation != 0:
page.transfer_rotation_to_content()
media_box = page.mediabox
trsf = pypdf.Transformation()
if bg_page.rotation != 0:
trsf = trsf.rotate(-bg_page.rotation)
mb = get_sizebox(bg_page)
pt1 = trsf.apply_on(mb.lower_left)
pt2 = trsf.apply_on(mb.upper_right)
trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1]))
fg_page = output.add_page(fg_page)
fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False)
if media_box.bottom != 0:
trsf = trsf.translate(0, -media_box.bottom)
if media_box.left != 0:
trsf = trsf.translate(-media_box.left, 0)
page.add_transformation(trsf, False)
for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]:
if b in page:
rr = pypdf.generic.RectangleObject(page[b])
pt1 = trsf.apply_on(rr.lower_left)
pt2 = trsf.apply_on(rr.upper_right)
page[pypdf.generic.NameObject(b)] = pypdf.generic.RectangleObject((
min(pt1[0], pt2[0]),
min(pt1[1], pt2[1]),
max(pt1[0], pt2[0]),
max(pt1[1], pt2[1]),
))
@deconstructible
-1
View File
@@ -1605,7 +1605,6 @@ def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None,
'max_value': str(max_value) if max_value is not None else None,
'info_data': info_data or {},
})
cart_session['payments_postpone'] = False
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
+3 -18
View File
@@ -961,7 +961,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
meta_info: dict, event: Event, sales_channel: SalesChannel, require_approval=False):
meta_info: dict, event: Event, require_approval=False):
fees = []
# Pre-rounding, pre-fee total is used for fee calculation
total = sum([c.gross_price_before_rounding for c in positions])
@@ -1021,14 +1021,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
payments_assigned += to_pay
p['payment_amount'] = to_pay
allow_postponed_payment = (
require_approval or
(
sales_channel.identifier in event.settings.payment_choice_postpone_allowed_channels and not payment_requests
)
)
if total != payments_assigned and not allow_postponed_payment:
if total != payments_assigned and not require_approval:
raise OrderError(_("The selected payment methods do not cover the total balance."))
return fees
@@ -1050,15 +1043,7 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
# Final calculation of fees, also performs final rounding
try:
fees = _apply_rounding_and_fees(
positions,
payment_requests,
address,
meta_info,
event,
sales_channel=sales_channel,
require_approval=require_approval
)
fees = _apply_rounding_and_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked'])
-76
View File
@@ -1,76 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
from django.utils.timezone import make_aware, now
from pretix.base.models import Event, SalesChannel
from pretix.base.reldate import RelativeDateWrapper
def compute_payment_deadline(event: Event, sales_channel: SalesChannel, now_dt=None, subevents=None) -> datetime:
now_dt = now_dt or now()
tz = ZoneInfo(event.settings.timezone)
sales_channel_suffix = "_" + sales_channel.identifier.replace(".", "_")
if not (mode := event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
mode = event.settings.get('payment_term_mode')
sales_channel_suffix = ""
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(
days=event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(
minutes=event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
expires = exp_by_date
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if event.has_subevents and subevents:
terms = [
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return expires
term_last = min(terms)
else:
term_last = term_last.datetime(event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < expires:
return term_last
return expires
File diff suppressed because one or more lines are too long
+10 -22
View File
@@ -50,8 +50,8 @@ from pretix.api.serializers.order import (
from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.i18n import LazyLocaleException
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, Invoice, InvoiceAddress,
OrderPayment, OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment,
OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
)
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.signals import register_data_shredders
@@ -598,30 +598,18 @@ class InvoiceShredder(BaseDataShredder):
def shred_data(self, progress_callback=None):
qs_i = self.event.invoices.filter(shredded=False)
total = qs_i.count()
ignore_fields = (
'prefix', 'invoice_no', 'full_invoice_no', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id',
'invoice_from_vat_id', 'locale', 'payment_provider_stamp', 'footer_text', 'foreign_currency_display',
'foreign_currency_source', 'transmission_type', 'transmission_provider', 'transmission_status',
)
for i in _progress_helper(qs_i, progress_callback, 0, total):
if i.file:
i.file.delete()
i.shredded = True
for f in Invoice._meta.fields:
if f.name in ignore_fields:
continue
val = getattr(i, f.name, None)
if val and isinstance(val, str):
setattr(i, f.name, "")
elif val and isinstance(val, list): # jsonfield
setattr(i, f.name, [])
elif val and isinstance(val, dict): # jsonfield
setattr(i, f.name, {"_shredded": True})
i.save()
i.lines.update(description="", attendee_name="")
i.shredded = True
i.introductory_text = ""
i.additional_text = ""
i.invoice_to = ""
i.payment_provider_text = ""
i.transmission_info = {"_shredded": True}
i.save()
i.lines.update(description="")
class CachedTicketShredder(BaseDataShredder):
+23 -52
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
from decimal import ROUND_HALF_UP, Decimal
from typing import Optional
from babel import Locale, UnknownLocaleError
from babel.numbers import format_currency
@@ -36,32 +35,32 @@ register = template.Library()
@register.filter("money")
def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_currency=False):
if isinstance(value, (float, int, str)):
if value == '':
return value
def money_filter(value: Decimal, arg='', hide_currency=False):
if isinstance(value, (float, int)):
value = Decimal(value)
if value is None:
value = Decimal('0.00')
if not isinstance(value, Decimal):
if value == '':
return value
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
if not arg:
raise ValueError("No currency passed.")
arg = arg.upper()
if value.normalize().as_tuple().exponent < -9:
# Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we
# do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations
# are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python
# land to this filter (even though it should not happen).
value = value.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize()
currency_places = settings.CURRENCY_PLACES.get(arg, 2)
required_places = -value.normalize().as_tuple().exponent
render_places = max(currency_places, required_places)
places = settings.CURRENCY_PLACES.get(arg, 2)
rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
if places < 2 and rounded != value:
# We display decimal places even if we shouldn't for this currency if rounding
# would make the numbers incorrect. If this branch executes, it's likely a bug in
# pretix, but we won't show wrong numbers!
if hide_currency:
return floatformat(value, "2g")
else:
return '{} {}'.format(arg, floatformat(value, "2g"))
if hide_currency:
return floatformat(value, f"{render_places}g")
return floatformat(value, f"{places}g")
try:
locale = Locale(get_babel_locale())
@@ -69,29 +68,14 @@ def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_curr
locale = "en"
try:
return format_currency(
value,
arg,
locale=locale,
# We only allow Babel to restrict the digits to the digits defined by the currency if this does not remove any
# precision in case we have sub-currency precision (which we shouldn't have in most places, but it's still
# better than showing wrong data). Note: Weird precision effects can occur after in-database arithmetic
# on SQLite, since SQLite does not have fixed-decimal computation.
currency_digits=currency_places >= required_places,
decimal_quantization=currency_places >= required_places,
)
return format_currency(value, arg, locale=locale)
except:
return '{} {}'.format(arg, floatformat(value, f"{render_places}g"))
@register.filter("money_without_currency")
def money_filter_without_currency(value: Optional[Decimal | float | int | str], arg=''):
return money_filter(value, arg, hide_currency=True)
return '{} {}'.format(arg, floatformat(value, f"{places}g"))
@register.filter("money_numberfield")
def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg=''):
if isinstance(value, (float, int, str)):
def money_numberfield_filter(value: Decimal, arg=''):
if isinstance(value, (float, int)):
value = Decimal(value)
if not isinstance(value, Decimal):
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
@@ -103,28 +87,15 @@ def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg='
@register.filter(is_safe=True)
def tax_rate_format(number: Optional[Decimal | float | int | str]):
def tax_rate_format(number):
"""
Display a Decimal to its significant decimal places, used for tax rates.
"""
if isinstance(number, (float, int, str)):
if number == '':
return number
number = Decimal(number)
if number is None:
number = Decimal('0.00')
if not isinstance(number, Decimal):
raise TypeError("Invalid data type passed to tax rate format filter: %r" % type(number))
if number.normalize().as_tuple().exponent < -9:
# Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we
# do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations
# are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python
# land to this filter (even though it should not happen).
number = number.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize()
assert isinstance(number, Decimal)
return mark_safe(
formats.number_format(
number,
-number.normalize().as_tuple().exponent,
number.normalize(),
-number.as_tuple().exponent,
use_l10n=True,
force_grouping=False,
)
+1 -12
View File
@@ -172,9 +172,7 @@ class CachedFileInput(forms.ClearableFileInput):
from ...base.models import CachedFile
v = super().value_from_datadict(data, files, name)
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
if not v.allowed_for_session(self.request):
v = None
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
return v
def get_context(self, name, value, attrs):
@@ -246,11 +244,6 @@ class ExtFileField(ExtValidationMixin, SizeFileField):
class CachedFileField(ExtFileField):
widget = CachedFileInput
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.widget.request = self.request
def to_python(self, data):
from ...base.models import CachedFile
@@ -278,8 +271,6 @@ class CachedFileField(ExtFileField):
filename=data.name,
type=data.content_type,
)
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file)
cf.save()
data._uploaded_to = cf
@@ -303,8 +294,6 @@ class CachedFileField(ExtFileField):
filename=data.name,
type=data.content_type,
)
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file)
cf.save()
data._uploaded_to = cf
+3 -10
View File
@@ -400,10 +400,10 @@ class EventMetaValueForm(forms.ModelForm):
if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_value(self):
def clean_slug(self):
if self.disabled:
return self.instance.value if self.instance else None
return self.cleaned_data['value']
return self.cleaned_data['slug']
class Meta:
model = EventMetaValue
@@ -855,21 +855,14 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
'payment_term_accept_late',
'payment_pending_hidden',
'payment_explanation',
'payment_choice_postpone_allowed_channels',
'tax_rule_payment',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
channels = list(self.obj.organizer.sales_channels.all())
self.fields['payment_choice_postpone_allowed_channels'].choices = [
(c.identifier, c.label) for c in channels
if c.type_instance.payment_restrictions_supported
]
self.term_channel_fields = {}
for c in channels:
for c in self.obj.organizer.sales_channels.all():
if c.type_instance.payment_restrictions_supported and c.identifier != "web":
# At the moment, it seems sufficient to allow this for the same channel types as other payment settings
# We can always introduce more flags later if needed
+3 -3
View File
@@ -105,12 +105,12 @@ class GlobalSettingsForm(SettingsForm):
domain=settings.SITE_URL
)
)),
('widget_vue2_origins', forms.CharField(
('widget_vite_origins', forms.CharField(
widget=forms.Textarea(attrs={'rows': '3'}),
required=False,
# Not translated on purpose, this is a temporary feature and contains too many special case words
label="Vue2 widget origins",
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the old vue2-based widget.",
label="Vite widget origins",
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the new vite-based widget.",
))
])
responses = register_global_settings.send(self)
-2
View File
@@ -87,7 +87,6 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
@@ -135,7 +134,6 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
+2 -2
View File
@@ -435,10 +435,10 @@ class SubEventMetaValueForm(forms.ModelForm):
if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_value(self):
def clean_slug(self):
if self.disabled:
return self.instance.value if self.instance else None
return self.cleaned_data['value']
return self.cleaned_data['slug']
class Meta:
model = SubEventMetaValue
+4
View File
@@ -717,6 +717,10 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
'pretix.property.created': _('An organizer meta property has been created.'),
'pretix.property.deleted': _('An organizer meta property has been deleted.'),
'pretix.property.changed': _('An organizer meta property has been changed.'),
'pretix.property.reordered': _('An organizer meta property has been reordered.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
@@ -109,7 +109,6 @@
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.tax_rule_payment layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %}
{% bootstrap_field form.payment_choice_postpone_allowed_channels layout="control" %}
</fieldset>
</div>
{% if "event.settings.payment:write" in request.eventpermset %}
+15 -53
View File
@@ -35,7 +35,6 @@
import base64
import json
import logging
import math
import time
from urllib.parse import quote, urljoin, urlparse
@@ -51,12 +50,11 @@ from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView
from django_otp import devices_for_user
from django_otp import match_token
from django_otp.plugins.otp_static.models import StaticDevice
from webauthn.helpers import generate_challenge
@@ -66,7 +64,6 @@ from pretix.base.forms.auth import (
)
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.helpers import OF_SELF
from pretix.helpers.http import get_client_ip, redirect_to_url
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
from pretix.helpers.security import handle_login_source, session_login
@@ -398,17 +395,15 @@ class Recover(TemplateView):
def post(self, request, *args, **kwargs):
if self.form.is_valid():
with transaction.atomic():
# Check token in transaction to prevent race condition
try:
user = User.objects.select_for_update(of=OF_SELF).get(id=self.request.GET.get('id'), auth_backend='native')
except User.DoesNotExist:
return self.invalid('unknownuser')
if not default_token_generator.check_token(user, self.request.GET.get('token')):
return self.invalid('invalid')
user.set_password(self.form.cleaned_data['password'])
user.needs_password_change = False
user.save()
try:
user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native')
except User.DoesNotExist:
return self.invalid('unknownuser')
if not default_token_generator.check_token(user, self.request.GET.get('token')):
return self.invalid('invalid')
user.set_password(self.form.cleaned_data['password'])
user.needs_password_change = False
user.save()
messages.success(request, _('You can now login using your new password.'))
user.log_action('pretix.control.auth.user.forgot_password.recovered')
@@ -465,7 +460,6 @@ class Login2FAView(TemplateView):
token = request.POST.get('token', '').strip().replace(' ', '')
valid = False
retry_after = None
if 'webauthn_challenge' in self.request.session and token.startswith('{'):
challenge = self.request.session['webauthn_challenge']
@@ -521,28 +515,12 @@ class Login2FAView(TemplateView):
valid = True
break
else:
with transaction.atomic():
for device in devices_for_user(self.user, for_verify=True):
if isinstance(device, StaticDevice) and len(token) < 12:
# If we enter a wrong TOTP token (which is 6 characters), do not even try if it is a valid
# emergency token, which will only "lock up" the StaticDevice due to the throttling plugin
# and just locks people out without security gain.
continue
if device.verify_token(token):
valid = True
break
elif hasattr(device, 'verify_is_allowed'):
verify_allowed, reason_dict = device.verify_is_allowed()
if not verify_allowed:
if not retry_after or reason_dict['locked_until'] > retry_after:
retry_after = reason_dict['locked_until']
else:
device = None
if isinstance(device, StaticDevice):
valid = match_token(self.user, token)
if isinstance(valid, StaticDevice):
self.user.send_security_notice([
_("A recovery code for two-factor authentification was used to log in.")
])
if valid:
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")
pretix_successful_logins.inc(1)
@@ -555,23 +533,7 @@ class Login2FAView(TemplateView):
return redirect('control:index')
else:
pretix_failed_logins.inc(1, reason="2fa")
msg = _('Invalid code, please try again.')
if retry_after:
seconds = (retry_after - now()).total_seconds()
minutes = seconds / 60
if minutes >= 1:
msg = ngettext(
'Invalid code. Please try again after waiting {value} minute.',
'Invalid code. Please try again after waiting {value} minutes.',
minutes,
).format(value=math.ceil(minutes))
elif seconds >= 1:
msg = ngettext(
'Invalid code. Please try again after waiting {value} second.',
'Invalid code. Please try again after waiting {value} seconds.',
seconds,
).format(value=math.ceil(seconds))
messages.error(request, msg)
messages.error(request, _('Invalid code, please try again.'))
return redirect('control:auth.login.2fa')
def get_context_data(self, **kwargs):
-3
View File
@@ -163,9 +163,6 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
i = modelcopy(self.copy_from)
i.pk = None
kwargs['instance'] = i
kwargs["initial"]["limit_sales_channels"] = self.copy_from.limit_sales_channels.all()
kwargs["initial"]["condition_limit_products"] = self.copy_from.condition_limit_products.all()
kwargs["initial"]["benefit_limit_products"] = self.copy_from.benefit_limit_products.all()
else:
kwargs['instance'] = Discount(event=self.request.event)
+4 -20
View File
@@ -1177,8 +1177,6 @@ class OrderRefundView(OrderView):
manual_value = formats.sanitize_separators(manual_value)
try:
manual_value = Decimal(manual_value)
if manual_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
@@ -1208,8 +1206,6 @@ class OrderRefundView(OrderView):
giftcard_value = formats.sanitize_separators(giftcard_value)
try:
giftcard_value = Decimal(giftcard_value)
if giftcard_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
@@ -1259,8 +1255,6 @@ class OrderRefundView(OrderView):
offsetting_value = formats.sanitize_separators(offsetting_value)
try:
offsetting_value = Decimal(offsetting_value)
if offsetting_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
@@ -1277,9 +1271,6 @@ class OrderRefundView(OrderView):
if offset_order.event.currency != self.request.event.currency:
messages.error(self.request, _('You entered an order in an event with a different currency.'))
is_valid = False
if not self.request.user.has_event_permission(self.request.organizer, offset_order.event, 'event.orders:write', request=self.request):
messages.error(self.request, _('You entered an order in an event that you do not have access to.'))
is_valid = False
refunds.append(OrderRefund(
order=order,
payment=None,
@@ -1295,13 +1286,10 @@ class OrderRefundView(OrderView):
))
for identifier, prov in self.request.event.get_payment_providers().items():
# prof = process form, not a typo for prov(ider)
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
prof_value = formats.sanitize_separators(prof_value)
try:
prof_value = Decimal(prof_value)
if prof_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
@@ -1325,8 +1313,6 @@ class OrderRefundView(OrderView):
value = formats.sanitize_separators(value)
try:
value = Decimal(value)
if value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.'))
is_valid = False
@@ -1356,12 +1342,7 @@ class OrderRefundView(OrderView):
))
any_success = False
if refund_selected != full_refund:
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
'amount.'))
is_valid = False
if is_valid:
if refund_selected == full_refund and is_valid:
for r in refunds:
r.save()
order.log_action('pretix.event.order.refund.created', {
@@ -1433,6 +1414,9 @@ class OrderRefundView(OrderView):
)
}))
return redirect(self.get_order_url())
else:
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
'amount.'))
def post(self, *args, **kwargs):
if self.start_form.is_valid():
+3 -3
View File
@@ -778,9 +778,9 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# Assumption: Who has access to modify organizer settings may see all events and disable/enable plugins
# for them. Otherwise, inconsistent situations occur.
kwargs["events"] = self.request.organizer.events.all()
kwargs["events"] = self.request.user.get_events_with_permission(
"event.settings.general:write", request=self.request
).filter(organizer=self.request.organizer)
kwargs["initial"] = {
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
}
-5
View File
@@ -27,7 +27,6 @@ from decimal import Decimal
from io import BytesIO
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
@@ -194,7 +193,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf'
c.save()
c.file.save('empty.pdf', ContentFile(buffer.read()))
@@ -220,7 +218,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf'
c.file = fileobj
c.save()
@@ -306,7 +303,5 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
if not cf.allowed_for_session(request, "ticketoutput-pdf-background"):
raise PermissionDenied()
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
return resp
-5
View File
@@ -1276,11 +1276,6 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
self._default_meta = self.request.event.meta_data
for p in self.request.organizer.meta_properties.all():
if p.protected and not self.request.user.has_organizer_permission(
self.request.organizer, 'organizer.settings.general:write', request=self.request
):
continue
inst = SubEventMetaValue(property=p)
if len(matches[p.id]) == 1 and matches[p.id][0]['c'] == total:
inst.value = matches[p.id][0]['value']
+2 -4
View File
@@ -144,7 +144,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
headers = [
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
_('Comment'), _('Budget'), _('Budget used')
_('Comment')
]
writer.writerow(headers)
@@ -170,9 +170,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
str(v.redeemed),
str(v.max_usages),
str(v.seat) if v.seat else "",
str(v.comment) if v.comment else "",
str(v.budget) if v.budget is not None else "",
str(v.budget_used) if v.budget is not None else "",
str(v.comment) if v.comment else ""
]
writer.writerow(row)
+1 -1
View File
@@ -29,7 +29,7 @@ from django.urls import reverse
def build_absolute_uri(urlname, args=None, kwargs=None):
warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with mainreverse_absolute.',
'Replace this usage with ',
DeprecationWarning
)
return mainreverse_absolute(urlname, args, kwargs)
+23 -30
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-20 14:00+0000\n"
"Last-Translator: Rita Gimenez <barcelonamusictech@gmail.com>\n"
"PO-Revision-Date: 2026-06-25 14:00+0000\n"
"Last-Translator: Kim Lozano <joaquim.lozano@upc.edu>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix/"
"ca/>\n"
"Language: ca\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 2026.9.1\n"
"X-Generator: Weblate 2026.6.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -37,11 +37,11 @@ msgstr "Àrab"
#: pretix/_base_settings.py
msgid "Basque"
msgstr "Basc"
msgstr ""
#: pretix/_base_settings.py
msgid "Catalan"
msgstr "Català"
msgstr ""
#: pretix/_base_settings.py
msgid "Chinese (simplified)"
@@ -49,11 +49,11 @@ msgstr "Xinès (simplificat)"
#: pretix/_base_settings.py
msgid "Chinese (traditional)"
msgstr "Xinès (tradicional)"
msgstr ""
#: pretix/_base_settings.py
msgid "Czech"
msgstr "Txec"
msgstr ""
#: pretix/_base_settings.py
msgid "Croatian"
@@ -81,7 +81,7 @@ msgstr "Finlandès"
#: pretix/_base_settings.py
msgid "Galician"
msgstr "Gallec"
msgstr ""
#: pretix/_base_settings.py
msgid "Greek"
@@ -89,15 +89,15 @@ msgstr "Grec"
#: pretix/_base_settings.py
msgid "Hebrew"
msgstr "Hebreu"
msgstr ""
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr "Hongarès"
msgstr ""
#: pretix/_base_settings.py
msgid "Indonesian"
msgstr "Indonesi"
msgstr ""
#: pretix/_base_settings.py
msgid "Italian"
@@ -105,7 +105,7 @@ msgstr "Italià"
#: pretix/_base_settings.py
msgid "Japanese"
msgstr "Japonès"
msgstr ""
#: pretix/_base_settings.py
msgid "Latvian"
@@ -113,7 +113,7 @@ msgstr "Letó"
#: pretix/_base_settings.py
msgid "Norwegian Bokmål"
msgstr "Noruec"
msgstr ""
#: pretix/_base_settings.py
msgid "Polish"
@@ -129,7 +129,7 @@ msgstr "Portuguès (Brasil)"
#: pretix/_base_settings.py
msgid "Romanian"
msgstr "Romanès"
msgstr ""
#: pretix/_base_settings.py
msgid "Russian"
@@ -137,11 +137,11 @@ msgstr "Rus"
#: pretix/_base_settings.py
msgid "Slovak"
msgstr "Eslovac"
msgstr ""
#: pretix/_base_settings.py
msgid "Swedish"
msgstr "Suec"
msgstr ""
#: pretix/_base_settings.py
msgid "Spanish"
@@ -149,11 +149,11 @@ msgstr "Espanyol"
#: pretix/_base_settings.py
msgid "Spanish (Latin America)"
msgstr "Espanyol (Llatinoamèrica)"
msgstr ""
#: pretix/_base_settings.py
msgid "Thai"
msgstr "Tailandès"
msgstr ""
#: pretix/_base_settings.py
msgid "Turkish"
@@ -161,15 +161,13 @@ msgstr "Turc"
#: pretix/_base_settings.py
msgid "Ukrainian"
msgstr "Ucraïnès"
msgstr ""
#: pretix/api/auth/devicesecurity.py
msgid ""
"Full device access (reading and changing orders and gift cards, reading of "
"products and settings)"
msgstr ""
"Accés total al dispositiu (lectura i modificació de comandes i targetes "
"regal, lectura de productes i configuració)"
#: pretix/api/auth/devicesecurity.py
msgid "pretixSCAN"
@@ -311,7 +309,7 @@ msgstr "La cistella és buida."
#: pretix/api/serializers/item.py
msgid "The program end must not be empty."
msgstr "El final del programa no ha de ser buit."
msgstr ""
#: pretix/api/serializers/item.py pretix/base/models/items.py
#, fuzzy
@@ -341,7 +339,7 @@ msgstr ""
#: pretix/api/serializers/item.py
msgid "Only admission products can currently be personalized."
msgstr "Actualment, només es poden personalitzar els productes d'accés."
msgstr ""
#: pretix/api/serializers/item.py
msgid ""
@@ -436,7 +434,7 @@ msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
#, python-format
msgid "You've been invited to join %(organizer)s"
msgstr "Has estat convidat/da a unir-te a %(organizer)s"
msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
msgid "This user already has been invited for this team."
@@ -458,7 +456,7 @@ msgstr ""
#: pretix/api/views/checkin.py
msgid "Medium connected to other event"
msgstr "Mitjà connectat a un altre esdeveniment"
msgstr ""
#: pretix/api/views/checkin.py
#, fuzzy
@@ -644,8 +642,6 @@ msgid ""
"This includes product added or deleted and changes to nested objects like "
"variations or bundles."
msgstr ""
"Això inclou productes afegits o eliminats i canvis en objectes imbricats, "
"com ara variacions o lots."
#: pretix/api/webhooks.py
#, fuzzy
@@ -658,9 +654,6 @@ msgid ""
"This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability."
msgstr ""
"Això inclou esdeveniments relacionats, com ara la creació, l'eliminació, "
"l'obertura o el tancament de quotes. No s'envia cap webhook per als canvis "
"en la disponibilitat resultant."
#: pretix/api/webhooks.py
#, fuzzy
+5 -5
View File
@@ -5,7 +5,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-14 23:00+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
@@ -14,7 +14,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 2026.9.1\n"
"X-Generator: Weblate 2026.8.1\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py
@@ -11708,7 +11708,7 @@ msgid ""
msgstr ""
"Hallo,\n"
"\n"
"Sie erhalten diese Nachricht, weil Sie einen neuen Link zu Ihrer Bestellung "
"Sie erhalten diese Nachricht weil Sie einen neuen Link zu Ihrer Bestellung "
"für\n"
"{event} angefordert haben.\n"
"\n"
@@ -11738,8 +11738,8 @@ msgid ""
msgstr ""
"Hallo,\n"
"\n"
"Sie erhalten diese Nachricht, weil Sie einen neuen Link zu Ihren "
"Bestellungen für\n"
"Sie erhalten diese Nachricht weil Sie einen neuen Link zu Ihren Bestellungen "
"für\n"
"{event} angefordert haben. Sie finden Ihre Bestellungen unter folgenden "
"Links:\n"
"\n"
+7 -7
View File
@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
"PO-Revision-Date: 2026-08-24 14:30+0000\n"
"Last-Translator: Albizuri <oier@puntu.eus>\n"
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/"
"eu/>\n"
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/eu/"
">\n"
"Language: eu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.9\n"
"X-Generator: Weblate 2026.8.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -891,7 +891,7 @@ msgstr "Produktuaren data"
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "Order details"
msgstr "Eskaeraren xehetasunak"
msgstr ""
#: pretix/base/datasync/sourcefields.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py
@@ -17296,7 +17296,7 @@ msgstr ""
#: pretix/control/logdisplay.py
msgid "The order details have been changed."
msgstr "Eskaeraren xehetasunak aldatu dira."
msgstr ""
#: pretix/control/logdisplay.py
msgid "The order has been marked as unpaid."
@@ -23106,7 +23106,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
#, python-format
msgid "Order details: %(code)s"
msgstr "Eskaeraren xehetasunak: %(code)s"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/index.html
+70 -51
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-07 18:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"PO-Revision-Date: 2026-08-16 22:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
"Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.9\n"
"X-Generator: Weblate 2026.8.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -93,7 +93,7 @@ msgstr "ヘブライ語"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr "ハンガリー語"
msgstr ""
#: pretix/_base_settings.py
msgid "Indonesian"
@@ -220,7 +220,7 @@ msgstr "ターゲットURL"
#: pretix/api/models.py pretix/base/models/devices.py
#: pretix/base/models/organizer.py
msgid "All events (including newly created ones)"
msgstr "すべてのイベント(今後作成されるものを含む)"
msgstr "すべてのイベント(最近作成されたイベントを含む)"
#: pretix/api/models.py pretix/base/models/devices.py
#: pretix/base/models/organizer.py
@@ -428,8 +428,10 @@ msgid "You cannot exchange a medium for a medium."
msgstr "メディアを別のメディアに変更することはできません。"
#: pretix/api/views/checkin.py
#, fuzzy
#| msgid "Product does not support medium exchange."
msgid "You cannot simulate a medium exchange."
msgstr "メディアの交換をシミュレートすることはできません。"
msgstr "本製品は中程度の交換に対応していません。"
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format
@@ -5689,7 +5691,7 @@ msgstr "国コード(ISO 3166-1 alpha-2"
#: pretix/base/models/items.py
msgid "Asked on"
msgstr "質問日"
msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py
msgid ""
@@ -7338,9 +7340,12 @@ msgid "The payment for this invoice has already been received."
msgstr "この請求書の支払いはすでに受領済みです。"
#: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment is already being processed and can not be canceled any more."
msgid ""
"This payment is already being processed and cannot be canceled any more."
msgstr "この支払いはすでに処理中のため、キャンセルできません。"
msgstr "この支払いは処理中のため、キャンセルできません。"
#: pretix/base/payment.py
msgid "Automatic refunds are not supported by this payment provider."
@@ -8100,12 +8105,16 @@ msgid "Presale end"
msgstr "前売り終了"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order email"
msgid "Order creation"
msgstr "注文の作成"
msgstr "注文者メール"
#: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry"
msgstr "注文の失効"
msgstr "注文の有効期限が切れました"
#: pretix/base/reldate.py
msgid "before"
@@ -8134,22 +8143,22 @@ msgstr "未設定"
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr "相対的な日付は、「{}」に対して「before」で表すことはできません"
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr "相対的な日付は「{}」に対して「after」として表すことはできません"
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr "相対時間は「{}」に対して「before」で表すことはできません"
msgstr ""
#: pretix/base/reldate.py
#, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr "相対時間は「{}」に対して「after」で表すことはできません"
msgstr ""
#: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)"
@@ -10058,12 +10067,12 @@ msgstr ""
#: pretix/base/settings.py
msgid "No dates match your criteria."
msgstr "条件に合致する日付はありません。"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
msgid "Text for empty date results"
msgstr "空の日付結果のテキスト"
msgstr ""
#: pretix/base/settings.py
msgctxt "subevents"
@@ -10074,10 +10083,6 @@ msgid ""
"touch with you to arrange further dates. We do not recommend more than one "
"or two sentences."
msgstr ""
"このテキストは、カレンダーまたは日付リストが空の場合に表示されます。たとえば"
"、月に日付が含まれていない場合や、ユーザーが選択したフィルターに結果が見つか"
"らない場合です。これをご利用いただくことで、今後の日程調整のために連絡する方"
"法を宣伝できます。1文または2文以上は推奨いたしません。"
#: pretix/base/settings.py
msgid "Guidance text"
@@ -13897,20 +13902,26 @@ msgid ""
msgstr "ギフトカードの有効期限を発行年を含めた{}年に設定しました。"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term"
msgstr "支払条件"
msgstr "支払い日"
#: pretix/control/forms/event.py
msgid "same as above"
msgstr "上記と同じ"
msgstr ""
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days"
msgstr "日数で表す異なる支払条件"
msgstr "支払条件(日数)"
#: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes"
msgstr "分で表す異なる支払条件"
msgstr "支払い期限(分単位)"
#: pretix/control/forms/event.py
msgid "Prices including tax"
@@ -22522,24 +22533,28 @@ msgstr ""
"食事を提供する場合、ユーザーに食事制限について尋ねることができる一例です。"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new per-ticket question"
msgstr "チケットごとに新しい質問を作成"
msgstr "新しい質問を作成"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new order-level question"
msgstr "注文ごとに新しい質問を作成"
msgstr "新しい質問を作成"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions"
msgstr "チケットごとの質問"
msgstr "質問を削除"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"These questions are asked for every ticket, so possibly multiple times in "
"the same order."
msgstr ""
"これらの質問はすべてのチケットに対して尋ねられるため、同じ順序で複数回になる"
"可能性があります。"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question"
@@ -22558,24 +22573,28 @@ msgid "All personalized products"
msgstr "すべてのパーソナライズされた製品"
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Include questions"
msgid "Per-order questions"
msgstr "注文ごとの質問"
msgstr "質問を含む"
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"This functionality is in active development and expected to change "
"significantly over the coming months."
msgstr "この機能は現在開発が活発で、今後数か月で大幅に変化すると予想されています。"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
msgid ""
"Per-order questions are currently not supported and will not be displayed in "
"pretixPOS."
msgstr "注文ごとの質問は現在サポートされておらず、pretixPOSでは表示されません。"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "The question has been reordered."
msgid "These questions are asked once per order."
msgstr "これらの質問は、注文ごとに1回ずつ尋ねられます。"
msgstr "質問の並び順が変更されました。"
#: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
@@ -23167,8 +23186,10 @@ msgstr "(任意)"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Additional information"
msgid "Additional order information"
msgstr "追加の注文情報"
msgstr "追加情報"
#: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order"
@@ -24835,13 +24856,14 @@ msgid "Hardware model"
msgstr "ハードウェアの機種"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format
#, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s"
msgstr "最後の閲覧: %(time)s"
msgstr "開始: %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact"
msgstr "最近の連絡なし"
msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized"
@@ -28198,14 +28220,19 @@ msgstr ""
"に高いことを意味します。ドメインのDNS設定を更新すべきです。"
#: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We did not find DMARC record for your domain. This means that there is a "
#| "very high chance most of the emails will be rejected or marked as spam. "
#| "You should update the DNS settings of your domain."
msgid ""
"We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain."
msgstr ""
"あなたのドメインのDMARCレコードが見つかりませんでした。これは、ほとんどの"
"メールが拒否されたスパムとしてマークされた可能性が非常に高いことを意味し"
"す。ドメインのDNS設定を更新すべきです。"
"お客様のドメインのDMARCレコードが見つかりませんでした。これは、ほとんどのメー"
"ルが拒否されたスパムとしてマークされたりする可能性が非常に高いことを意味し"
"す。ドメインのDNS設定を更新すべきです。"
#: pretix/control/views/mailsetup.py
msgid "The verification code was incorrect, please try again."
@@ -30879,7 +30906,7 @@ msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold"
msgstr "コンプライアンス保留の間、追加の支払いを許可する"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
@@ -30889,20 +30916,16 @@ msgid ""
"attempts during that window. This might result in them being charged twice "
"if the original payment is approved."
msgstr ""
"PayPalの不正防止は、個々の支払いの処理をかなりの期間ブロックする可能性があり"
"ます。この期間中、支払いは「保留中」とマークされています。顧客がその期間中に"
"別の支払い試行を開始できるように許可することができます。元の支払いが承認され"
"た場合、二重に請求される可能性があります。"
#: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts"
msgstr "追加の支払い試行がタイムアウト"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid ""
"Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation."
msgstr "最後の支払いがまだ調査中である間、別の支払い試行が可能となる時間(分)。"
msgstr ""
#: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --"
@@ -31167,10 +31190,6 @@ msgid ""
"twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case."
msgstr ""
"支払いはPayPalで処理されています。通常より時間がかかります。PayPalが支払いを"
"確認するまでお待ちいただくか、こちらまたは別の支払い方法で再度お支払いをお試"
"しください。PayPalが最初の支払い試行を許可した場合、二重に請求される可能性が"
"あります。この件を解決するために、弊社までご連絡ください。"
#: pretix/plugins/paypal2/views.py
msgid ""
+6 -11
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-02 08:35+0000\n"
"PO-Revision-Date: 2026-03-30 21:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\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 2026.8.1\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/_base_settings.py
msgid "English"
@@ -93,7 +93,7 @@ msgstr "Hebraico"
#: pretix/_base_settings.py
msgid "Hungarian"
msgstr "Húngaro"
msgstr ""
#: pretix/_base_settings.py
msgid "Indonesian"
@@ -153,7 +153,7 @@ msgstr "Espanhol (América latina)"
#: pretix/_base_settings.py
msgid "Thai"
msgstr "Tailandês"
msgstr ""
#: pretix/_base_settings.py
msgid "Turkish"
@@ -3022,12 +3022,11 @@ msgid ""
"The field \"%(label)s\" may not contain special characters such as "
"\"%(chars)s\"."
msgstr ""
"O campo \"%(label)s\" não pode conter caracteres especiais como %(chars)s\"."
#: pretix/base/forms/questions.py
#, python-format
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
msgstr "O campo \"%(label)s\" não pode conter uma URL (%(url)s)."
msgstr ""
#: pretix/base/forms/questions.py
msgctxt "phonenumber"
@@ -8174,7 +8173,7 @@ msgstr "Permitida"
#: pretix/base/permissions.py
msgctxt "permission_level"
msgid "Access existing events"
msgstr "Acessar eventos existentes"
msgstr ""
#: pretix/base/permissions.py
msgctxt "permission_level"
@@ -13608,15 +13607,11 @@ msgstr "Por favor, continue em uma nova aba"
#: pretix/base/templates/pretixbase/framebreak.html
msgid "For security reasons, the following step is only possible in a new tab."
msgstr ""
"Por razões de segurança, o passo seguinte só é possível de ser feito em uma "
"nova aba."
#: pretix/base/templates/pretixbase/framebreak.html
msgid ""
"If the new tab did not open automatically, please click the following button:"
msgstr ""
"Se uma nova aba não abrir automaticamente, por favor, clique no botão a "
"seguir:"
#: pretix/base/templates/pretixbase/framebreak.html
#: pretix/presale/templates/pretixpresale/event/cookies.html
+4 -5
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
"PO-Revision-Date: 2026-08-09 06:00+0000\n"
"Last-Translator: Julien <julien@circusiloveyou.com>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/"
"sv/>\n"
"Language: sv\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 2026.9\n"
"X-Generator: Weblate 2026.8.1\n"
#: pretix/_base_settings.py
msgid "English"
@@ -19170,9 +19170,8 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_pay.html
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html
#: pretix/presale/templates/pretixpresale/event/position_change.html
#, fuzzy
msgid "Continue"
msgstr "Fortsätt"
msgstr "Fortsätta"
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
msgid "Authorize an application"
+4 -5
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
"PO-Revision-Date: 2026-08-09 06:00+0000\n"
"Last-Translator: Julien <julien@circusiloveyou.com>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/sv/>\n"
"Language: sv\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 2026.9\n"
"X-Generator: Weblate 2026.8.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -133,9 +133,8 @@ msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
msgid "Continue"
msgstr "Fortsätt"
msgstr "Fortsätta"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
+100 -129
View File
@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-19 13:00+0000\n"
"Last-Translator: Nate Horst <nate@agcthailand.org>\n"
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/>"
"\n"
"PO-Revision-Date: 2026-05-20 10:58+0000\n"
"Last-Translator: Phumraphee Sae-tang <phumraphee@gmail.com>\n"
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/"
">\n"
"Language: th\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.9.1\n"
"X-Generator: Weblate 2026.5\n"
#: pretix/_base_settings.py
msgid "English"
@@ -17264,7 +17264,7 @@ msgstr ""
#: pretix/control/navigation.py
#: pretix/control/templates/pretixcontrol/dashboard.html
msgid "Dashboard"
msgstr "แผงควบคุม"
msgstr ""
#: pretix/control/navigation.py
#: pretix/control/templates/pretixcontrol/checkin/list_edit.html
@@ -17278,7 +17278,7 @@ msgstr "แผงควบคุม"
#: pretix/control/templates/pretixcontrol/organizers/mail.html
#: pretix/control/templates/pretixcontrol/organizers/property_edit.html
msgid "General"
msgstr "ทั่วไป"
msgstr ""
#: pretix/control/navigation.py
#: pretix/control/templates/pretixcontrol/event/quick_setup.html
@@ -17290,7 +17290,7 @@ msgstr "ทั่วไป"
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "Payment"
msgstr "การชำระเงิน"
msgstr ""
#: pretix/control/navigation.py pretix/control/views/event.py
#: pretix/control/views/subevents.py
@@ -17300,18 +17300,18 @@ msgstr "การชำระเงิน"
#: pretix/presale/templates/pretixpresale/event/fragment_subevent_list.html
#: pretix/presale/templates/pretixpresale/organizers/index.html
msgid "Tickets"
msgstr "ตั๋ว"
msgstr ""
#: pretix/control/navigation.py
#: pretix/control/templates/pretixcontrol/event/tax.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
msgid "Taxes"
msgstr "ภาษี"
msgstr ""
#: pretix/control/navigation.py
msgid "Invoicing"
msgstr "การออกใบแจ้งหนี้"
msgstr ""
#: pretix/control/navigation.py
msgctxt "action"
@@ -21506,19 +21506,19 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/oauth/authorized.html
msgid "Permissions"
msgstr "สิทธิ์การใช้งาน"
msgstr ""
#: pretix/control/templates/pretixcontrol/oauth/authorized.html
msgid "No applications have access to your pretix account."
msgstr "ไม่มีแอปพลิเคชันใดเข้าถึงบัญชี pretix ของคุณ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/approve.html
msgid "Approve order"
msgstr "อนุมัติคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/approve.html
msgid "Do you really want to approve this order?"
msgstr "คุณแน่ใจหรือไม่ว่าต้องการอนุมัติคำสั่งซื้อนี้?"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/approve.html
#: pretix/control/templates/pretixcontrol/order/cancel.html
@@ -21529,32 +21529,29 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา
#: pretix/control/templates/pretixcontrol/order/refund_cancel.html
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "No, take me back"
msgstr "ไม่, ย้อนกลับ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/approve.html
msgid "Yes, approve order"
msgstr "ใช่, อนุมัติคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/cancel.html
#: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/order.html
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "Cancel order"
msgstr "ยกเลิกคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/cancel.html
#: pretix/control/templates/pretixcontrol/order/deny.html
msgid "Do you really want to cancel this order? You cannot revert this action."
msgstr ""
"คุณแน่ใจหรือไม่ว่าต้องการยกเลิกคำสั่งซื้อนี้? การดำเนินการนี้ไม่สามารถย้อนกลับได้"
#: pretix/control/templates/pretixcontrol/order/cancel.html
msgid ""
"This will <strong>not</strong> automatically transfer the money back, but "
"you will be offered options to refund the payment afterwards."
msgstr ""
"การดำเนินการนี้จะ <strong>ไม่</strong>คืนเงินโดยอัตโนมัติ แต่คุณจะมีตัวเลือกในการคืนเงินหลังจา"
"กนี้"
#: pretix/control/templates/pretixcontrol/order/cancel.html
#, python-format
@@ -21563,17 +21560,15 @@ msgid ""
"%(fee)s for this order, but for a cancellation performed by you, you need to "
"set the cancellation fee here:"
msgstr ""
"ค่าธรรมเนียมการยกเลิกที่ตั้งค่าไว้สำหรับการยกเลิกด้วยตนเองจะอยู่ที่ %(fee)s สำหรับคำสั่งซื้อนี้ แต่สำ"
"หรับการยกเลิกที่ดำเนินการโดยคุณ คุณจะต้องกำหนดค่าธรรมเนียมการยกเลิกที่นี่:"
#: pretix/control/templates/pretixcontrol/order/cancel.html
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "Yes, cancel order"
msgstr "ใช่, ยกเลิกคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/cancellation_request_delete.html
msgid "Ignore cancellation request"
msgstr "เพิกเฉยต่อคำขอยกเลิก"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/cancellation_request_delete.html
msgid ""
@@ -21581,8 +21576,6 @@ msgid ""
"informed automatically, but you will have the option to email them "
"individually in the next step."
msgstr ""
"คุณแน่ใจหรือไม่ว่าต้องการลบคำขอยกเลิกนี้? ผู้ใช้จะไม่ได้รับการแจ้งเตือนโดยอัตโนมัติ แต่คุณจะมีตัวเลือก"
"ในการส่งอีเมลถึงพวกเขาโดยตรงในขั้นตอนถัดไป"
#: pretix/control/templates/pretixcontrol/order/cancellation_request_delete.html
msgid "Yes, delete request"
@@ -21760,24 +21753,24 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_change_confirm.html
#: pretix/presale/templates/pretixpresale/event/position_change_confirm.html
msgid "Perform changes"
msgstr "ดำเนินการเปลี่ยนแปลง"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_contact.html
#: pretix/control/templates/pretixcontrol/order/change_questions.html
msgid "Change contact information"
msgstr "เปลี่ยนแปลงข้อมูลการติดต่อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_locale.html
msgid "Change locale information"
msgstr "เปลี่ยนแปลงข้อมูลภาษาและภูมิภาค"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_locale.html
msgid "This language will be used whenever emails are sent to the users."
msgstr "ภาษานี้จะถูกใช้เมื่อใดก็ตามที่มีการส่งอีเมลไปยังผู้ใช้"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
msgid "Change order information"
msgstr "เปลี่ยนแปลงข้อมูลคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/control/templates/pretixcontrol/order/index.html
@@ -21785,13 +21778,13 @@ msgstr "เปลี่ยนแปลงข้อมูลคำสั่งซ
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "Invoice information"
msgstr "ข้อมูลใบแจ้งหนี้"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "(optional)"
msgstr "(ระบุหรือไม่ก็ได้)"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/change_questions.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -21804,19 +21797,17 @@ msgstr "ต้องการข้อมูลเพิ่มเติม"
#: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order"
msgstr "ลบคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/delete.html
msgid ""
"Do you really want to delete this order? <strong>You really cannot revert "
"this action and we can't either.</strong>"
msgstr ""
"คุณแน่ใจหรือไม่ว่าต้องการลบคำสั่งซื้อนี้? <strong>คุณจะไม่สามารถย้อนกลับการดำเนินการนี้ได้จริงๆ แล"
"ะเราก็ไม่สามารถย้อนกลับได้เช่นกัน</strong>"
#: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Yes, delete order"
msgstr "ใช่, ลบคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/deny.html
msgid "Deny order"
@@ -22265,13 +22256,12 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_cancel.html
msgid "Cancel refund"
msgstr "ยกเลิกการคืนเงิน"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_cancel.html
msgid ""
"Do you really want to cancel this refund? You cannot revert this action."
msgstr ""
"คุณแน่ใจหรือไม่ว่าต้องการยกเลิกการคืนเงินนี้? การดำเนินการนี้ไม่สามารถย้อนกลับได้"
#: pretix/control/templates/pretixcontrol/order/refund_cancel.html
msgid ""
@@ -22279,22 +22269,19 @@ msgid ""
"will just mark this transfer as aborted in pretix. This will also not "
"reactivate the order, it will just allow you to choose a new refund method."
msgstr ""
"หากอยู่ในระหว่างการโอนเงินคืนแล้ว การดำเนินการนี้จะไม่สามารถระงับการโอนเงินได้ แต่จะเป็นเพียง"
"การทำเครื่องหมายว่ารายการโอนนี้ถูกยกเลิกใน pretix เท่านั้น ทั้งนี้ จะไม่เป็นการเปิดใช้งานคำสั่งซื้อ"
"อีกครั้ง แต่จะเปิดโอกาสให้คุณเลือกวิธีการคืนเงินรูปแบบใหม่ได้"
#: pretix/control/templates/pretixcontrol/order/refund_cancel.html
msgid "Yes, cancel refund"
msgstr "ใช่, ยกเลิกการคืนเงิน"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
#: pretix/control/templates/pretixcontrol/order/refund_start.html
msgid "Refund order"
msgstr "คืนเงินคำสั่งซื้อ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "How should the refund be sent?"
msgstr "ต้องการส่งเงินคืนด้วยวิธีใด?"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid ""
@@ -22303,41 +22290,38 @@ msgid ""
"created as pending refunds, which you can later mark as done once you have "
"actually transferred the money back to the customer."
msgstr ""
"รายการชำระเงินใดที่คุณเลือกเป็นการคืนเงินอัตโนมัติ คำขอคืนเงินจะถูกส่งไปยังผู้ให้บริการชำระเงินที่เกี่"
"ยวข้องทันที ส่วนการคืนเงินด้วยตนเองจะถูกสร้างเป็นรายการคืนเงินที่รอดำเนินการ ซึ่งคุณสามารถทำเครื่"
"องหมายว่าเสร็จสิ้นได้ในภายหลัง หลังจากที่ได้โอนเงินคืนให้ลูกค้าเรียบร้อยแล้ว"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "Refund to original payment method"
msgstr "คืนเงินไปยังช่องทางชำระเงินเดิม"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "Amount not refunded"
msgstr "จำนวนเงินที่ไม่ได้รับการคืน"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "Refund amount"
msgstr "จำนวนเงินที่คืน"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "Full amount"
msgstr "จำนวนเงินเต็ม"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "This payment method does not support automatic refunds."
msgstr "วิธีการชำระเงินนี้ไม่รองรับการคืนเงินอัตโนมัติ"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "Refund to a different payment method"
msgstr "คืนเงินไปยังช่องทางชำระเงินอื่น"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "Recipient / options"
msgstr "ผู้รับ / ตัวเลือก"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
msgid "Transfer to other order"
msgstr "โอนไปยังคำสั่งซื้ออื่น"
msgstr ""
#: pretix/control/templates/pretixcontrol/order/refund_choose.html
#: pretix/control/templates/pretixcontrol/organizers/giftcard_create.html
@@ -31188,48 +31172,48 @@ msgstr ""
#: pretix/presale/forms/renderers.py
msgctxt "form"
msgid "is valid"
msgstr "ใช้ได้"
msgstr ""
#: pretix/presale/forms/renderers.py
msgctxt "form"
msgid "has errors"
msgstr "มีข้อผิดพลาด"
msgstr ""
#: pretix/presale/forms/renderers.py
#: pretix/presale/templates/pretixpresale/event/fragment_voucher_form.html
msgctxt "form"
msgid "required"
msgstr "จำเป็นต้องระบุ"
msgstr ""
#: pretix/presale/ical.py
#, python-brace-format
msgid "Tickets: {url}"
msgstr "ตั๋ว: {url}"
msgstr ""
#: pretix/presale/ical.py
#, python-brace-format
msgid "Admission: {datetime}"
msgstr "เวลาประตูเปิด: {datetime}"
msgstr ""
#: pretix/presale/ical.py
#, python-brace-format
msgid "Organizer: {organizer}"
msgstr "ผู้จัดงาน: {organizer}"
msgstr ""
#: pretix/presale/ical.py
#, python-brace-format
msgid "{event} - {item}"
msgstr "{event} - {item}"
msgstr ""
#: pretix/presale/ical.py
#, python-brace-format
msgid "Start: {datetime}"
msgstr "เริ่มต้น: {datetime}"
msgstr ""
#: pretix/presale/ical.py
#, python-brace-format
msgid "End: {datetime}"
msgstr "สิ้นสุด: {datetime}"
msgstr ""
#: pretix/presale/templates/pretixpresale/base.html
msgctxt "skip-to-main-nav"
@@ -31371,23 +31355,23 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/organizers/customer_address_delete.html
#: pretix/presale/templates/pretixpresale/organizers/customer_profile_delete.html
msgid "Go back"
msgstr "ย้อนกลับ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_base.html
#, python-format
msgid "Step %(current)s of %(total)s: %(label)s"
msgstr "ขั้นตอนที่ %(current)s จาก %(total)s: %(label)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_base.html
msgid "Checkout"
msgstr "ชำระเงิน"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_base.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html
msgid "Your cart"
msgstr "ตระกร้าของคุณ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_base.html
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
@@ -31397,36 +31381,36 @@ msgstr "ตะกร้าสินค้าหมดเวลาแล้ว"
#: pretix/presale/templates/pretixpresale/event/checkout_base.html
msgid "Show full cart"
msgstr "แสดงตะกร้าสินค้าทั้งหมด"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_base.html
#: pretix/presale/templates/pretixpresale/event/index.html
msgid "Add tickets for a different date"
msgstr "เพิ่มตั๋วสำหรับวันที่อื่น"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
msgid "Review order"
msgstr "ตรวจสอบคำสั่งซื้อ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
msgid "Please review the details below and confirm your order."
msgstr "โปรดตรวจสอบรายละเอียดด้านล่างและยืนยันคำสั่งซื้อของคุณ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
msgid "Add or remove tickets"
msgstr "เพิ่มหรือลบตั๋ว"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
msgid "Please hang tight, we're finalizing your order!"
msgstr "โปรดรอสักครู่ ระบบกำลังดำเนินการสรุปคำสั่งซื้อของคุณ!"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
msgid "Modify payment"
msgstr "แก้ไขการชำระเงิน"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
msgid "Modify"
msgstr "แก้ไข"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
msgid "Modify invoice information"
@@ -31917,7 +31901,8 @@ msgstr ""
#, python-format
msgid "One product"
msgid_plural "%(num)s products"
msgstr[0] "%(num)s รายการสินค้า"
msgstr[0] ""
msgstr[1] ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html
#, python-format
@@ -32457,35 +32442,31 @@ msgid ""
"your order later. We also sent you an email to the address you specified "
"containing the link to this page."
msgstr ""
"โปรดบุ๊กมาร์กหรือบันทึกลิงก์ของหน้านี้ไว้ หากคุณต้องการเข้าถึงคำสั่งซื้อของคุณในภายหลัง นอกจากนี้ เรา"
"ได้ส่งอีเมลพร้อมลิงก์สำหรับเข้าถึงหน้านี้ไปยังที่อยู่อีเมลที่คุณระบุไว้แล้ว"
#: pretix/presale/templates/pretixpresale/event/order.html
msgid ""
"Please save the following link if you want to access your order later. We "
"also sent you an email to the address you specified containing the link."
msgstr ""
"โปรดบันทึกลิงก์ต่อไปนี้ไว้ หากคุณต้องการเข้าถึงคำสั่งซื้อของคุณในภายหลัง นอกจากนี้ เราได้ส่งอีเมลพร้อ"
"มลิงก์ไปยังที่อยู่อีเมลที่คุณระบุไว้แล้ว"
#: pretix/presale/templates/pretixpresale/event/order.html
#: pretix/presale/templates/pretixpresale/event/position.html
msgid "View in backend"
msgstr "ดูในระบบหลังบ้าน"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
#, python-format
msgid "A payment of %(total)s is still pending for this order."
msgstr "คำสั่งซื้อนี้ยังมียอดคงค้างชำระอยู่อีก %(total)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
#, python-format
msgid "Please complete your payment before %(date)s"
msgstr "โปรดชำระเงินให้เสร็จสิ้นก่อนวันที่ %(date)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "Re-try payment or choose another payment method"
msgstr "ลองชำระเงินอีกครั้ง หรือเลือกวิธีชำระเงินอื่น"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgid ""
@@ -32529,22 +32510,22 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
#: pretix/presale/templates/pretixpresale/event/position.html
msgid "Change ordered items"
msgstr "เปลี่ยนแปลงรายการสินค้าที่สั่งซื้อ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
#: pretix/presale/templates/pretixpresale/event/position.html
msgid "Change details"
msgstr "เปลี่ยนแปลงรายละเอียด"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgid ""
"You need to select a payment method above before you can request an invoice."
msgstr "คุณต้องเลือกวิธีการชำระเงินด้านบนก่อน จึงจะสามารถขอใบแจ้งหนี้ได้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "Request invoice"
msgstr "ขอใบแจ้งหนี้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "Your information"
@@ -32552,42 +32533,41 @@ msgstr "ข้อมูลของคุณ"
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "Change your information"
msgstr "เปลี่ยนแปลงข้อมูลของคุณ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "Internal Reference"
msgstr "รหัสอ้างอิงภายใน"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgctxt "action"
msgid "Change or cancel your order"
msgstr "เปลี่ยนแปลงหรือยกเลิกคำสั่งซื้อของคุณ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgctxt "action"
msgid "Change your order"
msgstr "เปลี่ยนแปลงคำสั่งซื้อของคุณ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgctxt "action"
msgid "Cancel your order"
msgstr "ยกเลิกคำสั่งซื้อของคุณ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgid ""
"If you want to make changes to the products you bought, you can click on the "
"button to change your order."
msgstr ""
"หากคุณต้องการเปลี่ยนแปลงสินค้าที่ซื้อไว้ คุณสามารถคลิกที่ปุ่มเพื่อเปลี่ยนแปลงคำสั่งซื้อของคุณได้"
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "Change order"
msgstr "เปลี่ยนแปลงคำสั่งซื้อ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "You can request to cancel this order."
msgstr "คุณสามารถยื่นขอยกเลิกคำสั่งซื้อนี้ได้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
@@ -32655,17 +32635,17 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html
msgid "You can cancel this order using the following button."
msgstr "คุณสามารถยกเลิกคำสั่งซื้อนี้ได้โดยใช้ปุ่มต่อไปนี้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
#, python-format
msgid "Request cancellation: %(code)s"
msgstr "ยื่นขอยกเลิก: %(code)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
#, python-format
msgid "Cancel order: %(code)s"
msgstr "ยกเลิกคำสั่งซื้อ: %(code)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid ""
@@ -32673,16 +32653,12 @@ msgid ""
"organizer will then decide on your request. If they approve, your order will "
"be canceled and all tickets will be invalidated."
msgstr ""
"คุณสามารถยื่นขอยกเลิกคำสั่งซื้อของคุณได้ในหน้านี้ โดยผู้จัดงานจะพิจารณาคำขอของคุณ หากคำขอได้รับกา"
"รอนุมัติ คำสั่งซื้อของคุณจะถูกยกเลิก และตั๋วทั้งหมดจะถูกยกเลิกใช้งาน"
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid ""
"If you cancel this order, all tickets will be invalidated and you can no "
"longer use them. You cannot revert this action."
msgstr ""
"หากคุณยกเลิกคำสั่งซื้อนี้ ตั๋วทั้งหมดจะถูกยกเลิกใช้งาน และคุณจะไม่สามารถใช้งานตั๋วได้อีกต่อไป การดำเ"
"นินการนี้ไม่สามารถย้อนกลับได้"
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
#, python-format
@@ -32690,102 +32666,97 @@ msgid ""
"If you want, you can request a refund for the full amount minus a "
"cancellation fee of %(fee)s."
msgstr ""
"หากคุณต้องการ คุณสามารถยื่นขอคืนเงินเต็มจำนวนโดยหักค่าธรรมเนียมการยกเลิก %(fee)s"
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "If you want, you can request a full refund."
msgstr "หากคุณต้องการ คุณสามารถยื่นขอคืนเงินเต็มจำนวนได้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "Enter custom amount"
msgstr "ระบุจำนวนเงินเอง"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "Refund amount:"
msgstr "จำนวนเงินที่คืน:"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
#, python-format
msgid "Your gift card will be valid until %(expiry_date)s."
msgstr "บัตรของขวัญของคุณจะสามารถใช้งานได้จนถึงวันที่ %(expiry_date)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "I want the refund as a gift card for later purchases"
msgstr "ฉันต้องการรับเงินคืนเป็นบัตรของขวัญสำหรับการซื้อในครั้งถัดไป"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "I want the refund to be sent to my original payment method"
msgstr "ฉันต้องการรับเงินคืนผ่านช่องทางชำระเงินเดิม"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "The following payment methods will be used to refund the money to you:"
msgstr "ช่องทางการชำระเงินต่อไปนี้จะถูกใช้เพื่อคืนเงินให้กับคุณ:"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_cancel.html
msgid "Yes, request cancellation"
msgstr "ใช่, ยื่นขอยกเลิก"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_change_confirm.html
msgid "Please confirm the following changes to your order."
msgstr "โปรดยืนยันการเปลี่ยนแปลงคำสั่งซื้อของคุณดังต่อไปนี้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_giftcard.html
#: pretix/presale/templates/pretixpresale/event/position_giftcard.html
#, python-format
msgid "Gift card: %(code)s"
msgstr "บัตรของขวัญ: %(code)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid "Modify order"
msgstr "แก้ไขคำสั่งซื้อ"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#, python-format
msgid "Modify order: %(code)s"
msgstr "แก้ไขคำสั่งซื้อ: %(code)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_modify.html
msgid ""
"Modifying your invoice address will not automatically generate a new "
"invoice. Please contact us if you need a new invoice."
msgstr ""
"การแก้ไขที่อยู่สำหรับออกใบแจ้งหนี้จะไม่สร้างใบแจ้งหนี้ใหม่โดยอัตโนมัติ โปรดติดต่อเราหากคุณต้องการใบ"
"แจ้งหนี้ใหม่"
#: pretix/presale/templates/pretixpresale/event/order_modify.html
#: pretix/presale/templates/pretixpresale/event/position_modify.html
msgid "Save changes"
msgstr "บันทึกการเปลี่ยนแปลง"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html
msgid "Change payment method"
msgstr "เปลี่ยนแปลงวิธีการชำระเงิน"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html
#, python-format
msgid "Choose payment method: %(code)s"
msgstr "เลือกวิธีการชำระเงิน: %(code)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html
msgid ""
"Please note: If you change your payment method, your order total will change "
"by the amount displayed to the right of each method."
msgstr ""
"ข้อควรจำ: หากคุณเปลี่ยนแปลงวิธีการชำระเงิน ยอดรวมคำสั่งซื้อของคุณจะเปลี่ยนแปลงตามจำนวนที่แสดง"
"ด้านขวาของแต่ละวิธี"
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html
msgid "There are no alternative payment providers available for this order."
msgstr "ไม่มีผู้ให้บริการชำระเงินอื่นที่สามารถใช้งานได้สำหรับคำสั่งซื้อนี้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_pay_confirm.html
msgid "Please confirm the following payment details."
msgstr "โปรดยืนยันรายละเอียดการชำระเงินดังต่อไปนี้"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_pay_confirm.html
#, python-format
msgid "Total: %(total)s"
msgstr "ยอดรวม: %(total)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/payment_qr_codes.html
msgid ""
+9 -1
View File
@@ -55,6 +55,7 @@ from django.db.models.functions import Cast, Coalesce
from django.utils.timezone import make_aware
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from pypdf import PageObject, PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from reportlab.lib import pagesizes
from reportlab.lib.units import inch, mm
from reportlab.pdfgen import canvas
@@ -237,8 +238,15 @@ def _render_nup_page(nup_pdf: PdfWriter, input_pages: PageObject, opt: dict) ->
di = i % badges_per_page
tx = opt['margins'][3] + (di % opt['cols']) * opt['offsets'][0]
ty = opt['margins'][2] + (opt['rows'] - 1 - (di // opt['cols'])) * opt['offsets'][1]
page.add_transformation(Transformation().translate(tx, ty))
page.mediabox = RectangleObject((
Decimal('%.5f' % (page.mediabox.left.as_numeric() + tx)),
Decimal('%.5f' % (page.mediabox.bottom.as_numeric() + ty)),
Decimal('%.5f' % (page.mediabox.right.as_numeric() + tx)),
Decimal('%.5f' % (page.mediabox.top.as_numeric() + ty))
))
page.trimbox = page.cropbox = page.mediabox
nup_page.merge_transformed_page(page, Transformation().translate(tx, ty))
nup_page.merge_page(page)
return nup_page
@@ -1,183 +1,183 @@
/* global gettext */
/*global $, gettext*/
var bankimport_transactionlist = {
_btn_click: function (e) {
console.log(e.delegateTarget)
let trans_id = parseInt($(e.delegateTarget).attr('name').split('_')[1])
let value = $(e.delegateTarget).val()
if (value === 'discard') {
bankimport_transactionlist.discard(trans_id)
} else if (value === 'accept') {
bankimport_transactionlist.accept(trans_id)
} else if (value === 'retry') {
bankimport_transactionlist.retry(trans_id)
} else if (value === 'assign') {
bankimport_transactionlist.assign(trans_id)
}
return false
},
_btn_click: function (e) {
console.log(e.delegateTarget);
var trans_id = parseInt($(e.delegateTarget).attr("name").split("_")[1]);
var value = $(e.delegateTarget).val();
if (value === "discard") {
bankimport_transactionlist.discard(trans_id);
} else if (value === "accept") {
bankimport_transactionlist.accept(trans_id);
} else if (value === "retry") {
bankimport_transactionlist.retry(trans_id);
} else if (value === "assign") {
bankimport_transactionlist.assign(trans_id);
}
return false;
},
_action: function (id, action, success) {
$('tr[data-id=' + id + '] button').prop('disabled', true)
let data = {
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
}
data['action_' + id] = action
$.ajax({
method: 'POST',
url: $('.transaction-list').attr('data-url'),
data: data,
dataType: 'json',
success: function (data) {
if (data.status == 'ok') {
$('tr[data-id=' + id + ']').removeClass('has-error')
if (data.comment) {
bankimport_transactionlist.comment_reset_to_text(id, data.comment, data.plain)
}
success()
} else {
$('tr[data-id=' + id + '] button').prop('disabled', false)
$('tr[data-id=' + id + '] .help-block').remove()
$('tr[data-id=' + id + ']').addClass('has-error')
$('<p>').addClass('help-block').text(data.message).appendTo($('tr[data-id=' + id + '] td.actions'))
}
}
})
},
_action: function (id, action, success) {
$("tr[data-id=" + id + "] button").prop("disabled", true);
var data = {
"csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
};
data["action_" + id] = action;
$.ajax({
"method": "POST",
"url": $(".transaction-list").attr("data-url"),
"data": data,
"dataType": "json",
"success": function (data) {
if (data.status == "ok") {
$("tr[data-id=" + id + "]").removeClass("has-error");
if (data.comment) {
bankimport_transactionlist.comment_reset_to_text(id, data.comment, data.plain);
}
success();
} else {
$("tr[data-id=" + id + "] button").prop("disabled", false);
$("tr[data-id=" + id + "] .help-block").remove();
$("tr[data-id=" + id + "]").addClass("has-error");
$("<p>").addClass("help-block").text(data.message).appendTo($("tr[data-id=" + id + "] td.actions"));
}
}
});
},
discard: function (id) {
bankimport_transactionlist._action(id, 'discard', function () {
$('tr[data-id=' + id + '] td').remove()
})
},
discard: function (id) {
bankimport_transactionlist._action(id, "discard", function () {
$("tr[data-id=" + id + "] td").remove();
});
},
retry: function (id) {
bankimport_transactionlist._action(id, 'retry', function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid'))
})
},
retry: function (id) {
bankimport_transactionlist._action(id, "retry", function () {
$("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
});
},
accept: function (id) {
bankimport_transactionlist._action(id, 'accept', function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid'))
})
},
accept: function (id) {
bankimport_transactionlist._action(id, "accept", function () {
$("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
});
},
assign: function (id) {
bankimport_transactionlist._action(id, 'assign:' + $('tr[data-id=' + id + '] input.form-control:not(.tt-hint)').val(), function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid'))
})
},
assign: function (id) {
bankimport_transactionlist._action(id, "assign:" + $("tr[data-id=" + id + "] input.form-control:not(.tt-hint)").val(), function () {
$("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
});
},
comment_reset_to_text: function (id, text, plain) {
let $box = $('tr[data-id=' + id + '] .comment-box')
$box[0].dataset['plain'] = plain
$box.html('')
.append($('<strong>').text(gettext('Comment:')))
.append(' ')
.append($('<span>').addClass('comment').append(' ').append(text))
.append(' ')
.append($('<a>').addClass('comment-modify btn btn-default btn-xs')
.append('<span class=\'fa fa-edit\'></span>'))
},
comment_reset_to_text: function (id, text, plain) {
var $box = $("tr[data-id=" + id + "] .comment-box");
$box[0].dataset["plain"] = plain;
$box.html("")
.append($("<strong>").text(gettext("Comment:")))
.append(" ")
.append($("<span>").addClass("comment").append(" ").append(text))
.append(" ")
.append($("<a>").addClass("comment-modify btn btn-default btn-xs")
.append("<span class='fa fa-edit'></span>"));
},
comment_start_edit: function (e) {
let $box = $(e.target).closest('div')
let id = $box.closest('tr').attr('data-id')
let $inp = $('<textarea>').addClass('form-control')
let orig_rendered = $box.find('.comment')
let orig_text = $box[0].dataset.plain
$inp.val(orig_text)
comment_start_edit: function (e) {
var $box = $(e.target).closest("div");
var id = $box.closest("tr").attr("data-id");
var $inp = $("<textarea>").addClass("form-control");
var orig_rendered = $box.find(".comment");
var orig_text = $box[0].dataset.plain;
$inp.val(orig_text);
let $btngrp = $('<div>')
$btngrp.addClass('btn-group')
let $btn1 = $('<button>')
$btn1.attr('type', 'button').addClass('btn btn-default')
$btn1.append('<span class=\'fa fa-check\'></span>')
$btngrp.append($btn1)
let $btn2 = $('<button>')
$btn2.attr('type', 'button').addClass('btn btn-default')
$btn2.append('<span class=\'fa fa-close\'></span>')
$btngrp.append($btn2)
$box.html('').append($inp).append($btngrp)
$btn1.click(function () {
let text = $box.find('textarea').val()
$box.find('input, textarea, button').prop('disabled', true)
bankimport_transactionlist._action(id, 'comment:' + text, function () {
$('tr[data-id=' + id + '] button').prop('disabled', false)
})
})
$btn2.click(function () {
bankimport_transactionlist.comment_reset_to_text(id, orig_rendered, orig_text)
})
var $btngrp = $("<div>");
$btngrp.addClass("btn-group");
var $btn1 = $("<button>");
$btn1.attr("type", "button").addClass("btn btn-default");
$btn1.append("<span class='fa fa-check'></span>");
$btngrp.append($btn1);
var $btn2 = $("<button>");
$btn2.attr("type", "button").addClass("btn btn-default");
$btn2.append("<span class='fa fa-close'></span>");
$btngrp.append($btn2);
$box.html("").append($inp).append($btngrp);
$btn1.click(function () {
var text = $box.find("textarea").val();
$box.find("input, textarea, button").prop("disabled", true);
bankimport_transactionlist._action(id, "comment:" + text, function () {
$("tr[data-id=" + id + "] button").prop("disabled", false);
});
});
$btn2.click(function () {
bankimport_transactionlist.comment_reset_to_text(id, orig_rendered, orig_text);
});
e.preventDefault()
},
e.preventDefault();
},
typeahead_source: function () {
return new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: $('.transaction-list').attr('data-url'),
prepare: function (query, settings) {
settings.url = settings.url + '?query=' + encodeURIComponent(query)
return settings
},
transform: function (object) {
let results = object.results
let suggs = []
let reslen = results.length
for (let i = 0; i < reslen; i++) {
suggs.push(results[i])
}
return suggs
}
}
})
},
typeahead_source: function () {
return new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: $(".transaction-list").attr("data-url"),
prepare: function (query, settings) {
settings.url = settings.url + '?query=' + encodeURIComponent(query);
return settings;
},
transform: function (object) {
var results = object.results;
var suggs = [];
var reslen = results.length;
for (var i = 0; i < reslen; i++) {
suggs.push(results[i]);
}
return suggs;
}
}
});
},
init: function () {
if ($('.transaction-list').length) {
$('.transaction-list button').click(bankimport_transactionlist._btn_click)
init: function () {
if ($(".transaction-list").length) {
$(".transaction-list button").click(bankimport_transactionlist._btn_click);
$('.transaction-list').on('click', '.comment-modify', bankimport_transactionlist.comment_start_edit)
$(".transaction-list").on("click", ".comment-modify", bankimport_transactionlist.comment_start_edit);
$('.transaction-list .form-control').typeahead(null, {
minLength: 2,
name: 'order-dataset',
source: bankimport_transactionlist.typeahead_source(),
display: function (obj) {
return obj.code
},
templates: {
suggestion: function (obj) {
return '<div>' + obj.code + ' (' + obj.total + ', ' + obj.status + ')</div>'
}
}
}).keypress(function (e) {
if (e.keyCode === 13) {
$(this).parent().parent().find('button[value=assign]').click()
}
})
}
$(".transaction-list .form-control").typeahead(null, {
minLength: 2,
name: 'order-dataset',
source: bankimport_transactionlist.typeahead_source(),
display: function (obj) {
return obj.code;
},
templates: {
suggestion: function (obj) {
return '<div>' + obj.code + ' (' + obj.total + ', ' + obj.status + ')</div>';
}
}
}).keypress(function (e) {
if (e.keyCode === 13) {
$(this).parent().parent().find("button[value=assign]").click();
}
});
}
if ($('[data-job-waiting]').length) {
window.setTimeout(bankimport_transactionlist.check_state, 750)
}
},
if ($("[data-job-waiting]").length) {
window.setTimeout(bankimport_transactionlist.check_state, 750);
}
},
check_state: function () {
$.getJSON($('[data-job-waiting-url]').attr('data-job-waiting-url'), function (data) {
if (data.state == 'running' || data.state == 'pending') {
window.setTimeout(bankimport_transactionlist.check_state, 750)
} else {
location.reload()
}
})
}
}
check_state: function () {
$.getJSON($("[data-job-waiting-url]").attr("data-job-waiting-url"), function (data) {
if (data.state == 'running' || data.state == 'pending') {
window.setTimeout(bankimport_transactionlist.check_state, 750);
} else {
location.reload();
}
});
}
};
$(function () {
bankimport_transactionlist.init()
})
bankimport_transactionlist.init();
});
@@ -1,350 +1,350 @@
/* global paypal_client_id, paypal_loadingmessage, gettext */
'use strict'
/*global $, paypal_client_id, paypal_loadingmessage, gettext */
'use strict';
var pretixpaypal = {
paypal: null,
client_id: null,
order_id: null,
payer_id: null,
merchant_id: null,
currency: null,
method: null,
additional_disabled_funding: null,
additional_enabled_funding: null,
debug_buyer_country: null,
continue_button: null,
paypage: false,
method_map: {
wallet: {
method: 'wallet',
funding_source: 'paypal',
// disable_funding: null,
// enable_funding: 'paylater',
early_auth: true,
},
apm: {
method: 'apm',
funding_source: null,
// disable_funding: null,
// enable_funding: null,
early_auth: false,
}
},
apm_map: {
paypal: gettext('PayPal'),
venmo: gettext('Venmo'),
applepay: gettext('Apple Pay'),
itau: gettext('Itaú'),
credit: gettext('PayPal Credit'),
card: gettext('Credit Card'),
paylater: gettext('PayPal Pay Later'),
ideal: gettext('iDEAL | Wero'),
sepa: gettext('SEPA Direct Debit'),
bancontact: gettext('Bancontact'),
giropay: gettext('giropay'),
sofort: gettext('SOFORT'),
eps: gettext('eps'),
mybank: gettext('MyBank'),
p24: gettext('Przelewy24'),
verkkopankki: gettext('Verkkopankki'),
payu: gettext('PayU'),
blik: gettext('BLIK'),
trustly: gettext('Trustly'),
zimpler: gettext('Zimpler'),
maxima: gettext('Maxima'),
oxxo: gettext('OXXO'),
boleto: gettext('Boleto'),
wechatpay: gettext('WeChat Pay'),
mercadopago: gettext('Mercado Pago')
},
readyToSubmitApproval: false,
paypal: null,
client_id: null,
order_id: null,
payer_id: null,
merchant_id: null,
currency: null,
method: null,
additional_disabled_funding: null,
additional_enabled_funding: null,
debug_buyer_country: null,
continue_button: null,
paypage: false,
method_map: {
wallet: {
method: 'wallet',
funding_source: 'paypal',
//disable_funding: null,
//enable_funding: 'paylater',
early_auth: true,
},
apm: {
method: 'apm',
funding_source: null,
//disable_funding: null,
//enable_funding: null,
early_auth: false,
}
},
apm_map: {
paypal: gettext('PayPal'),
venmo: gettext('Venmo'),
applepay: gettext('Apple Pay'),
itau: gettext('Itaú'),
credit: gettext('PayPal Credit'),
card: gettext('Credit Card'),
paylater: gettext('PayPal Pay Later'),
ideal: gettext('iDEAL | Wero'),
sepa: gettext('SEPA Direct Debit'),
bancontact: gettext('Bancontact'),
giropay: gettext('giropay'),
sofort: gettext('SOFORT'),
eps: gettext('eps'),
mybank: gettext('MyBank'),
p24: gettext('Przelewy24'),
verkkopankki: gettext('Verkkopankki'),
payu: gettext('PayU'),
blik: gettext('BLIK'),
trustly: gettext('Trustly'),
zimpler: gettext('Zimpler'),
maxima: gettext('Maxima'),
oxxo: gettext('OXXO'),
boleto: gettext('Boleto'),
wechatpay: gettext('WeChat Pay'),
mercadopago: gettext('Mercado Pago')
},
readyToSubmitApproval: false,
load: function () {
if (pretixpaypal.paypal === null) {
pretixpaypal.client_id = $.trim($('#paypal_client_id').html())
pretixpaypal.merchant_id = $.trim($('#paypal_merchant_id').html())
pretixpaypal.debug_buyer_country = $.trim($('#paypal_buyer_country').html())
pretixpaypal.continue_button = $('.checkout-button-row').closest('form').find('.checkout-button-row .btn-primary')
pretixpaypal.continue_button.closest('div').append('<div id="paypal-button-container"></div>')
pretixpaypal.additional_disabled_funding = $.trim($('#paypal_disable_funding').html())
pretixpaypal.additional_enabled_funding = $.trim($('#paypal_enable_funding').html())
pretixpaypal.paypage = Boolean($('#paypal-button-container').data('paypage'))
pretixpaypal.order_id = $.trim($('#paypal_oid').html())
pretixpaypal.currency = $('body').attr('data-currency')
pretixpaypal.locale = this.guessLocale()
}
load: function () {
if (pretixpaypal.paypal === null) {
pretixpaypal.client_id = $.trim($("#paypal_client_id").html());
pretixpaypal.merchant_id = $.trim($("#paypal_merchant_id").html());
pretixpaypal.debug_buyer_country = $.trim($("#paypal_buyer_country").html());
pretixpaypal.continue_button = $('.checkout-button-row').closest("form").find(".checkout-button-row .btn-primary");
pretixpaypal.continue_button.closest('div').append('<div id="paypal-button-container"></div>');
pretixpaypal.additional_disabled_funding = $.trim($("#paypal_disable_funding").html());
pretixpaypal.additional_enabled_funding = $.trim($("#paypal_enable_funding").html());
pretixpaypal.paypage = Boolean($('#paypal-button-container').data('paypage'));
pretixpaypal.order_id = $.trim($("#paypal_oid").html());
pretixpaypal.currency = $("body").attr("data-currency");
pretixpaypal.locale = this.guessLocale();
}
$('input[name=payment][value^=\'paypal\']').change(function () {
if (pretixpaypal.paypal !== null) {
pretixpaypal.renderButton($(this).val())
} else {
pretixpaypal.continue_button.prop('disabled', true)
}
})
$("input[name=payment][value^='paypal']").change(function () {
if (pretixpaypal.paypal !== null) {
pretixpaypal.renderButton($(this).val());
} else {
pretixpaypal.continue_button.prop("disabled", true);
}
});
$('input[name=payment]').not('[value^=\'paypal\']').change(function () {
pretixpaypal.restore()
})
$("input[name=payment]").not("[value^='paypal']").change(function () {
pretixpaypal.restore();
});
// If paypal is pre-selected, we must disable the continue button and handle it after SDK is loaded
if ($('input[name=payment][value^=\'paypal\']').is(':checked')) {
pretixpaypal.continue_button.prop('disabled', true)
}
// If paypal is pre-selected, we must disable the continue button and handle it after SDK is loaded
if ($("input[name=payment][value^='paypal']").is(':checked')) {
pretixpaypal.continue_button.prop("disabled", true);
}
// We are setting the cogwheel already here, as the renderAPM() method might take some time to get loaded.
const apmtextselector = $('input[name=payment][value=paypal_apm]').closest('label').find('.accordion-label-text')
apmtextselector.append(' <span aria-hidden="true" class="fa fa-cog fa-spin"></span>')
// We are setting the cogwheel already here, as the renderAPM() method might take some time to get loaded.
const apmtextselector = $("input[name=payment][value=paypal_apm]").closest("label").find(".accordion-label-text");
apmtextselector.append(' <span aria-hidden="true" class="fa fa-cog fa-spin"></span>');
let sdk_url = 'https://www.paypal.com/sdk/js'
+ '?client-id=' + pretixpaypal.client_id
+ '&components=buttons,funding-eligibility'
+ '&currency=' + pretixpaypal.currency
let sdk_url = 'https://www.paypal.com/sdk/js' +
'?client-id=' + pretixpaypal.client_id +
'&components=buttons,funding-eligibility' +
'&currency=' + pretixpaypal.currency;
if (pretixpaypal.locale) {
sdk_url += '&locale=' + pretixpaypal.locale
}
if (pretixpaypal.locale) {
sdk_url += '&locale=' + pretixpaypal.locale;
}
if (pretixpaypal.merchant_id) {
sdk_url += '&merchant-id=' + pretixpaypal.merchant_id
}
if (pretixpaypal.merchant_id) {
sdk_url += '&merchant-id=' + pretixpaypal.merchant_id;
}
if (pretixpaypal.additional_disabled_funding) {
sdk_url += '&disable-funding=' + [pretixpaypal.additional_disabled_funding].filter(Boolean).join(',')
}
if (pretixpaypal.additional_disabled_funding) {
sdk_url += '&disable-funding=' + [pretixpaypal.additional_disabled_funding].filter(Boolean).join(',');
}
if (pretixpaypal.additional_enabled_funding) {
sdk_url += '&enable-funding=' + [pretixpaypal.additional_enabled_funding].filter(Boolean).join(',')
}
if (pretixpaypal.additional_enabled_funding) {
sdk_url += '&enable-funding=' + [pretixpaypal.additional_enabled_funding].filter(Boolean).join(',');
}
if (pretixpaypal.debug_buyer_country) {
sdk_url += '&buyer-country=' + pretixpaypal.debug_buyer_country
}
if (pretixpaypal.debug_buyer_country) {
sdk_url += '&buyer-country=' + pretixpaypal.debug_buyer_country;
}
let ppscript = document.createElement('script')
let ready = false
let head = document.getElementsByTagName('head')[0]
ppscript.setAttribute('src', sdk_url)
ppscript.setAttribute('data-csp-nonce', $.trim($('#csp_nonce').html()))
ppscript.setAttribute('data-page-type', 'checkout')
ppscript.setAttribute('data-partner-attribution-id', 'ramiioGmbH_Cart_PPCP')
document.head.appendChild(ppscript)
let ppscript = document.createElement('script');
let ready = false;
let head = document.getElementsByTagName("head")[0];
ppscript.setAttribute('src', sdk_url);
ppscript.setAttribute('data-csp-nonce', $.trim($("#csp_nonce").html()));
ppscript.setAttribute('data-page-type', 'checkout');
ppscript.setAttribute('data-partner-attribution-id', 'ramiioGmbH_Cart_PPCP');
document.head.appendChild(ppscript);
ppscript.onload = ppscript.onreadystatechange = function () {
if (!ready && (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')) {
ready = true
ppscript.onload = ppscript.onreadystatechange = function () {
if (!ready && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) {
ready = true;
pretixpaypal.paypal = paypal
pretixpaypal.paypal = paypal;
// Handle memory leak in IE
ppscript.onload = ppscript.onreadystatechange = null
if (head && ppscript.parentNode) {
head.removeChild(ppscript)
}
}
}
// Handle memory leak in IE
ppscript.onload = ppscript.onreadystatechange = null;
if (head && ppscript.parentNode) {
head.removeChild(ppscript);
}
}
};
document.addEventListener('visibilitychange', this.onApproveSubmit)
},
document.addEventListener("visibilitychange", this.onApproveSubmit);
},
ready: function () {
if ($('input[name=payment][value=paypal_apm]').length > 0) {
pretixpaypal.renderAPMs()
}
ready: function () {
if ($("input[name=payment][value=paypal_apm]").length > 0) {
pretixpaypal.renderAPMs();
}
if ($('input[name=payment][value^=\'paypal\']').is(':checked')) {
pretixpaypal.renderButton($('input[name=payment][value^=\'paypal\']:checked').val())
} else if ($('.payment-redo-form').length) {
pretixpaypal.renderButton($('input[name=payment][value^=\'paypal\']').val())
} else if ($('#paypal-button-container').data('paypage')) {
pretixpaypal.renderButton('paypal_apm')
}
},
if ($("input[name=payment][value^='paypal']").is(':checked')) {
pretixpaypal.renderButton($("input[name=payment][value^='paypal']:checked").val());
} else if ($(".payment-redo-form").length) {
pretixpaypal.renderButton($("input[name=payment][value^='paypal']").val());
} else if ($('#paypal-button-container').data('paypage')) {
pretixpaypal.renderButton('paypal_apm');
}
},
restore: function () {
// if PayPal has not been initialized, there shouldn't be anything to cleanup
if (pretixpaypal.paypal !== null) {
$('#paypal-button-container').empty()
pretixpaypal.continue_button.text(gettext('Continue'))
pretixpaypal.continue_button.show()
}
pretixpaypal.continue_button.prop('disabled', false)
},
restore: function () {
// if PayPal has not been initialized, there shouldn't be anything to cleanup
if (pretixpaypal.paypal !== null) {
$('#paypal-button-container').empty()
pretixpaypal.continue_button.text(gettext('Continue'));
pretixpaypal.continue_button.show();
}
pretixpaypal.continue_button.prop("disabled", false);
},
renderButton: function (method) {
if (method === 'paypal') {
method = 'wallet'
} else {
method = method.split('paypal_').at(-1)
}
pretixpaypal.method = pretixpaypal.method_map[method]
renderButton: function (method) {
if (method === 'paypal') {
method = "wallet"
} else {
method = method.split('paypal_').at(-1)
}
pretixpaypal.method = pretixpaypal.method_map[method];
if (pretixpaypal.method.method === 'apm' && !pretixpaypal.paypage) {
pretixpaypal.restore()
return
}
if (pretixpaypal.method.method === 'apm' && !pretixpaypal.paypage) {
pretixpaypal.restore();
return;
}
$('#paypal-button-container').empty()
$('#paypal-card-container').empty()
$('#paypal-button-container').empty()
$('#paypal-card-container').empty()
let button = pretixpaypal.paypal.Buttons({
fundingSource: pretixpaypal.method.funding_source,
style: {
layout: pretixpaypal.method.early_auth ? 'horizontal' : 'vertical',
// color: 'white',
shape: 'rect',
label: 'pay',
tagline: false
},
createOrder: function (data, actions) {
if (pretixpaypal.order_id) {
return pretixpaypal.order_id
}
let button = pretixpaypal.paypal.Buttons({
fundingSource: pretixpaypal.method.funding_source,
style: {
layout: pretixpaypal.method.early_auth ? 'horizontal' : 'vertical',
//color: 'white',
shape: 'rect',
label: 'pay',
tagline: false
},
createOrder: function (data, actions) {
if (pretixpaypal.order_id) {
return pretixpaypal.order_id;
}
// On the paypal:pay view, we already pregenerated the OID.
// Since this view is also only used for APMs, we only need the XHR-calls for the Smart Payment Buttons.
if (pretixpaypal.paypage) {
return $('#payment_paypal_' + pretixpaypal.method.method + '_oid')
} else {
var xhrurl = $('#payment_paypal_' + pretixpaypal.method.method + '_xhr').val()
}
// On the paypal:pay view, we already pregenerated the OID.
// Since this view is also only used for APMs, we only need the XHR-calls for the Smart Payment Buttons.
if (pretixpaypal.paypage) {
return $("#payment_paypal_" + pretixpaypal.method.method + "_oid");
} else {
var xhrurl = $("#payment_paypal_" + pretixpaypal.method.method + "_xhr").val();
}
return fetch(xhrurl, {
method: 'POST'
}).then(function (res) {
return res.json()
}).then(function (data) {
if ('id' in data) {
return data.id
} else {
// Refreshing the page to surface the request-error message
location.reload()
}
})
},
onApprove: function (data, actions) {
waitingDialog.show(gettext('Confirming your payment …'))
pretixpaypal.order_id = data.orderID
pretixpaypal.payer_id = data.payerID
return fetch(xhrurl, {
method: 'POST'
}).then(function (res) {
return res.json();
}).then(function (data) {
if ('id' in data) {
return data.id;
} else {
// Refreshing the page to surface the request-error message
location.reload();
}
});
},
onApprove: function (data, actions) {
waitingDialog.show(gettext("Confirming your payment …"));
pretixpaypal.order_id = data.orderID;
pretixpaypal.payer_id = data.payerID;
let method = pretixpaypal.paypage ? 'wallet' : pretixpaypal.method.method
let selectorstub = '#payment_paypal_' + method
// Insert the tokens into the form, so it gets submitted to the server
$(selectorstub + '_oid').val(pretixpaypal.order_id)
$(selectorstub + '_payer').val(pretixpaypal.payer_id)
let method = pretixpaypal.paypage ? "wallet" : pretixpaypal.method.method;
let selectorstub = "#payment_paypal_" + method;
// Insert the tokens into the form, so it gets submitted to the server
$(selectorstub + "_oid").val(pretixpaypal.order_id);
$(selectorstub + "_payer").val(pretixpaypal.payer_id);
// We are moving the submission to a separate function, which is also an EventListener, since
// SFSafariView refuses to submit a form that is not visible. Unfortunately, that is exactly the case
// when the ticket shop is used on iOS within an SFSafariView and the PayPal payment popup has not
// closed itself quickly enough.
pretixpaypal.readyToSubmitApproval = true
pretixpaypal.onApproveSubmit()
// We are moving the submission to a separate function, which is also an EventListener, since
// SFSafariView refuses to submit a form that is not visible. Unfortunately, that is exactly the case
// when the ticket shop is used on iOS within an SFSafariView and the PayPal payment popup has not
// closed itself quickly enough.
pretixpaypal.readyToSubmitApproval = true;
pretixpaypal.onApproveSubmit();
// billingToken: null
// facilitatorAccessToken: "A21AAL_fEu0gDD-sIXyOy65a6MjgSJJrhmxuPcxxUGnL5gW2DzTxiiAksfoC4x8hD-BjeY1LsFVKl7ceuO7UR1a9pQr8Q_AVw"
// orderID: "7RF70259NY7589848"
// payerID: "8M3BU92Z97VXA"
// paymentID: null
},
})
// billingToken: null
// facilitatorAccessToken: "A21AAL_fEu0gDD-sIXyOy65a6MjgSJJrhmxuPcxxUGnL5gW2DzTxiiAksfoC4x8hD-BjeY1LsFVKl7ceuO7UR1a9pQr8Q_AVw"
// orderID: "7RF70259NY7589848"
// payerID: "8M3BU92Z97VXA"
// paymentID: null
},
});
if (button.isEligible()) {
button.render('#paypal-button-container')
pretixpaypal.continue_button.hide()
} else {
pretixpaypal.continue_button.text(gettext('Payment method unavailable'))
pretixpaypal.continue_button.show()
}
},
if (button.isEligible()) {
button.render('#paypal-button-container');
pretixpaypal.continue_button.hide();
} else {
pretixpaypal.continue_button.text(gettext('Payment method unavailable'));
pretixpaypal.continue_button.show();
}
},
onApproveSubmit: function () {
if (document.visibilityState === 'visible' && pretixpaypal.readyToSubmitApproval === true) {
let method = pretixpaypal.paypage ? 'wallet' : pretixpaypal.method.method
let selectorstub = '#payment_paypal_' + method
let $form = $(selectorstub + '_oid').closest('form')
onApproveSubmit: function() {
if (document.visibilityState === "visible" && pretixpaypal.readyToSubmitApproval === true) {
let method = pretixpaypal.paypage ? "wallet" : pretixpaypal.method.method;
let selectorstub = "#payment_paypal_" + method;
var $form = $(selectorstub + "_oid").closest("form");
$form.get(0).submit()
}
},
$form.get(0).submit();
}
},
renderAPMs: function () {
pretixpaypal.restore()
let inputselector = $('input[name=payment][value=paypal_apm]')
let textselector = inputselector.closest('label').find('.accordion-label-text')
let eligibles = []
renderAPMs: function () {
pretixpaypal.restore();
let inputselector = $("input[name=payment][value=paypal_apm]");
let textselector = inputselector.closest("label").find('.accordion-label-text');
let eligibles = [];
pretixpaypal.paypal.getFundingSources().forEach(function (fundingSource) {
// Let's always skip PayPal, since it's always a dedicated funding source
if (fundingSource === 'paypal') {
return
}
pretixpaypal.paypal.getFundingSources().forEach(function (fundingSource) {
// Let's always skip PayPal, since it's always a dedicated funding source
if (fundingSource === 'paypal') {
return;
}
// This could also be paypal.Marks() - but they only expose images instead of cleartext...
let button = pretixpaypal.paypal.Buttons({
fundingSource: fundingSource
})
// This could also be paypal.Marks() - but they only expose images instead of cleartext...
let button = pretixpaypal.paypal.Buttons({
fundingSource: fundingSource
});
if (button.isEligible()) {
eligibles.push(gettext(pretixpaypal.apm_map[fundingSource] || fundingSource))
}
})
if (button.isEligible()) {
eligibles.push(gettext(pretixpaypal.apm_map[fundingSource] || fundingSource));
}
});
inputselector.attr('title', eligibles.join(', '))
textselector.fadeOut(300, function () {
textselector.text(eligibles.join(', '))
textselector.fadeIn(300)
})
},
inputselector.attr('title', eligibles.join(', '));
textselector.fadeOut(300, function () {
textselector.text(eligibles.join(', '));
textselector.fadeIn(300);
});
},
guessLocale: function () {
// This is a horrible hackjob and does not at all take into consideration the actual locale.
// Instead, we only look at the language that the shop is currently being displayed in and make
// that into a locale.
let allowed_locales = [
'en_US',
'ar_DZ',
'fr_FR',
'es_ES',
'zh_CN',
'de_DE',
'nl_NL',
'pt_PT',
'cs_CZ',
'da_DK',
'fi_FI',
'el_GR',
'hu_HU',
'id_ID',
'he_IL',
'it_IT',
'ja_JP',
'ru_RU',
'no_NO',
'pl_PL',
'sk_SK',
'sv_SE',
'th_TH',
'tr_TR',
]
let lang = $('body').attr('data-locale').split('-')[0]
return allowed_locales.find(element => element.startsWith(lang))
}
}
guessLocale: function() {
// This is a horrible hackjob and does not at all take into consideration the actual locale.
// Instead, we only look at the language that the shop is currently being displayed in and make
// that into a locale.
let allowed_locales = [
'en_US',
'ar_DZ',
'fr_FR',
'es_ES',
'zh_CN',
'de_DE',
'nl_NL',
'pt_PT',
'cs_CZ',
'da_DK',
'fi_FI',
'el_GR',
'hu_HU',
'id_ID',
'he_IL',
'it_IT',
'ja_JP',
'ru_RU',
'no_NO',
'pl_PL',
'sk_SK',
'sv_SE',
'th_TH',
'tr_TR',
]
let lang = $("body").attr("data-locale").split('-')[0];
return allowed_locales.find(element => element.startsWith(lang));
}
};
$(function () {
// This script is always loaded if paypal is enabled as a payment method, regardless of
// whether it is available (it could e.g. be hidden or limited to certain countries).
// We do not want to unnecessarily load the sdk.
// If no paypal/paypal_apm payment option is present and we are not on
// the (APM) PayView, then we do not need the SDK.
if (!$('input[name=payment][value^=\'paypal\']').length && !$('#paypal-button-container').data('paypage')) {
return
}
// This script is always loaded if paypal is enabled as a payment method, regardless of
// whether it is available (it could e.g. be hidden or limited to certain countries).
// We do not want to unnecessarily load the sdk.
// If no paypal/paypal_apm payment option is present and we are not on
// the (APM) PayView, then we do not need the SDK.
if (!$("input[name=payment][value^='paypal']").length && !$('#paypal-button-container').data('paypage')) {
return
}
pretixpaypal.load();
pretixpaypal.load();
(async () => {
while (!pretixpaypal.paypal)
await new Promise(resolve => setTimeout(resolve, 1000))
pretixpaypal.ready()
})()
})
(async() => {
while(!pretixpaypal.paypal)
await new Promise(resolve => setTimeout(resolve, 1000));
pretixpaypal.ready();
})();
});
+3 -15
View File
@@ -55,8 +55,6 @@ from django_countries.fields import Country
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.units import mm
from reportlab.lib.utils import simpleSplit
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import PageBreak, Spacer, Table, TableStyle
@@ -228,20 +226,10 @@ class ReportlabExportMixin:
def page_header(self, canvas, doc):
from reportlab.lib.units import mm
font_name = 'OpenSans'
font_size = 10
left_string = self.get_left_header_string()
right_string = self.get_right_header_string()
right_width = stringWidth(right_string, font_name, font_size)
max_left_width = self.pagesize[0] - doc.leftMargin - doc.rightMargin - right_width - 5 * mm
left_string_lines = simpleSplit(left_string, font_name, font_size, max_left_width)
if len(left_string_lines) > 1:
left_string = left_string_lines[0] + ""
canvas.setFont(font_name, font_size)
canvas.drawString(doc.leftMargin, self.pagesize[1] - 15 * mm, left_string)
canvas.setFont('OpenSans', 10)
canvas.drawString(doc.leftMargin, self.pagesize[1] - 15 * mm, self.get_left_header_string())
canvas.drawRightString(self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 15 * mm,
right_string)
self.get_right_header_string())
canvas.setStrokeColorRGB(0, 0, 0)
canvas.line(doc.leftMargin, self.pagesize[1] - 17 * mm,
self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 17 * mm)
+8 -11
View File
@@ -56,11 +56,18 @@ from pretix.base.services.placeholders import FormPlaceholderMixin # noqa
class BaseMailForm(FormPlaceholderMixin, forms.Form):
subject = forms.CharField(label=_("Subject"))
message = forms.CharField(label=_("Message"))
attachment = CachedFileField(
label=_("Attachment"),
required=False,
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
'of no more than 2 MB in size.'),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT
)
def __init__(self, *args, **kwargs):
event = self.event = kwargs.pop('event')
context_parameters = kwargs.pop('context_parameters')
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
self.fields['subject'] = I18nFormField(
label=_('Subject'),
@@ -72,16 +79,6 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
widget=I18nMarkdownTextarea, required=True,
locales=event.settings.get('locales'),
)
self.fields['attachment'] = CachedFileField(
label=_("Attachment"),
required=False,
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
help_text=_(
'Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
'of no more than 2 MB in size.'),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT,
request=request,
)
self._set_field_placeholders('subject', context_parameters, rich=False)
self._set_field_placeholders('message', context_parameters, rich=True)
+18 -26
View File
@@ -157,7 +157,6 @@ class BaseSenderView(EventPermissionRequiredMixin, FormView):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
kwargs['context_parameters'] = self.context_parameters
kwargs['request'] = self.request
if 'from_log' in self.request.GET:
try:
from_log_id = self.request.GET.get('from_log')
@@ -355,9 +354,9 @@ class OrderSendView(BaseSenderView):
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True)
orders = qs.filter(statusq)
opq = OrderPosition.objects.with_scopes_disabled().filter(
opq = OrderPosition.objects.filter(
Q(item_id__in=[i.pk for i in form.cleaned_data.get('items')]) | Q(Exists(
OrderPosition.objects.with_scopes_disabled().filter(
OrderPosition.objects.filter(
addon_to_id=OuterRef('pk'),
item_id__in=[i.pk for i in form.cleaned_data.get('items')]
)
@@ -367,43 +366,36 @@ class OrderSendView(BaseSenderView):
)
if form.cleaned_data.get('filter_checkins'):
ci_filter = Q(pk__in=[]) # return nothing
ql = []
if form.cleaned_data.get('not_checked_in'):
consider_tickets_used_lists = list(self.request.event.checkin_lists.filter(consider_tickets_used=True).values_list("id", flat=True))
opq = opq.alias(
any_checkins=Exists(
Checkin.objects.with_scopes_disabled().filter(
position_id=OuterRef('pk'),
list_id__in=consider_tickets_used_lists,
)
) | Exists(
Checkin.objects.with_scopes_disabled().filter(
position__addon_to_id=OuterRef('pk'),
list_id__in=consider_tickets_used_lists,
Checkin.all.filter(
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
successful=True,
list__consider_tickets_used=True,
)
)
)
ci_filter |= Q(any_checkins=False)
ql.append(Q(any_checkins=False))
if form.cleaned_data.get('checkin_lists'):
opq = opq.alias(
matching_checkins=Exists(
Checkin.objects.with_scopes_disabled().filter(
position_id=OuterRef('pk'),
list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])],
)
) | Exists(
Checkin.objects.with_scopes_disabled().filter(
position__addon_to_id=OuterRef('pk'),
Checkin.all.filter(
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])],
successful=True
)
)
)
ci_filter |= Q(matching_checkins=True)
opq = opq.filter(ci_filter)
ql.append(Q(matching_checkins=True))
if len(ql) == 2:
opq = opq.filter(ql[0] | ql[1])
elif ql:
opq = opq.filter(ql[0])
else:
opq = opq.none()
if form.cleaned_data.get('subevent'):
opq = opq.filter(subevent=form.cleaned_data.get('subevent'))
@@ -1,81 +1,81 @@
/* globals Morris, django */
function gettext (msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid)
}
return msgid
/*globals $, Morris, gettext, django*/
function gettext(msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid);
}
return msgid;
}
$(function () {
$('.chart').css('height', '250px')
new Morris.Area({
element: 'obd_chart',
data: JSON.parse($('#obd-data').html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
})
new Morris.Area({
element: 'abd_chart',
data: JSON.parse($('#abd-data').html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
})
new Morris.Area({
element: 'abt_chart',
data: JSON.parse($('#abt-data').html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
})
new Morris.Area({
element: 'rev_chart',
data: JSON.parse($('#rev-data').html()),
xkey: 'date',
ykeys: ['revenue'],
labels: [gettext('Total revenue')],
smooth: false,
resize: true,
lineColors: ['#3b1c4a'],
fillOpacity: 0.3,
preUnits: $.trim($('#currency').html()) + ' '
})
new Morris.Bar({
element: 'obp_chart',
data: JSON.parse($('#obp-data').html()),
xkey: 'item_short',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
barColors: ['#3b1c4a', '#50a167'],
hoverCallback: function (index, options, content, row) {
console.log(content)
let $c = $('<div>' + content + '</div>')
let $label = $c.find('.morris-hover-row-label')
$label.text(row.item)
let newc = $label.get(0).outerHTML
$c.find('.morris-hover-point').each(function (i, r) {
if ($.trim($(r).text().split('\n')[2]) !== '0') {
newc += r.outerHTML
}
})
return newc
},
resize: true,
xLabelAngle: 30
})
})
$(".chart").css("height", "250px");
new Morris.Area({
element: 'obd_chart',
data: JSON.parse($("#obd-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abd_chart',
data: JSON.parse($("#abd-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abt_chart',
data: JSON.parse($("#abt-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'rev_chart',
data: JSON.parse($("#rev-data").html()),
xkey: 'date',
ykeys: ['revenue'],
labels: [gettext('Total revenue')],
smooth: false,
resize: true,
lineColors: ['#3b1c4a'],
fillOpacity: 0.3,
preUnits: $.trim($("#currency").html()) + ' '
});
new Morris.Bar({
element: 'obp_chart',
data: JSON.parse($("#obp-data").html()),
xkey: 'item_short',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
barColors: ['#3b1c4a', '#50a167'],
hoverCallback: function (index, options, content, row) {
console.log(content);
var $c = $("<div>" + content + "</div>");
var $label = $c.find(".morris-hover-row-label");
$label.text(row.item);
var newc = $label.get(0).outerHTML;
$c.find('.morris-hover-point').each(function (i, r) {
if ($.trim($(r).text().split("\n")[2]) !== "0") {
newc += r.outerHTML;
}
});
return newc;
},
resize: true,
xLabelAngle: 30
});
});
@@ -1,435 +1,435 @@
/* global stripe_pubkey, stripe_loadingmessage, gettext */
'use strict'
/*global $, stripe_pubkey, stripe_loadingmessage, gettext */
'use strict';
var pretixstripe = {
stripe: null,
elements: null,
card: null,
sepa: null,
affirm: null,
klarna: null,
paymentRequest: null,
paymentRequestButton: null,
stripe: null,
elements: null,
card: null,
sepa: null,
affirm: null,
klarna: null,
paymentRequest: null,
paymentRequestButton: null,
pm_request: function (method, element, kwargs = {}) {
waitingDialog.show(gettext('Contacting Stripe …'))
$('.stripe-errors').hide()
'pm_request': function (method, element, kwargs = {}) {
waitingDialog.show(gettext("Contacting Stripe …"));
$(".stripe-errors").hide();
pretixstripe.stripe.createPaymentMethod(method, element, kwargs).then(function (result) {
waitingDialog.hide()
if (result.error) {
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
let $form = $('#stripe_' + method + '_payment_method_id').closest('form')
// Insert the token into the form so it gets submitted to the server
$('#stripe_' + method + '_payment_method_id').val(result.paymentMethod.id)
if (method === 'card') {
$('#stripe_card_brand').val(result.paymentMethod.card.brand)
$('#stripe_card_last4').val(result.paymentMethod.card.last4)
}
if (method === 'sepa_debit') {
$('#stripe_sepa_debit_last4').val(result.paymentMethod.sepa_debit.last4)
}
// and submit
$form.get(0).submit()
}
}).catch((e) => {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + e + '</div>')
$('.stripe-errors').slideDown()
})
},
load: function () {
if (pretixstripe.stripe !== null) {
return
}
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', true)
$.ajax(
{
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($('#stripe_connectedAccountId').html())) {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
stripeAccount: $.trim($('#stripe_connectedAccountId').html()),
locale: $.trim($('body').attr('data-locale'))
})
} else {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
locale: $.trim($('body').attr('data-locale'))
})
}
pretixstripe.elements = pretixstripe.stripe.elements()
if ($.trim($('#stripe_merchantcountry').html()) !== '') {
try {
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
country: $('#stripe_merchantcountry').html(),
currency: $('#stripe_card_currency').val().toLowerCase(),
total: {
label: gettext('Total'),
amount: parseInt($('#stripe_card_total').val())
},
displayItems: [],
requestPayerName: false,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: false,
})
pretixstripe.stripe.createPaymentMethod(method, element, kwargs).then(function (result) {
waitingDialog.hide();
if (result.error) {
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>" + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
var $form = $("#stripe_" + method + "_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_" + method + "_payment_method_id").val(result.paymentMethod.id);
if (method === 'card') {
$("#stripe_card_brand").val(result.paymentMethod.card.brand);
$("#stripe_card_last4").val(result.paymentMethod.card.last4);
}
if (method === 'sepa_debit') {
$("#stripe_sepa_debit_last4").val(result.paymentMethod.sepa_debit.last4);
}
// and submit
$form.get(0).submit();
}
}).catch((e) => {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + e + "</div>");
$(".stripe-errors").slideDown();
});
},
'load': function () {
if (pretixstripe.stripe !== null) {
return;
}
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", true);
$.ajax(
{
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($("#stripe_connectedAccountId").html()),
locale: $.trim($("body").attr("data-locale"))
});
} else {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
locale: $.trim($("body").attr("data-locale"))
});
}
pretixstripe.elements = pretixstripe.stripe.elements();
if ($.trim($("#stripe_merchantcountry").html()) !== "") {
try {
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
country: $("#stripe_merchantcountry").html(),
currency: $("#stripe_card_currency").val().toLowerCase(),
total: {
label: gettext('Total'),
amount: parseInt($("#stripe_card_total").val())
},
displayItems: [],
requestPayerName: false,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: false,
});
pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success')
pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success');
let $form = $('#stripe_card_payment_method_id').closest('form')
// Insert the token into the form so it gets submitted to the server
$('#stripe_card_payment_method_id').val(ev.paymentMethod.id)
$('#stripe_card_brand').val(ev.paymentMethod.card.brand)
$('#stripe_card_last4').val(ev.paymentMethod.card.last4)
// and submit
$form.get(0).submit()
})
} catch (e) {
pretixstripe.paymentRequest = null
}
} else {
pretixstripe.paymentRequest = null
}
if ($('#stripe-card').length) {
pretixstripe.card = pretixstripe.elements.create('card', {
style: {
base: {
fontFamily: '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
fontSize: '14px',
color: '#555555',
lineHeight: '1.42857',
border: '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
invalid: {
color: 'red',
},
},
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
})
pretixstripe.card.mount('#stripe-card')
pretixstripe.card.on('ready', function () {
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', false)
})
}
if ($('#stripe-sepa').length) {
pretixstripe.sepa = pretixstripe.elements.create('iban', {
style: {
base: {
fontFamily: '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
fontSize: '14px',
color: '#555555',
lineHeight: '1.42857',
border: '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
invalid: {
color: 'red',
},
},
supportedCountries: ['SEPA'],
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
})
pretixstripe.sepa.on('change', function (event) {
// List of IBAN-countries, that require the country as well as line1-property according to
// https://stripe.com/docs/payments/sepa-debit/accept-a-payment?platform=web&ui=element#web-submit-payment
if (['AD', 'PF', 'TF', 'GI', 'GB', 'GG', 'VA', 'IM', 'JE', 'MC', 'NC', 'BL', 'PM', 'SM', 'CH', 'WF'].indexOf(event.country) > 0) {
$('#stripe_sepa_debit_country').prop('checked', true)
$('#stripe_sepa_debit_country').change()
} else {
$('#stripe_sepa_debit_country').prop('checked', false)
$('#stripe_sepa_debit_country').change()
}
if (event.bankName) {
$('#stripe_sepa_debit_bank').val(event.bankName)
}
})
pretixstripe.sepa.mount('#stripe-sepa')
pretixstripe.sepa.on('ready', function () {
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', false)
})
}
if ($('#stripe-affirm').length) {
pretixstripe.affirm = pretixstripe.elements.create('affirmMessage', {
amount: parseInt($('#stripe_affirm_total').val()),
currency: $('#stripe_affirm_currency').val(),
})
var $form = $("#stripe_card_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_card_payment_method_id").val(ev.paymentMethod.id);
$("#stripe_card_brand").val(ev.paymentMethod.card.brand);
$("#stripe_card_last4").val(ev.paymentMethod.card.last4);
// and submit
$form.get(0).submit();
});
} catch (e) {
pretixstripe.paymentRequest = null;
}
} else {
pretixstripe.paymentRequest = null;
}
if ($("#stripe-card").length) {
pretixstripe.card = pretixstripe.elements.create('card', {
'style': {
'base': {
'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
'fontSize': '14px',
'color': '#555555',
'lineHeight': '1.42857',
'border': '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
'invalid': {
'color': 'red',
},
},
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
});
pretixstripe.card.mount("#stripe-card");
pretixstripe.card.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
}
if ($("#stripe-sepa").length) {
pretixstripe.sepa = pretixstripe.elements.create('iban', {
'style': {
'base': {
'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
'fontSize': '14px',
'color': '#555555',
'lineHeight': '1.42857',
'border': '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
'invalid': {
'color': 'red',
},
},
supportedCountries: ['SEPA'],
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
});
pretixstripe.sepa.on('change', function (event) {
// List of IBAN-countries, that require the country as well as line1-property according to
// https://stripe.com/docs/payments/sepa-debit/accept-a-payment?platform=web&ui=element#web-submit-payment
if (['AD', 'PF', 'TF', 'GI', 'GB', 'GG', 'VA', 'IM', 'JE', 'MC', 'NC', 'BL', 'PM', 'SM', 'CH', 'WF'].indexOf(event.country) > 0) {
$("#stripe_sepa_debit_country").prop('checked', true);
$("#stripe_sepa_debit_country").change();
} else {
$("#stripe_sepa_debit_country").prop('checked', false);
$("#stripe_sepa_debit_country").change();
}
if (event.bankName) {
$("#stripe_sepa_debit_bank").val(event.bankName);
}
});
pretixstripe.sepa.mount("#stripe-sepa");
pretixstripe.sepa.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
}
if ($("#stripe-affirm").length) {
pretixstripe.affirm = pretixstripe.elements.create('affirmMessage', {
'amount': parseInt($("#stripe_affirm_total").val()),
'currency': $("#stripe_affirm_currency").val(),
});
pretixstripe.affirm.mount('#stripe-affirm')
}
if ($('#stripe-klarna').length) {
try {
pretixstripe.klarna = pretixstripe.elements.create('paymentMethodMessaging', {
amount: parseInt($('#stripe_klarna_total').val()),
currency: $('#stripe_klarna_currency').val(),
countryCode: $('#stripe_klarna_country').val(),
paymentMethodTypes: ['klarna'],
})
pretixstripe.affirm.mount('#stripe-affirm');
}
if ($("#stripe-klarna").length) {
try {
pretixstripe.klarna = pretixstripe.elements.create('paymentMethodMessaging', {
'amount': parseInt($("#stripe_klarna_total").val()),
'currency': $("#stripe_klarna_currency").val(),
'countryCode': $("#stripe_klarna_country").val(),
'paymentMethodTypes': ['klarna'],
});
pretixstripe.klarna.mount('#stripe-klarna')
} catch (e) {
console.error(e)
$('#stripe-klarna').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + e + '</div>')
}
}
if ($('#stripe-payment-request-button').length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest,
})
pretixstripe.klarna.mount('#stripe-klarna');
} catch (e) {
console.error(e);
$("#stripe-klarna").html("<div class='alert alert-danger'>Technical error, please contact support: " + e + "</div>");
}
}
if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest,
});
pretixstripe.paymentRequest.canMakePayment().then(function (result) {
if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button')
$('#stripe-card-elements .stripe-or').removeClass('hidden')
$('#stripe-payment-request-button').parent().removeClass('hidden')
} else {
$('#stripe-payment-request-button').hide()
document.getElementById('stripe-payment-request-button').style.display = 'none'
}
})
}
}
}
)
},
withStripe: function (callback) {
$.ajax({
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($('#stripe_connectedAccountId').html())) {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
stripeAccount: $.trim($('#stripe_connectedAccountId').html()),
locale: $.trim($('body').attr('data-locale'))
})
} else {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
locale: $.trim($('body').attr('data-locale'))
})
}
callback()
}
})
},
handleAlipayAction: function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmAlipayPayment(
payment_intent_client_secret,
{
return_url: window.location.href
}
).then(function (result) {
if (result.error) {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
waitingDialog.show(gettext('Confirming your payment …'))
}
})
})
},
handleWechatAction: function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmWechatPayPayment(
payment_intent_client_secret,
{
payment_method_options: {
wechat_pay: {
client: 'web',
},
},
}
).then(function (result) {
if (result.error) {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
waitingDialog.show(gettext('Confirming your payment …'))
location.reload()
}
})
})
},
handleCardAction: function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.handleCardAction(
payment_intent_client_secret
).then(function (result) {
if (result.error) {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
waitingDialog.show(gettext('Confirming your payment …'))
location.reload()
}
})
})
},
handlePaymentRedirectAction: function (payment_intent_next_action_redirect_url) {
waitingDialog.show(gettext('Contacting your bank …'))
pretixstripe.paymentRequest.canMakePayment().then(function (result) {
if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button');
$('#stripe-card-elements .stripe-or').removeClass("hidden");
$('#stripe-payment-request-button').parent().removeClass("hidden");
} else {
$('#stripe-payment-request-button').hide();
document.getElementById('stripe-payment-request-button').style.display = 'none';
}
});
}
}
}
);
},
'withStripe': function (callback) {
$.ajax({
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($("#stripe_connectedAccountId").html()),
locale: $.trim($("body").attr("data-locale"))
});
} else {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
locale: $.trim($("body").attr("data-locale"))
});
}
callback();
}
});
},
'handleAlipayAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmAlipayPayment(
payment_intent_client_secret,
{
return_url: window.location.href
}
).then(function (result) {
if (result.error) {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
waitingDialog.show(gettext("Confirming your payment …"));
}
});
});
},
'handleWechatAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmWechatPayPayment(
payment_intent_client_secret,
{
payment_method_options: {
wechat_pay: {
client: 'web',
},
},
}
).then(function (result) {
if (result.error) {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
waitingDialog.show(gettext("Confirming your payment …"));
location.reload();
}
});
});
},
'handleCardAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.handleCardAction(
payment_intent_client_secret
).then(function (result) {
if (result.error) {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
waitingDialog.show(gettext("Confirming your payment …"));
location.reload();
}
});
});
},
'handlePaymentRedirectAction': function (payment_intent_next_action_redirect_url) {
waitingDialog.show(gettext("Contacting your bank …"));
let payment_intent_redirect_action_handling = $.trim($('#stripe_payment_intent_redirect_action_handling').html())
if (payment_intent_redirect_action_handling === 'iframe') {
let iframe = document.createElement('iframe')
iframe.src = payment_intent_next_action_redirect_url
iframe.className = 'embed-responsive-item'
$('#scacontainer').append(iframe)
$('#scacontainer iframe').on('load', function () {
waitingDialog.hide()
})
} else if (payment_intent_redirect_action_handling === 'redirect') {
window.location.href = payment_intent_next_action_redirect_url
}
}
}
let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
if (payment_intent_redirect_action_handling === 'iframe') {
let iframe = document.createElement('iframe');
iframe.src = payment_intent_next_action_redirect_url;
iframe.className = 'embed-responsive-item';
$('#scacontainer').append(iframe);
$('#scacontainer iframe').on("load", function () {
waitingDialog.hide();
});
} else if (payment_intent_redirect_action_handling === 'redirect') {
window.location.href = payment_intent_next_action_redirect_url;
}
}
};
$(function () {
if ($('#stripe_payment_intent_SCA_status').length) {
let payment_intent_redirect_action_handling = $.trim($('#stripe_payment_intent_redirect_action_handling').html())
let order_status = $.trim($('#order_status').html())
let order_url = $.trim($('#order_url').html())
if ($("#stripe_payment_intent_SCA_status").length) {
let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
let order_status = $.trim($("#order_status").html());
let order_url = $.trim($("#order_url").html())
if (payment_intent_redirect_action_handling === 'iframe') {
window.parent.postMessage('3DS-authentication-complete.' + order_status, '*')
return
} else if (payment_intent_redirect_action_handling === 'redirect') {
waitingDialog.show(gettext('Confirming your payment …'))
if (payment_intent_redirect_action_handling === 'iframe') {
window.parent.postMessage('3DS-authentication-complete.' + order_status, '*');
return;
} else if (payment_intent_redirect_action_handling === 'redirect') {
waitingDialog.show(gettext("Confirming your payment …"));
if (order_status === 'p') {
window.location.href = order_url + '?paid=yes'
} else {
window.location.href = order_url
}
}
} else if ($('#stripe_payment_intent_next_action_redirect_url').length) {
let payment_intent_next_action_redirect_url = JSON.parse($('#stripe_payment_intent_next_action_redirect_url').html())
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url)
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'promptpay_display_qr_code') {
waitingDialog.hide()
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'wechat_pay_display_qr_code') {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html())
pretixstripe.handleWechatAction(payment_intent_client_secret)
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'alipay_handle_redirect') {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html())
pretixstripe.handleAlipayAction(payment_intent_client_secret)
} else if ($('#stripe_payment_intent_client_secret').length) {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html())
pretixstripe.handleCardAction(payment_intent_client_secret)
}
if (order_status === 'p') {
window.location.href = order_url + '?paid=yes';
} else {
window.location.href = order_url;
}
}
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
waitingDialog.hide();
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "wechat_pay_display_qr_code") {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleWechatAction(payment_intent_client_secret);
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "alipay_handle_redirect") {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleAlipayAction(payment_intent_client_secret);
} else if ($("#stripe_payment_intent_client_secret").length) {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleCardAction(payment_intent_client_secret);
}
$(window).on('message onmessage', function (e) {
if (typeof e.originalEvent.data === 'string' && e.originalEvent.data.startsWith('3DS-authentication-complete.')) {
waitingDialog.show(gettext('Confirming your payment …'))
$('#scacontainer').hide()
$('#continuebutton').removeClass('hidden')
$(window).on("message onmessage", function (e) {
if (typeof e.originalEvent.data === "string" && e.originalEvent.data.startsWith('3DS-authentication-complete.')) {
waitingDialog.show(gettext("Confirming your payment …"));
$('#scacontainer').hide();
$('#continuebutton').removeClass('hidden');
if (e.originalEvent.data.split('.')[1] == 'p') {
window.location.href = $('#continuebutton').attr('href') + '?paid=yes'
} else {
window.location.href = $('#continuebutton').attr('href')
}
}
})
if (e.originalEvent.data.split('.')[1] == 'p') {
window.location.href = $('#continuebutton').attr('href') + '?paid=yes';
} else {
window.location.href = $('#continuebutton').attr('href');
}
}
});
if (!$('.stripe-container').length)
return
if (!$(".stripe-container").length)
return;
if (
$('input[name=payment][value=stripe]').is(':checked')
|| $('input[name=payment][value=stripe_sepa_debit]').is(':checked')
|| $('input[name=payment][value=stripe_affirm]').is(':checked')
|| $('input[name=payment][value=stripe_klarna]').is(':checked')
|| $('.payment-redo-form').length) {
pretixstripe.load()
} else {
$('input[name=payment]').change(function () {
if (['stripe', 'stripe_sepa_debit', 'stripe_affirm', 'stripe_klarna'].indexOf($(this).val()) > -1) {
pretixstripe.load()
}
})
}
if (
$("input[name=payment][value=stripe]").is(':checked')
|| $("input[name=payment][value=stripe_sepa_debit]").is(':checked')
|| $("input[name=payment][value=stripe_affirm]").is(':checked')
|| $("input[name=payment][value=stripe_klarna]").is(':checked')
|| $(".payment-redo-form").length) {
pretixstripe.load();
} else {
$("input[name=payment]").change(function () {
if (['stripe', 'stripe_sepa_debit', 'stripe_affirm', 'stripe_klarna'].indexOf($(this).val()) > -1) {
pretixstripe.load();
}
})
}
$('#stripe_other_card').click(
function (e) {
$('#stripe_card_payment_method_id').val('')
$('#stripe-current-card').slideUp()
$('#stripe-card-elements').slideDown()
$("#stripe_other_card").click(
function (e) {
$("#stripe_card_payment_method_id").val("");
$("#stripe-current-card").slideUp();
$("#stripe-card-elements").slideDown();
e.preventDefault()
return false
}
)
e.preventDefault();
return false;
}
);
if ($('#stripe-current-card').length) {
$('#stripe-card-elements').hide()
}
if ($("#stripe-current-card").length) {
$("#stripe-card-elements").hide();
}
$('#stripe_other_account').click(
function (e) {
$('#stripe_sepa_debit_payment_method_id').val('')
$('#stripe-current-account').slideUp()
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').slideDown()
$("#stripe_other_account").click(
function (e) {
$("#stripe_sepa_debit_payment_method_id").val("");
$("#stripe-current-account").slideUp();
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').slideDown();
e.preventDefault()
return false
}
)
e.preventDefault();
return false;
}
);
if ($('#stripe-current-account').length) {
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').hide()
}
if ($("#stripe-current-account").length) {
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').hide();
}
$('.stripe-container').closest('form').submit(
function () {
if ($('input[name=card_new]').length && !$('input[name=card_new]').prop('checked')) {
return null
}
if (($('input[name=payment][value=stripe]').prop('checked') || $('input[name=payment][type=radio]').length === 0)
&& $('#stripe_card_payment_method_id').val() == '') {
pretixstripe.pm_request('card', pretixstripe.card)
return false
}
$('.stripe-container').closest("form").submit(
function () {
if ($("input[name=card_new]").length && !$("input[name=card_new]").prop('checked')) {
return null;
}
if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment][type=radio]").length === 0)
&& $("#stripe_card_payment_method_id").val() == "") {
pretixstripe.pm_request('card', pretixstripe.card);
return false;
}
if (($('input[name=payment][value=stripe_sepa_debit]').prop('checked')) && $('#stripe_sepa_debit_payment_method_id').val() == '') {
pretixstripe.pm_request('sepa_debit', pretixstripe.sepa, {
billing_details: {
name: $('#id_payment_stripe_sepa_debit-accountname').val(),
email: $('#stripe_sepa_debit_email').val(),
address: {
line1: $('#id_payment_stripe_sepa_debit-line1').val(),
postal_code: $('#id_payment_stripe_sepa_debit-postal_code').val(),
city: $('#id_payment_stripe_sepa_debit-city').val(),
country: $('#id_payment_stripe_sepa_debit-country').val(),
}
}
})
return false
}
}
)
})
if (($("input[name=payment][value=stripe_sepa_debit]").prop('checked')) && $("#stripe_sepa_debit_payment_method_id").val() == "") {
pretixstripe.pm_request('sepa_debit', pretixstripe.sepa, {
billing_details: {
name: $("#id_payment_stripe_sepa_debit-accountname").val(),
email: $("#stripe_sepa_debit_email").val(),
address: {
line1: $("#id_payment_stripe_sepa_debit-line1").val(),
postal_code: $("#id_payment_stripe_sepa_debit-postal_code").val(),
city: $("#id_payment_stripe_sepa_debit-city").val(),
country: $("#id_payment_stripe_sepa_debit-country").val(),
}
}
});
return false;
}
}
);
});
+3 -54
View File
@@ -35,7 +35,6 @@ import copy
import inspect
import uuid
from collections import defaultdict
from datetime import time
from decimal import Decimal
from django import forms
@@ -53,7 +52,6 @@ from django.shortcuts import redirect
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
@@ -73,7 +71,6 @@ from pretix.base.services.cart import (
from pretix.base.services.cross_selling import CrossSellingService
from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order
from pretix.base.services.payment import compute_payment_deadline
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -873,28 +870,6 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
'attendee_name_parts': d
})
wd = self.cart_session.get('widget_data', {})
if wd.get('attendee-fix', '') == 'true':
for k, v in wd.items():
if v and k.startswith('attendee-name'):
o.append({
'attendee_name_parts': {
'disabled': True,
}
})
elif v and k.startswith('email'):
o.append({
'attendee_email': {
'disabled': True,
}
})
elif v and k.startswith('question-'):
o.append({
k[9:].upper(): {
'disabled': True,
}
})
return o
@cached_property
@@ -1369,11 +1344,6 @@ class PaymentStep(CartMixin, TemplateFlowStep):
self.request = request
self.request.pci_dss_payment_page = True
if "postpone" in request.POST and self._allow_postpone:
self.cart_session['payments_postpone'] = True
self.cart_session['payments'] = []
return redirect_to_url(self.get_next_url(request))
if "remove_payment" in request.POST:
self._remove_payment(request.POST["remove_payment"])
return redirect_to_url(self.get_step_url(request))
@@ -1462,41 +1432,20 @@ class PaymentStep(CartMixin, TemplateFlowStep):
ctx['providers'] = self.provider_forms
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
if 'payment' in self.request.POST:
ctx['selected'] = self.request.POST['payment']
elif self.cart_session.get('payments_postpone') and self._allow_postpone:
ctx['selected'] = ''
elif len(self.provider_forms) == 1:
if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
elif 'payment' in self.request.POST:
ctx['selected'] = self.request.POST['payment']
elif self.single_use_payment:
ctx['selected'] = self.single_use_payment['provider']
else:
ctx['selected'] = ''
ctx['allow_postpone'] = self._allow_postpone
if self._allow_postpone:
now_dt = now()
ctx['payment_deadline'] = compute_payment_deadline(
event=self.request.event,
sales_channel=self.request.sales_channel,
subevents={p.subevent for p in ctx['cart']['raw']},
now_dt=now_dt,
)
if ctx['payment_deadline'].time() != time(hour=23, minute=59, second=59):
ctx['payment_deadline_minutes'] = int((ctx['payment_deadline'] - now_dt).total_seconds() // 60)
return ctx
@cached_property
def _allow_postpone(self):
return self.request.sales_channel.identifier in self.request.event.settings.payment_choice_postpone_allowed_channels
def _is_allowed(self, prov, request):
return prov.is_allowed(request, total=self._total_order_value)
def is_completed(self, request, warn=False):
if self.cart_session.get('payments_postpone') and self._allow_postpone:
return True
if not self.cart_session.get('payments'):
if warn:
messages.error(request, _('Please select a payment method to proceed.'))
@@ -128,35 +128,6 @@
{% endif %}
</div>
{% endif %}
{% if allow_postpone %}
<div class="panel panel-default">
<div class="panel-body row">
<div class="col-md-9 col-xs-12">
{% trans "Not sure yet? You can complete your order first and then select a payment method later." %}
<br>
<span class="text-muted">
{% if current_payments %}
{% trans "To do so, please first remove the payment methods you already selected above." %}
{% elif payment_deadline_minutes %}
{% blocktrans trimmed with minutes=payment_deadline_minutes %}
Your payment needs to be completed within {{ minutes }} minutes.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with deadline=payment_deadline|date:"SHORT_DATE_FORMAT" %}
Your payment needs to be completed by {{ deadline }}.
{% endblocktrans %}
{% endif %}
</span>
</div>
<div class="col-md-3 col-xs-12 text-right flip">
<button name="postpone" value="on" class="btn btn-primary"
{% if current_payments %}disabled{% endif %}>
{% trans "Proceed without selection" %}
</button>
</div>
</div>
</div>
{% endif %}
<div class="row checkout-button-row">
<div class="col-md-4 col-sm-6">
<a class="btn btn-block btn-default btn-lg"
@@ -23,11 +23,7 @@
{% endblocktrans %} ::
{% endif %}
{% elif subevent %}
{% if subevent.name|upper != event.name|upper %}
{# The |upper is a trick to force LazyI18nString→str conversion before comparison #}
{{ subevent.name }} ::
{% endif %}
{{ subevent.get_date_range_display_with_times }} ::
{{ subevent.get_date_range_display }} ::
{% endif %}
{% endblock %}
@@ -3,7 +3,7 @@
{% load escapejson %}
{% if payment_qr_codes %}
<div class="tabcontainer col-md-6 col-sm-6 col-xs-12 text-center js-only blank-after">
<div class="tabcontainer col-md-6 col-sm-6 hidden-xs text-center js-only blank-after">
<div id="banktransfer_qrcodes_tabs_content" class="tabpanels blank-after">
{% for code_info in payment_qr_codes %}
<div id="banktransfer_qrcodes_{{ code_info.id }}"
-6
View File
@@ -54,7 +54,6 @@ from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition
from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import customer_created, customer_signed_in
from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.models import KnownDomain
@@ -281,11 +280,6 @@ class SetPasswordView(FormView):
def form_valid(self, form):
with transaction.atomic():
# Re-check token in transaction to prevent race condition
self.customer = Customer.objects.select_for_update(of=OF_SELF).get(pk=self.customer.pk)
if not TokenGenerator().check_token(self.customer, self.request.GET.get('token', '')):
return HttpResponseRedirect(self.get_success_url())
self.customer.set_password(form.cleaned_data['password'])
self.customer.is_verified = True
self.customer.save()
+8 -9
View File
@@ -122,20 +122,19 @@ def widget_css_etag(request, version, **kwargs):
return f'{_get_source_cache_key(version)}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}'
# use vite by default, serve old vue2-based widget only for widget_vue2_origins
def _use_vite(request):
if getattr(settings, 'PRETIX_WIDGET_VUE', False) or "legacy" in request.GET:
return False
if getattr(settings, 'PRETIX_WIDGET_VITE', False) or "beta" in request.GET:
return True
origin = request.META.get('HTTP_ORIGIN', '')
gs = GlobalSettingsObject()
vue_origins = gs.settings.get('widget_vue2_origins', as_type=str, default='')
if vue_origins and not origin:
vite_origins = gs.settings.get('widget_vite_origins', as_type=str, default='')
if vite_origins and not origin:
referer = request.META.get('HTTP_REFERER', '')
origin = '/'.join(referer.split('/', 3)[:3])
if origin and vue_origins:
origins_list = [o.strip() for o in vue_origins.strip().splitlines() if o.strip()]
return origin not in origins_list
return True
if origin and vite_origins:
origins_list = [o.strip() for o in vite_origins.strip().splitlines() if o.strip()]
return origin in origins_list
return False
def widget_js_etag(request, version, lang, **kwargs):
+1 -1
View File
@@ -913,7 +913,7 @@ VITE_DEV_SERVER_PORT = 5173
VITE_DEV_SERVER = f"http://localhost:{VITE_DEV_SERVER_PORT}"
VITE_DEV_MODE = DEBUG
VITE_IGNORE = False # Used to ignore `collectstatic`/`rebuild`
PRETIX_WIDGET_VUE = os.environ.get('PRETIX_WIDGET_VUE', '') not in ('', '0')
PRETIX_WIDGET_VITE = os.environ.get('PRETIX_WIDGET_VITE', '') not in ('', '0')
if DEBUG:
# Reload if settings file changes
+171 -169
View File
@@ -1,193 +1,195 @@
$(function () {
'use strict'
"use strict";
// Responses are expected to only depend on the GET parameters passed, so we can have a little client-side cache
// to prevent fetching the same thing many times.
let responseCache = {}
// Responses are expected to only depend on the GET parameters passed, so we can have a little client-side cache
// to prevent fetching the same thing many times.
var responseCache = {};
const cleanName = (name) => {
// Remove form prefix
name = name.split('-').pop()
// Remove settings prefix
name = name.replace(/^invoice_address_from_/, '')
return name
}
const cleanName = (name) => {
// Remove form prefix
name = name.split("-").pop();
// Remove settings prefix
name = name.replace(/^invoice_address_from_/, "");
return name
}
$('[data-address-information-url]').each(function () {
let xhr
const form = $(this)
const dependencies = $(this).find('[data-trigger-address-info]')
const loader = $('<span class=\'fa fa-cog fa-spin\'></span>').hide().prependTo(dependencies.closest('.form-group').find('label').first())
const baseUrl = this.getAttribute('data-address-information-url')
const isAnyRequired = dependencies.toArray().some(function (e) { return $(e).closest('.form-group').is('.required') })
$("[data-address-information-url]").each(function () {
let xhr;
const form = $(this);
const dependencies = $(this).find("[data-trigger-address-info]");
const loader = $("<span class='fa fa-cog fa-spin'></span>").hide().prependTo(dependencies.closest(".form-group").find("label").first())
const baseUrl = this.getAttribute('data-address-information-url')
const isAnyRequired = dependencies.toArray().some(function (e) { return $(e).closest(".form-group").is(".required") });
const dependents = {
city: form.find('input[name$=city]'),
zipcode: form.find('input[name$=zipcode]'),
street: form.find('textarea[name$=street]'),
state: form.find('select[name$=state]'),
vat_id: form.find('input[name$=vat_id]'),
}
const dependents = {
'city': form.find("input[name$=city]"),
'zipcode': form.find("input[name$=zipcode]"),
'street': form.find("textarea[name$=street]"),
'state': form.find("select[name$=state]"),
'vat_id': form.find("input[name$=vat_id]"),
};
form.find('select[name*=transmission_], textarea[name*=transmission_], input[name*=transmission_]').each(function () {
dependents[cleanName($(this).attr('name'))] = $(this)
})
form.find("select[name*=transmission_], textarea[name*=transmission_], input[name*=transmission_]").each(function () {
dependents[cleanName($(this).attr("name"))] = $(this)
})
const dependentsDisabled = []
for (let k in dependents) {
if (dependents[k].prop('disabled')) {
dependentsDisabled.push(k)
}
}
const dependentsDisabled = [];
for (var k in dependents) {
if (dependents[k].prop("disabled")) {
dependentsDisabled.push(k);
}
}
if (!Object.values(dependents).some((el) => el.length)) {
// No address fields found, do not create request
return
}
if (!Object.values(dependents).some((el) => el.length)) {
// No address fields found, do not create request
return;
}
const update_form = function (data) {
let selected_state = dependents.state.prop('data-selected-value')
if (selected_state) dependents.state.prop('data-selected-value', '')
dependents.state.find('option:not([value=\'\'])').remove()
$.each(data.data, function (k, s) {
let o = $('<option>').attr('value', s.code).text(s.name)
if (selected_state === s.code) o.prop('selected', true)
dependents.state.append(o)
})
const update_form = function (data) {
var selected_state = dependents.state.prop("data-selected-value");
if (selected_state) dependents.state.prop("data-selected-value", "");
dependents.state.find("option:not([value=''])").remove();
$.each(data.data, function (k, s) {
var o = $("<option>").attr("value", s.code).text(s.name);
if (selected_state === s.code) o.prop("selected", true);
dependents.state.append(o);
});
if (dependents.transmission_type) {
let selected_transmission_type = dependents.transmission_type.prop('data-selected-value')
if (selected_transmission_type) dependents.transmission_type.prop('data-selected-value', '')
dependents.transmission_type.find('option:not([value=\'\']):not([value=\'-\'])').remove()
if (dependents.transmission_type) {
var selected_transmission_type = dependents.transmission_type.prop("data-selected-value");
if (selected_transmission_type) dependents.transmission_type.prop("data-selected-value", "");
dependents.transmission_type.find("option:not([value='']):not([value='-'])").remove();
if (!data.transmission_type.visible) {
selected_transmission_type = 'email'
}
if (!data.transmission_type.visible) {
selected_transmission_type = "email";
}
$.each(data.transmission_types, function (k, s) {
let o = $('<option>').attr('value', s.code).text(s.name)
if (selected_transmission_type === s.code) {
o.prop('selected', true)
}
dependents.transmission_type.append(o)
})
}
$.each(data.transmission_types, function (k, s) {
var o = $("<option>").attr("value", s.code).text(s.name);
if (selected_transmission_type === s.code) {
o.prop("selected", true);
}
dependents.transmission_type.append(o);
});
for (var k in dependents) {
const options = data[k],
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()
)
}
for (var k in dependents) {
const options = data[k],
dependent = dependents[k];
let visible = 'visible' in options ? options.visible : true;
if ('label' in options) {
dependent.closest('.form-group').find('.control-label').text(options.label)
}
if ('helptext_visible' in options) {
dependent.closest('.form-group').find('.help-block').toggle(options.helptext_visible)
}
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 && visible && (
(options.required === 'if_any' && isAnyRequired)
|| (options.required === true)
)
dependent.closest('.form-group').toggle(visible).toggleClass('required', required)
dependent.prop('required', required)
if ('label' in options) {
dependent.closest(".form-group").find(".control-label").text(options.label);
}
if ('helptext_visible' in options) {
dependent.closest(".form-group").find(".help-block").toggle(options.helptext_visible);
}
const label = dependent.closest('.form-group').find('label')
const labelRequired = label.find('.label-required')
if (!required) {
labelRequired.remove()
} else if (!labelRequired.length) {
label.append('<i class="label-required">' + gettext('required') + '</i>')
}
}
for (var k in dependents) dependents[k].prop('disabled', dependentsDisabled.includes(k))
loader.hide()
}
const required = 'required' in options && visible && (
(options.required === 'if_any' && isAnyRequired) ||
(options.required === true)
);
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required);
const update = function (ev) {
dependents.state.prop('data-selected-value', dependents.state.val())
if (dependents.transmission_type) {
dependents.transmission_type.prop('data-selected-value', dependents.transmission_type.val())
}
const label = dependent.closest(".form-group").find("label");
const labelRequired = label.find(".label-required");
if (!required) {
labelRequired.remove();
} else if (!labelRequired.length) {
label.append('<i class="label-required">' + gettext('required') + '</i>')
}
}
for (var k in dependents) dependents[k].prop("disabled", dependentsDisabled.includes(k));
loader.hide();
}
for (let k in dependents) dependents[k].prop('disabled', true)
loader.show()
let url = new URL(baseUrl, location.href)
// Address depends on all annotated fields
form.find('[data-trigger-address-info]').each(function () {
// Remove prefix of the form to get actual field name
if (($(this).attr('type') === 'radio' || $(this).attr('type') === 'checkbox') && !$(this).prop('checked')) {
return
}
url.searchParams.append(cleanName($(this).attr('name')), $(this).val())
})
if (dependents.transmission_type) {
url.searchParams.append('transmission_type_required', !dependents.transmission_type.find('option[value=\'-\']').length)
}
const update = function (ev) {
dependents.state.prop("data-selected-value", dependents.state.val());
if (dependents.transmission_type) {
dependents.transmission_type.prop("data-selected-value", dependents.transmission_type.val());
}
if (xhr && url in responseCache) {
if (responseCache[url] == xhr) {
// already requested this, but XHR is still running and will resolve promise
// only re-resolve promise for JSON-data in responseCache[url]
return
} else {
// abort current xhr as it is not the one we want
// aborting deletes responseCache[url] but async
xhr.abort()
}
}
for (var k in dependents) dependents[k].prop("disabled", true);
loader.show();
var url = new URL(baseUrl, location.href);
// Address depends on all annotated fields
form.find("[data-trigger-address-info]").each(function () {
// Remove prefix of the form to get actual field name
if (($(this).attr("type") === "radio" || $(this).attr("type") === "checkbox") && !$(this).prop("checked")) {
return
}
url.searchParams.append(cleanName($(this).attr("name")), $(this).val());
})
if (dependents.transmission_type) {
url.searchParams.append("transmission_type_required", !dependents.transmission_type.find("option[value='-']").length);
}
if (!(url in responseCache)) {
responseCache[url] = xhr = $.ajax({
dataType: 'json',
url: url,
timeout: 3000,
})
}
if (xhr && url in responseCache) {
if (responseCache[url] == xhr) {
// already requested this, but XHR is still running and will resolve promise
// only re-resolve promise for JSON-data in responseCache[url]
return;
} else {
// abort current xhr as it is not the one we want
// aborting deletes responseCache[url] but async
xhr.abort();
}
}
Promise.resolve(responseCache[url]).then(function (data) {
responseCache[url] = data
update_form(data)
}).catch(function () {
delete responseCache[url]
// In case of errors, show everything and require nothing, we can still handle errors in backend
for (let k in dependents) {
const dependent = dependents[k],
visible = true,
required = false
if (!(url in responseCache)) {
responseCache[url] = xhr = $.ajax({
dataType: "json",
url: url,
timeout: 3000,
});
}
dependent.closest('.form-group').toggle(visible).toggleClass('required', required)
dependent.prop('required', required).prop('disabled', dependentsDisabled.includes(k))
}
}).finally(function () {
loader.hide()
})
}
update()
dependencies.on('change', update)
Promise.resolve(responseCache[url]).then(function (data) {
responseCache[url] = data;
update_form(data);
}).catch(function () {
delete responseCache[url];
// In case of errors, show everything and require nothing, we can still handle errors in backend
for (var k in dependents) {
const dependent = dependents[k],
visible = true,
required = false;
if (dependents.vat_id && dependents.transmission_type && dependents.transmission_peppol_participant_id) {
// In Belgium, the VAT ID is built from "BE" + the company ID. The Peppol ID also needs to be built
// from the company ID with ID scheme 0208. We can save users some knowing and typing by filling this in!
if (!dependents.transmission_peppol_participant_id.val()) {
const fill_peppol_id = function () {
const vatId = dependents.vat_id.val()
if (vatId && vatId.startsWith('BE') && dependents.transmission_type.val() === 'peppol') {
dependents.transmission_peppol_participant_id.val('0208:' + vatId.substring(2).replaceAll('.', ''))
}
}
dependents.vat_id.add(dependents.transmission_type).on('change', fill_peppol_id)
dependents.transmission_peppol_participant_id.one('change', () => {
dependents.vat_id.add(dependents.transmission_type).unbind('change', fill_peppol_id)
})
}
}
})
})
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required).prop("disabled", dependentsDisabled.includes(k));
}
}).finally(function () {
loader.hide();
});
};
update();
dependencies.on("change", update);
if (dependents.vat_id && dependents.transmission_type && dependents.transmission_peppol_participant_id) {
// In Belgium, the VAT ID is built from "BE" + the company ID. The Peppol ID also needs to be built
// from the company ID with ID scheme 0208. We can save users some knowing and typing by filling this in!
if (!dependents.transmission_peppol_participant_id.val()) {
const fill_peppol_id = function () {
const vatId = dependents.vat_id.val();
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol") {
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2).replaceAll(".", ""))
}
}
dependents.vat_id.add(dependents.transmission_type).on("change", fill_peppol_id);
dependents.transmission_peppol_participant_id.one("change", () => {
dependents.vat_id.add(dependents.transmission_type).unbind("change", fill_peppol_id)
});
}
}
});
});
@@ -1,10 +1,10 @@
let check = function () {
$.getJSON(location.href + '&ajax=1', function (data, _status) {
if (data.redirect) {
location.href = data.redirect
} else {
window.setTimeout(check, 500)
}
})
var check = function () {
$.getJSON(location.href + '&ajax=1', function (data, status) {
if (data.redirect) {
location.href = data.redirect;
} else {
window.setTimeout(check, 500);
}
});
}
window.setTimeout(check, 500)
window.setTimeout(check, 500);
+330 -328
View File
@@ -1,357 +1,359 @@
/* global gettext */
let async_task_id = null
let async_task_timeout = null
let async_task_check_url = null
let async_task_old_url = null
let async_task_is_download = false
let async_task_is_long = false
let async_task_dont_redirect = false
/*global $, gettext */
var async_task_id = null;
var async_task_timeout = null;
var async_task_check_url = null;
var async_task_old_url = null;
var async_task_is_download = false;
var async_task_is_long = false;
var async_task_dont_redirect = false;
let async_task_status_messages = {
// These are functions in order to be lazily evaluated after the gettext file is loaded
long_task_started: () => gettext(
'Your request is currently being processed. Depending on the size of your event, this might take up to '
+ 'a few minutes.'
),
long_task_pending: () => gettext(
'Your request has been queued on the server and will soon be '
+ 'processed.'
),
short_task: () => gettext(
'Your request arrived on the server but we still wait for it to be '
+ 'processed. If this takes longer than two minutes, please contact us or go '
+ 'back in your browser and try again.'
)
var async_task_status_messages = {
// These are functions in order to be lazily evaluated after the gettext file is loaded
long_task_started: () => gettext(
'Your request is currently being processed. Depending on the size of your event, this might take up to ' +
'a few minutes.'
),
long_task_pending: () => gettext(
'Your request has been queued on the server and will soon be ' +
'processed.'
),
short_task: () => gettext(
'Your request arrived on the server but we still wait for it to be ' +
'processed. If this takes longer than two minutes, please contact us or go ' +
'back in your browser and try again.'
)
};
function async_task_schedule_check(context, timeout) {
"use strict";
async_task_timeout = window.setTimeout(function() {
$.ajax(
{
'type': 'GET',
'url': async_task_check_url,
'success': async_task_check_callback,
'error': async_task_check_error,
'context': context,
'dataType': 'json'
}
);
}, timeout);
}
function async_task_schedule_check (context, timeout) {
'use strict'
async_task_timeout = window.setTimeout(function () {
$.ajax(
{
type: 'GET',
url: async_task_check_url,
success: async_task_check_callback,
error: async_task_check_error,
context: context,
dataType: 'json'
}
)
}, timeout)
function async_task_on_success(data) {
"use strict";
if ((async_task_is_download && data.success) || async_task_dont_redirect) {
waitingDialog.hide();
if (location.href.indexOf("async_id") !== -1) {
history.replaceState({}, "pretix", async_task_old_url);
}
}
if (!async_task_dont_redirect) {
$(window).one("pageshow", function (e) {
// hide waitingDialog when using browser's history back
waitingDialog.hide();
});
if (async_task_is_download && window.self !== window.top) {
// if in an iframe, force to download an async_task_is_download
// e.g. pretix-reseller embeds order-page in iframe, which would cause ticket-PDFs to be displayed inline
var a = document.createElement("a");
a.href = data.redirect;
a.download = "";
a.target = "_blank";
a.click();
} else {
location.href = data.redirect;
}
}
$(this).trigger('pretix:async-task-success', data);
}
function async_task_on_success (data) {
'use strict'
if ((async_task_is_download && data.success) || async_task_dont_redirect) {
waitingDialog.hide()
if (location.href.indexOf('async_id') !== -1) {
history.replaceState({}, 'pretix', async_task_old_url)
}
}
if (!async_task_dont_redirect) {
$(window).one('pageshow', function (e) {
// hide waitingDialog when using browser's history back
waitingDialog.hide()
})
if (async_task_is_download && window.self !== window.top) {
// if in an iframe, force to download an async_task_is_download
// e.g. pretix-reseller embeds order-page in iframe, which would cause ticket-PDFs to be displayed inline
let a = document.createElement('a')
a.href = data.redirect
a.download = ''
a.target = '_blank'
a.click()
} else {
location.href = data.redirect
}
}
$(this).trigger('pretix:async-task-success', data)
function async_task_check_callback(data, textStatus, jqXHR) {
"use strict";
if (data.ready && data.redirect) {
async_task_on_success.call(this, data);
return;
}
if (typeof data.percentage === "number") {
waitingDialog.setProgress(data.percentage);
}
if (typeof data.steps === "object" && Array.isArray(data.steps)) {
waitingDialog.setSteps(data.steps);
}
async_task_schedule_check(this, 250);
async_task_update_status(data);
}
function async_task_check_callback (data, textStatus, jqXHR) {
'use strict'
if (data.ready && data.redirect) {
async_task_on_success.call(this, data)
return
}
if (typeof data.percentage === 'number') {
waitingDialog.setProgress(data.percentage)
}
if (typeof data.steps === 'object' && Array.isArray(data.steps)) {
waitingDialog.setSteps(data.steps)
}
async_task_schedule_check(this, 250)
async_task_update_status(data)
function async_task_update_status(data) {
if (async_task_is_long) {
if (data.started) {
waitingDialog.setStatus(async_task_status_messages.long_task_started());
} else {
waitingDialog.setStatus(async_task_status_messages.long_task_pending());
}
} else {
waitingDialog.setStatus(async_task_status_messages.short_task());
}
}
function async_task_update_status (data) {
if (async_task_is_long) {
if (data.started) {
waitingDialog.setStatus(async_task_status_messages.long_task_started())
} else {
waitingDialog.setStatus(async_task_status_messages.long_task_pending())
}
} else {
waitingDialog.setStatus(async_task_status_messages.short_task())
}
function async_task_replace_page(target, new_html) {
"use strict";
waitingDialog.hide();
$(target).html(new_html);
setup_basics($(target));
form_handlers($(target));
setup_collapsible_details($(target));
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
$(document).trigger("pretix:bind-forms");
}
function async_task_replace_page (target, new_html) {
'use strict'
waitingDialog.hide()
$(target).html(new_html)
setup_basics($(target))
form_handlers($(target))
setup_collapsible_details($(target))
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
$(document).trigger('pretix:bind-forms')
function async_task_check_error(jqXHR, textStatus, errorThrown) {
"use strict";
var respdom = $(jqXHR.responseText);
var c = respdom.filter('.container');
if (jqXHR.status === 401 && jqXHR.getResponseHeader("X-Login-Url")) {
window.location = jqXHR.getResponseHeader("X-Login-Url") + "?next=" + encodeURIComponent(location.pathname + location.search + location.hash);
return;
}
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it
$("body").data('ajaxing', false);
async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
$("body").data('ajaxing', false);
waitingDialog.hide();
if (location.href.indexOf("async_id") !== -1) {
history.replaceState({}, "pretix", async_task_old_url);
}
ajaxErrDialog.show(c.first().html());
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
$("body").data('ajaxing', false);
waitingDialog.hide();
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else {
// 500 can be an application error or overload in some cases :(
waitingDialog.setStatus(gettext('We currently cannot reach the server, but we keep trying.' +
' Last error code: {code}').replace(/\{code\}/, jqXHR.status));
async_task_schedule_check(this, 5000);
}
}
}
function async_task_check_error (jqXHR, textStatus, errorThrown) {
'use strict'
let respdom = $(jqXHR.responseText)
let c = respdom.filter('.container')
if (jqXHR.status === 401 && jqXHR.getResponseHeader('X-Login-Url')) {
window.location = jqXHR.getResponseHeader('X-Login-Url') + '?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
return
}
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it
$('body').data('ajaxing', false)
async_task_replace_page('body', jqXHR.responseText.substring(
jqXHR.responseText.indexOf('<body'),
jqXHR.responseText.indexOf('</body')
))
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
$('body').data('ajaxing', false)
waitingDialog.hide()
if (location.href.indexOf('async_id') !== -1) {
history.replaceState({}, 'pretix', async_task_old_url)
}
ajaxErrDialog.show(c.first().html())
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
$('body').data('ajaxing', false)
waitingDialog.hide()
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
} else {
// 500 can be an application error or overload in some cases :(
waitingDialog.setStatus(gettext('We currently cannot reach the server, but we keep trying.'
+ ' Last error code: {code}').replace(/\{code\}/, jqXHR.status))
async_task_schedule_check(this, 5000)
}
}
function async_task_callback(data, jqXHR, status) {
"use strict";
$("body").data('ajaxing', false);
if (data.redirect) {
async_task_on_success.call(this, data);
return;
}
var check_url = new URL(data.check_url, window.location);
if (async_task_dont_redirect) {
check_url.searchParams.set('ajax_dont_redirect', '1');
}
async_task_id = data.async_id;
async_task_check_url = check_url.toString();
async_task_schedule_check(this, 100);
async_task_update_status(data);
if (location.href.indexOf("async_id") === -1) {
history.pushState({}, "Waiting", async_task_check_url.replace(/ajax=1/, ''));
}
}
function async_task_callback (data, jqXHR, status) {
'use strict'
$('body').data('ajaxing', false)
if (data.redirect) {
async_task_on_success.call(this, data)
return
}
let check_url = new URL(data.check_url, window.location)
if (async_task_dont_redirect) {
check_url.searchParams.set('ajax_dont_redirect', '1')
}
async_task_id = data.async_id
async_task_check_url = check_url.toString()
async_task_schedule_check(this, 100)
function async_task_error(jqXHR, textStatus, errorThrown) {
"use strict";
$("body").data('ajaxing', false);
if (jqXHR.status === 401 && jqXHR.getResponseHeader("X-Login-Url")) {
window.location = jqXHR.getResponseHeader("X-Login-Url") + "?next=" + encodeURIComponent(location.pathname + location.search + location.hash);
return;
}
waitingDialog.hide();
if (textStatus === "timeout") {
alert(gettext("The request took too long. Please try again."));
} else if (jqXHR.responseText.indexOf('<html') > 0) {
var respdom = $(jqXHR.responseText);
var c = respdom.filter('.container');
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
// This is a failed form validation, let's just use it
async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
} else {
async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
document.dispatchEvent(new Event("pretix:async-task-error"))
async_task_update_status(data)
}
if (location.href.indexOf('async_id') === -1) {
history.pushState({}, 'Waiting', async_task_check_url.replace(/ajax=1/, ''))
}
}
function async_task_error (jqXHR, textStatus, errorThrown) {
'use strict'
$('body').data('ajaxing', false)
if (jqXHR.status === 401 && jqXHR.getResponseHeader('X-Login-Url')) {
window.location = jqXHR.getResponseHeader('X-Login-Url') + '?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
return
}
waitingDialog.hide()
if (textStatus === 'timeout') {
alert(gettext('The request took too long. Please try again.'))
} else if (jqXHR.responseText.indexOf('<html') > 0) {
let respdom = $(jqXHR.responseText)
let c = respdom.filter('.container')
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
// This is a failed form validation, let's just use it
async_task_replace_page('#page-wrapper', respdom.find('#page-wrapper').html())
} else {
async_task_replace_page('body', jqXHR.responseText.substring(
jqXHR.responseText.indexOf('<body'),
jqXHR.responseText.indexOf('</body')
))
document.dispatchEvent(new Event('pretix:async-task-error'))
}
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
ajaxErrDialog.show(c.first().html())
} else {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
}
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
} else {
alert(gettext('We currently cannot reach the server. Please try again. '
+ 'Error code: {code}').replace(/\{code\}/, jqXHR.status))
}
}
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
ajaxErrDialog.show(c.first().html());
} else {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
}
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else {
alert(gettext('We currently cannot reach the server. Please try again. ' +
'Error code: {code}').replace(/\{code\}/, jqXHR.status));
}
}
}
$(function () {
'use strict'
$('body').on('submit', 'form[data-asynctask]', function (e) {
// Not supported on IE, may lead to wrong results, but we don't support IE in the backend anymore
let submitter = e.originalEvent ? e.originalEvent.submitter : null
"use strict";
$("body").on('submit', 'form[data-asynctask]', function (e) {
// Not supported on IE, may lead to wrong results, but we don't support IE in the backend anymore
var submitter = e.originalEvent ? e.originalEvent.submitter : null;
if (submitter && submitter.hasAttribute('data-no-asynctask')) {
return
}
if (submitter && submitter.hasAttribute("data-no-asynctask")) {
return;
}
e.preventDefault()
$(this).removeClass('dirty') // Avoid problems with are-you-sure.js
if ($('body').data('ajaxing')) {
return
}
async_task_id = null
async_task_is_download = $(this).is('[data-asynctask-download]')
async_task_dont_redirect = $(this).is('[data-asynctask-no-redirect]')
async_task_is_long = $(this).is('[data-asynctask-long]')
async_task_old_url = location.href
$('body').data('ajaxing', true)
waitingDialog.show(
$(this).attr('data-asynctask-headline') || gettext('We are processing your request …'),
$(this).attr('data-asynctask-text') || '',
gettext(
'We are currently sending your request to the server. If this takes longer '
+ 'than one minute, please check your internet connection and then reload '
+ 'this page and try again.'
)
)
e.preventDefault();
$(this).removeClass("dirty"); // Avoid problems with are-you-sure.js
if ($("body").data('ajaxing')) {
return;
}
async_task_id = null;
async_task_is_download = $(this).is("[data-asynctask-download]");
async_task_dont_redirect = $(this).is("[data-asynctask-no-redirect]");
async_task_is_long = $(this).is("[data-asynctask-long]");
async_task_old_url = location.href;
$("body").data('ajaxing', true);
waitingDialog.show(
$(this).attr("data-asynctask-headline") || gettext('We are processing your request …'),
$(this).attr("data-asynctask-text") || '',
gettext(
'We are currently sending your request to the server. If this takes longer ' +
'than one minute, please check your internet connection and then reload ' +
'this page and try again.'
)
);
let action = this.action
let formData = new FormData(this)
formData.append('ajax', '1')
if (async_task_dont_redirect) {
formData.append('ajax_dont_redirect', '1')
}
if (submitter && submitter.name) {
formData.append(submitter.name, submitter.value)
}
if (submitter && submitter.getAttribute('formaction')) {
action = submitter.getAttribute('formaction')
}
$.ajax(
{
type: 'POST',
url: action,
data: formData,
processData: false,
contentType: false,
success: async_task_callback,
error: async_task_error,
context: this,
dataType: 'json',
timeout: 60000,
}
)
})
var action = this.action;
var formData = new FormData(this);
formData.append('ajax', '1');
if (async_task_dont_redirect) {
formData.append('ajax_dont_redirect', '1');
}
if (submitter && submitter.name) {
formData.append(submitter.name, submitter.value);
}
if (submitter && submitter.getAttribute("formaction")) {
action = submitter.getAttribute("formaction");
}
$.ajax(
{
'type': 'POST',
'url': action,
'data': formData,
processData: false,
contentType: false,
'success': async_task_callback,
'error': async_task_error,
'context': this,
'dataType': 'json',
'timeout': 60000,
}
);
});
window.addEventListener('pageshow', function (evt) {
// In Safari, if you submit an async task, then get redirected, then go back,
// Safari won't reload the HTML from disk cache but instead reuse the DOM of the
// previous request, thus not clearing the "loading" state.
if (evt.persisted && $('body').hasClass('loading')) {
setTimeout(function () {
window.location.reload()
}, 10)
}
}, false)
window.addEventListener("pageshow", function (evt) {
// In Safari, if you submit an async task, then get redirected, then go back,
// Safari won't reload the HTML from disk cache but instead reuse the DOM of the
// previous request, thus not clearing the "loading" state.
if (evt.persisted && $("body").hasClass("loading")) {
setTimeout(function () {
window.location.reload();
}, 10);
}
}, false);
$('#ajaxerr').on('click', '.ajaxerr-close', ajaxErrDialog.hide)
$('#loadingmodal').on('cancel', function () {
return false
})
$('#loadingmodal').prop('closedBy', 'none')
})
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
$("#loadingmodal").on("cancel", function() {
return false;
});
$("#loadingmodal").prop("closedBy", "none");
});
var waitingDialog = {
show: function (title, text, status) {
'use strict'
this.setTitle(title)
this.setText(text)
this.setStatus(status || gettext('If this takes longer than a few minutes, please contact us.'))
this.setProgress(null)
this.setSteps(null)
document.getElementById('loadingmodal').showModal()
},
hide: function () {
'use strict'
document.getElementById('loadingmodal').close()
},
setTitle: function (title) {
$('#loadingmodal .modal-card-title').text(title)
},
setStatus: function (statusText) {
$('#loadingmodal p.status').text(statusText)
},
setText: function (text) {
if (text)
$('#loadingmodal .modal-card-description').text(text).show()
else
$('#loadingmodal .modal-card-description').hide()
},
setProgress: function (percentage) {
if (typeof percentage === 'number') {
$('#loadingmodal .progress').show()
$('#loadingmodal .progress .progress-bar').css('width', percentage + '%')
} else {
$('#loadingmodal .progress').hide()
}
},
setSteps: function (steps) {
let $steps = $('#loadingmodal .steps')
if (steps) {
$steps.html('').show()
for (let step of steps) {
$steps.append(
$('<span>').addClass('fa fa-fw')
.toggleClass('fa-check text-success', step.done)
.toggleClass('fa-cog fa-spin text-muted', !step.done)
).append(
$('<span>').text(step.label)
).append(
$('<br>')
)
}
} else {
$steps.hide()
}
}
}
show: function (title, text, status) {
"use strict";
this.setTitle(title);
this.setText(text);
this.setStatus(status || gettext('If this takes longer than a few minutes, please contact us.'));
this.setProgress(null);
this.setSteps(null);
document.getElementById("loadingmodal").showModal();
},
hide: function () {
"use strict";
document.getElementById("loadingmodal").close();
},
setTitle: function(title) {
$("#loadingmodal .modal-card-title").text(title);
},
setStatus: function(statusText) {
$("#loadingmodal p.status").text(statusText);
},
setText: function(text) {
if (text)
$("#loadingmodal .modal-card-description").text(text).show();
else
$("#loadingmodal .modal-card-description").hide();
},
setProgress: function(percentage) {
if (typeof percentage === 'number') {
$("#loadingmodal .progress").show();
$("#loadingmodal .progress .progress-bar").css("width", percentage + "%");
} else {
$("#loadingmodal .progress").hide();
}
},
setSteps: function(steps) {
var $steps = $("#loadingmodal .steps");
if (steps) {
$steps.html("").show()
for (var step of steps) {
$steps.append(
$("<span>").addClass("fa fa-fw")
.toggleClass("fa-check text-success", step.done)
.toggleClass("fa-cog fa-spin text-muted", !step.done)
).append(
$("<span>").text(step.label)
).append(
$("<br>")
)
}
} else {
$steps.hide();
}
}
};
var ajaxErrDialog = {
show: function (c) {
'use strict'
$('#ajaxerr').html(c)
$('#ajaxerr .links').html('<a class=\'btn btn-default ajaxerr-close\'>'
+ gettext('Close message') + '</a>')
$('body').addClass('ajaxerr has-modal-dialog')
$('#ajaxerr').prop('hidden', false)
},
hide: function () {
'use strict'
$('body').removeClass('ajaxerr has-modal-dialog')
$('#ajaxerr').prop('hidden', true)
},
}
show: function (c) {
"use strict";
$("#ajaxerr").html(c);
$("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
+ gettext("Close message") + "</a>");
$("body").addClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", false);
},
hide: function () {
"use strict";
$("body").removeClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", true);
},
};
@@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('a[href^="mailto:"]').forEach(function (link) {
// Replace [at] with @ and the [dot] with . in both the href and the displayed text (if needed)
link.href = link.href.replace('[at]', '@').replaceAll('[dot]', '.')
link.textContent = link.textContent.replace('[at]', '@').replaceAll('[dot]', '.')
})
})
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href^="mailto:"]').forEach(function(link) {
// Replace [at] with @ and the [dot] with . in both the href and the displayed text (if needed)
link.href = link.href.replace('[at]', '@').replace('[dot]', '.');
link.textContent = link.textContent.replace('[at]', '@').replace('[dot]', '.');
});
});
+140 -137
View File
@@ -1,149 +1,152 @@
/*global $ */
setup_collapsible_details = function (el) {
el.find('.sneak-peek-trigger').each(function () {
let trigger = this
let button = this.querySelector('button')
let content = document.getElementById(button.getAttribute('aria-controls'))
if (content.scrollHeight < 200) {
trigger.remove()
content.classList.remove('sneak-peek-content')
return
}
content.setAttribute('aria-hidden', 'true')
content.setAttribute('inert', true)
button.setAttribute('aria-expanded', 'false')
button.addEventListener('click', function (e) {
button.setAttribute('aria-expanded', 'true')
content.setAttribute('aria-hidden', 'false')
content.removeAttribute('inert')
content.addEventListener('transitionend', function () {
content.classList.remove('sneak-peek-content')
content.style.removeProperty('height')
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
trigger.classList.add('sr-only')
}, { once: true })
content.style.height = content.scrollHeight + 'px'
el.find('.sneak-peek-trigger').each(function() {
var trigger = this;
var button = this.querySelector('button');
var content = document.getElementById(button.getAttribute('aria-controls'));
if (content.scrollHeight < 200) {
trigger.remove();
content.classList.remove('sneak-peek-content');
return;
}
content.setAttribute('aria-hidden', 'true');
content.setAttribute('inert', true);
button.setAttribute('aria-expanded', 'false');
button.addEventListener('click', function (e) {
button.setAttribute('aria-expanded', 'true');
content.setAttribute('aria-hidden', 'false');
content.removeAttribute('inert');
button.addEventListener('click', function (e) {
// this will be called by screenreader users if they kept focus on the button after expanding
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
let expanded = button.getAttribute('aria-expanded') == 'true'
button.setAttribute('aria-expanded', !expanded)
content.setAttribute('aria-hidden', expanded)
})
button.addEventListener('blur', function (e) {
// if content is visible and the user leaves the button, we can safely remove the trigger/button
if (button.getAttribute('aria-expanded') == 'true') {
trigger.remove()
}
})
}, { once: true })
content.addEventListener('transitionend', function() {
content.classList.remove('sneak-peek-content');
content.style.removeProperty('height');
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
trigger.classList.add('sr-only');
}, {once: true});
content.style.height = content.scrollHeight + 'px';
let container = this.closest('details.sneak-peek-container')
if (container) {
function removeSneekPeakWhenClosed (e) {
if (e.newState == 'closed') {
container.removeEventListener('toggle', removeSneekPeakWhenClosed)
trigger.remove()
content.removeAttribute('aria-hidden')
content.removeAttribute('inert')
content.classList.remove('sneak-peek-content')
}
}
container.addEventListener('toggle', removeSneekPeakWhenClosed)
}
})
button.addEventListener('click', function (e) {
// this will be called by screenreader users if they kept focus on the button after expanding
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
var expanded = button.getAttribute('aria-expanded') == 'true';
button.setAttribute('aria-expanded', !expanded);
content.setAttribute('aria-hidden', expanded);
});
button.addEventListener('blur', function (e) {
// if content is visible and the user leaves the button, we can safely remove the trigger/button
if (button.getAttribute('aria-expanded') == 'true') {
trigger.remove();
}
});
}, { once: true });
let isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'
el.find('details summary').click(function (e) {
if (this.tagName !== 'A' && $(e.target).closest('a').length > 0) {
return true
}
let $details = $(this).closest('details')
let isOpen = $details.prop('open')
let $detailsNotSummary = $details.children(':not(summary)')
if ($detailsNotSummary.is(':animated')) {
e.preventDefault()
return false
}
if (isOpen) {
$details.removeClass('details-open')
$detailsNotSummary.stop().show().slideUp(500, function () {
$details.prop('open', false)
})
} else {
$detailsNotSummary.stop().hide()
$details.prop('open', true)
$details.addClass('details-open')
$detailsNotSummary.slideDown()
}
e.preventDefault()
return false
}).keyup(function (event) {
if (32 == event.keyCode || (13 == event.keyCode && !isOpera)) {
// Space or Enter is pressed — trigger the `click` event on the `summary` element
// Opera already seems to trigger the `click` event when Enter is pressed
event.preventDefault()
$(this).click()
}
})
var container = this.closest('details.sneak-peek-container');
if (container) {
function removeSneekPeakWhenClosed(e) {
if (e.newState == "closed") {
container.removeEventListener("toggle", removeSneekPeakWhenClosed);
trigger.remove();
content.removeAttribute('aria-hidden');
content.removeAttribute('inert');
content.classList.remove('sneak-peek-content');
}
}
container.addEventListener("toggle", removeSneekPeakWhenClosed);
}
});
$('details').each(function () {
let $details = $(this),
$detailsSummary = $('summary', $details).first(),
$detailsNotSummary = $details.children(':not(summary)')
$details.prop('open', typeof $details.attr('open') == 'string')
if (!$details.prop('open')) {
if ($details.find('.has-error, .alert-danger').length) {
$details.addClass('details-open')
$details.prop('open', true)
} else {
$detailsNotSummary.hide()
}
} else {
$details.addClass('details-open')
}
$detailsSummary.attr({
role: 'button',
'aria-controls': $details.attr('id')
}).prop('tabIndex', 0).bind('selectstart dragstart mousedown', function () {
return false
})
})
var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
el.find("details summary").click(function (e) {
if (this.tagName !== "A" && $(e.target).closest("a").length > 0) {
return true;
}
var $details = $(this).closest("details");
var isOpen = $details.prop("open");
var $detailsNotSummary = $details.children(':not(summary)');
if ($detailsNotSummary.is(':animated')) {
e.preventDefault();
return false;
}
if (isOpen) {
$details.removeClass("details-open");
$detailsNotSummary.stop().show().slideUp(500, function () {
$details.prop("open", false);
});
} else {
$detailsNotSummary.stop().hide();
$details.prop("open", true);
$details.addClass("details-open");
$detailsNotSummary.slideDown();
}
e.preventDefault();
return false;
}).keyup(function (event) {
if (32 == event.keyCode || (13 == event.keyCode && !isOpera)) {
// Space or Enter is pressed — trigger the `click` event on the `summary` element
// Opera already seems to trigger the `click` event when Enter is pressed
event.preventDefault();
$(this).click();
}
});
el.find('article button[data-toggle=variations]').click(function (e) {
let $button = $(this)
let $details = $button.closest('article')
let $detailsNotSummary = $button.attr('aria-controls') ? $('#' + $button.attr('aria-controls')) : $('.variations', $details)
let isOpen = !$detailsNotSummary.prop('hidden')
if ($detailsNotSummary.is(':animated')) {
e.preventDefault()
return false
}
$('details').each(function () {
var $details = $(this),
$detailsSummary = $('summary', $details).first(),
$detailsNotSummary = $details.children(':not(summary)');
$details.prop('open', typeof $details.attr('open') == 'string');
if (!$details.prop('open')) {
if ($details.find(".has-error, .alert-danger").length) {
$details.addClass("details-open");
$details.prop('open', true);
} else {
$detailsNotSummary.hide();
}
} else {
$details.addClass("details-open");
}
$detailsSummary.attr({
'role': 'button',
'aria-controls': $details.attr('id')
}).prop('tabIndex', 0).bind('selectstart dragstart mousedown', function () {
return false;
});
});
let altLabel = $button.attr('data-label-alt')
$button.attr('data-label-alt', $button.text().trim())
$button.find('span').text(altLabel)
$button.attr('aria-expanded', !isOpen)
el.find("article button[data-toggle=variations]").click(function (e) {
var $button = $(this);
var $details = $button.closest("article");
var $detailsNotSummary = $button.attr("aria-controls") ? $('#' + $button.attr("aria-controls")) : $(".variations", $details);
var isOpen = !$detailsNotSummary.prop("hidden");
if ($detailsNotSummary.is(':animated')) {
e.preventDefault();
return false;
}
if (isOpen) {
$details.removeClass('details-open')
$detailsNotSummary.stop().show().slideUp(500, function () {
$detailsNotSummary.prop('hidden', true)
})
} else {
$detailsNotSummary.prop('hidden', false).stop().hide()
$details.addClass('details-open')
$detailsNotSummary.slideDown()
}
e.preventDefault()
return false
})
el.find('.variations-collapsed').prop('hidden', true)
}
var altLabel = $button.attr("data-label-alt");
$button.attr("data-label-alt", $button.text().trim());
$button.find("span").text(altLabel);
$button.attr("aria-expanded", !isOpen);
if (isOpen) {
$details.removeClass("details-open");
$detailsNotSummary.stop().show().slideUp(500, function () {
$detailsNotSummary.prop("hidden", true);
});
} else {
$detailsNotSummary.prop("hidden", false).stop().hide();
$details.addClass("details-open");
$detailsNotSummary.slideDown();
}
e.preventDefault();
return false;
});
el.find(".variations-collapsed").prop("hidden", true);
};
$(function () {
'use strict'
"use strict";
setup_collapsible_details($('body'))
})
setup_collapsible_details($("body"));
});
+10 -10
View File
@@ -1,11 +1,11 @@
['DOMContentLoaded', 'pretix:async-task-error'].forEach(function (ev) {
document.addEventListener(ev, function () {
document.querySelectorAll('#goback, #reload').forEach(function (element) {
const regularLoad = ev === 'DOMContentLoaded' && element.id === 'goback'
element.addEventListener('click', regularLoad
? () => window.history.back()
: () => window.location.reload()
)
})
})
})
document.addEventListener(ev, function () {
document.querySelectorAll('#goback, #reload').forEach(function (element) {
const regularLoad = ev === 'DOMContentLoaded' && element.id === 'goback';
element.addEventListener('click', regularLoad
? () => window.history.back()
: () => window.location.reload()
);
});
});
});
@@ -1,3 +1,3 @@
// Attempt to auto-open page in new tab. Will be ignored by most browser's popup blockers anyways, though.
let url = JSON.parse(document.getElementById('framebreak-url').innerText)
var url = JSON.parse(document.getElementById('framebreak-url').innerText)
window.open(url)
+22 -21
View File
@@ -1,29 +1,30 @@
// The actual gettext implementation is loaded asynchronously with the translation
function gettext (msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid)
}
return msgid
function gettext(msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid);
}
return msgid;
}
function ngettext (singular, plural, count) {
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
return django.ngettext(singular, plural, count)
}
return plural
function ngettext(singular, plural, count) {
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
return django.ngettext(singular, plural, count);
}
return plural;
}
function pgettext (context, msgid) {
if (typeof django !== 'undefined' && typeof django.pgettext !== 'undefined') {
return django.pgettext(context, msgid)
}
return msgid
function pgettext(context, msgid) {
if (typeof django !== 'undefined' && typeof django.pgettext !== 'undefined') {
return django.pgettext(context, msgid);
}
return msgid;
}
function interpolate (fmt, object, named) {
if (named) {
return fmt.replace(/%\(\w+\)s/g, function (match) { return String(obj[match.slice(2, -2)]) })
} else {
return fmt.replace(/%s/g, function (match) { return String(obj.shift()) })
}
function interpolate(fmt, object, named) {
if (named) {
return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
} else {
return fmt.replace(/%s/g, function(match){return String(obj.shift())});
}
}
+20 -19
View File
@@ -1,24 +1,25 @@
function i18nstring_localize (o) {
let locale = document.body.attributes['data-pretixlocale'].value
let short_locale = locale.split('-')[0]
if (o[locale])
return o[locale]
function i18nstring_localize(o) {
var locale = document.body.attributes['data-pretixlocale'].value
var short_locale = locale.split('-')[0]
if (o[locale])
return o[locale]
if (o[short_locale])
return o[short_locale]
if (o[short_locale])
return o[short_locale]
for (let k of Object.keys(o)) {
if (k.split('-')[0] === short_locale && o[k]) {
return o[k]
}
}
for (k of Object.keys(o)) {
if (k.split('-')[0] === short_locale && o[k]) {
return o[k]
}
}
if (o['en'])
return o['en']
if (o['en'])
return o['en']
for (let k of Object.keys(o)) {
if (o[k]) {
return o[k]
}
}
for (k of Object.keys(o)) {
if (o[k]) {
return o[k]
}
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ $(document).on('pretix:bind-forms', () => {
}
if (dirty) {
beforeAfterSelect.dispatchEvent(new Event('change', { bubbles: true }))
beforeAfterSelect.dispatchEvent(new Event('change', {bubbles: true}))
}
}
referenceSelect.addEventListener('change', updateBeforeOption)
@@ -1,8 +1,8 @@
const intId = window.setInterval(function () {
$.get(location.href + '?ajax=1', function (data, _status) {
if (data === '1') {
window.clearInterval(intId)
location.reload()
}
})
}, 500)
var intId = window.setInterval(function () {
$.get(location.href + '?ajax=1', function (data, status) {
if (data === "1") {
window.clearInterval(intId);
location.reload();
}
});
}, 500);
@@ -170,14 +170,13 @@ body.has-modal-dialog .container, body.has-modal-dialog #wrapper {
#lightbox-dialog {
width: fit-content;
max-width: 80%;
min-width: calc(min(24em, 90%));
min-width: 24em;
.modal-card-content {
padding: 2.5em;
}
img {
max-width: 100%;
max-height: calc(100dvh - 60px - 5em - 5em);
}
button {
+17 -17
View File
@@ -1,21 +1,21 @@
let hiddenfield = document.querySelector('input[name=origin][type=hidden]')
var hiddenfield = document.querySelector("input[name=origin][type=hidden]");
if (hiddenfield) {
hiddenfield.value = window.location.origin
hiddenfield.value = window.location.origin
}
async function runCheck () {
if (document.getElementById('good_origin')) {
if (document.getElementById('good_origin').innerText.split('').reverse().join('') !== window.location.origin) {
const _response = await fetch(document.getElementById('bad_origin_report_url').innerText.split('').reverse().join(''), {
method: 'POST',
mode: 'cors',
referrerPolicy: 'unsafe-url',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'origin=' + window.location.origin,
})
}
}
async function runCheck() {
if (document.getElementById("good_origin")) {
if (document.getElementById("good_origin").innerText.split('').reverse().join('') !== window.location.origin) {
const response = await fetch(document.getElementById("bad_origin_report_url").innerText.split('').reverse().join(''), {
method: "POST",
mode: "cors",
referrerPolicy: "unsafe-url",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: "origin=" + window.location.origin,
});
}
}
}
runCheck()
runCheck();
+95 -95
View File
@@ -1,118 +1,118 @@
let lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
;(function (exports) {
'use strict'
'use strict'
let Arr = (typeof Uint8Array !== 'undefined')
? Uint8Array
: Array
var Arr = (typeof Uint8Array !== 'undefined')
? Uint8Array
: Array
let PLUS = '+'.charCodeAt(0)
let SLASH = '/'.charCodeAt(0)
let NUMBER = '0'.charCodeAt(0)
let LOWER = 'a'.charCodeAt(0)
let UPPER = 'A'.charCodeAt(0)
let PLUS_URL_SAFE = '-'.charCodeAt(0)
let SLASH_URL_SAFE = '_'.charCodeAt(0)
var PLUS = '+'.charCodeAt(0)
var SLASH = '/'.charCodeAt(0)
var NUMBER = '0'.charCodeAt(0)
var LOWER = 'a'.charCodeAt(0)
var UPPER = 'A'.charCodeAt(0)
var PLUS_URL_SAFE = '-'.charCodeAt(0)
var SLASH_URL_SAFE = '_'.charCodeAt(0)
function decode (elt) {
let code = elt.charCodeAt(0)
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
if (code < NUMBER) return -1 // no match
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
if (code < UPPER + 26) return code - UPPER
if (code < LOWER + 26) return code - LOWER + 26
}
function decode (elt) {
var code = elt.charCodeAt(0)
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
if (code < NUMBER) return -1 // no match
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
if (code < UPPER + 26) return code - UPPER
if (code < LOWER + 26) return code - LOWER + 26
}
function b64ToByteArray (b64) {
let i, j, l, tmp, placeHolders, arr
function b64ToByteArray (b64) {
var i, j, l, tmp, placeHolders, arr
if (b64.length % 4 > 0) {
throw new Error('Invalid string. Length must be a multiple of 4')
}
if (b64.length % 4 > 0) {
throw new Error('Invalid string. Length must be a multiple of 4')
}
// the number of equal signs (place holders)
// if there are two placeholders, than the two characters before it
// represent one byte
// if there is only one, then the three characters before it represent 2 bytes
// this is just a cheap hack to not do indexOf twice
let len = b64.length
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
// the number of equal signs (place holders)
// if there are two placeholders, than the two characters before it
// represent one byte
// if there is only one, then the three characters before it represent 2 bytes
// this is just a cheap hack to not do indexOf twice
var len = b64.length
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
// base64 is 4/3 + up to two characters of the original data
arr = new Arr(b64.length * 3 / 4 - placeHolders)
// base64 is 4/3 + up to two characters of the original data
arr = new Arr(b64.length * 3 / 4 - placeHolders)
// if there are placeholders, only get up to the last complete 4 chars
l = placeHolders > 0 ? b64.length - 4 : b64.length
// if there are placeholders, only get up to the last complete 4 chars
l = placeHolders > 0 ? b64.length - 4 : b64.length
let L = 0
var L = 0
function push (v) {
arr[L++] = v
}
function push (v) {
arr[L++] = v
}
for (i = 0, j = 0; i < l; i += 4, j += 3) {
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
push((tmp & 0xFF0000) >> 16)
push((tmp & 0xFF00) >> 8)
push(tmp & 0xFF)
}
for (i = 0, j = 0; i < l; i += 4, j += 3) {
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
push((tmp & 0xFF0000) >> 16)
push((tmp & 0xFF00) >> 8)
push(tmp & 0xFF)
}
if (placeHolders === 2) {
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
push(tmp & 0xFF)
} else if (placeHolders === 1) {
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
push((tmp >> 8) & 0xFF)
push(tmp & 0xFF)
}
if (placeHolders === 2) {
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
push(tmp & 0xFF)
} else if (placeHolders === 1) {
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
push((tmp >> 8) & 0xFF)
push(tmp & 0xFF)
}
return arr
}
return arr
}
function uint8ToBase64 (uint8) {
let i
let extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
let output = ''
let temp, length
function uint8ToBase64 (uint8) {
var i
var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
var output = ''
var temp, length
function encode (num) {
return lookup.charAt(num)
}
function encode (num) {
return lookup.charAt(num)
}
function tripletToBase64 (num) {
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
}
function tripletToBase64 (num) {
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
}
// go through the array every three bytes, we'll deal with trailing stuff later
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
output += tripletToBase64(temp)
}
// go through the array every three bytes, we'll deal with trailing stuff later
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
output += tripletToBase64(temp)
}
// pad the end with zeros, but make sure to not forget the extra bytes
switch (extraBytes) {
case 1:
temp = uint8[uint8.length - 1]
output += encode(temp >> 2)
output += encode((temp << 4) & 0x3F)
output += '=='
break
case 2:
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
output += encode(temp >> 10)
output += encode((temp >> 4) & 0x3F)
output += encode((temp << 2) & 0x3F)
output += '='
break
default:
break
}
// pad the end with zeros, but make sure to not forget the extra bytes
switch (extraBytes) {
case 1:
temp = uint8[uint8.length - 1]
output += encode(temp >> 2)
output += encode((temp << 4) & 0x3F)
output += '=='
break
case 2:
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
output += encode(temp >> 10)
output += encode((temp >> 4) & 0x3F)
output += encode((temp << 2) & 0x3F)
output += '='
break
default:
break
}
return output
}
return output
}
exports.toByteArray = b64ToByteArray
exports.fromByteArray = uint8ToBase64
exports.toByteArray = b64ToByteArray
exports.fromByteArray = uint8ToBase64
}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
+28 -28
View File
@@ -1,33 +1,33 @@
/* global gettext */
$(function () {
$('.btn-clipboard').tooltip({
trigger: 'click',
placement: 'bottom'
})
$(function() {
$('.btn-clipboard').tooltip({
trigger: 'click',
placement: 'bottom'
});
function setTooltip (btn, message) {
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show')
}
function setTooltip(btn, message) {
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show');
}
function hideTooltip (btn) {
setTimeout(function () {
$(btn).tooltip('hide')
}, 1000)
}
function hideTooltip(btn) {
setTimeout(function() {
$(btn).tooltip('hide');
}, 1000);
}
let clipboard = new Clipboard('.btn-clipboard')
var clipboard = new Clipboard('.btn-clipboard');
clipboard.on('success', function (e) {
if (e.text.length > 0) {
setTooltip(e.trigger, gettext('Copied!'))
hideTooltip(e.trigger)
}
})
clipboard.on('success', function(e) {
if (e.text.length > 0) {
setTooltip(e.trigger, gettext('Copied!'));
hideTooltip(e.trigger);
}
});
clipboard.on('error', function(e) {
setTooltip(e.trigger, gettext('Press Ctrl-C to copy!'));
hideTooltip(e.trigger);
});
});
clipboard.on('error', function (e) {
setTooltip(e.trigger, gettext('Press Ctrl-C to copy!'))
hideTooltip(e.trigger)
})
})
+65 -61
View File
@@ -8,75 +8,79 @@
* Under MIT License
* Modified by Raphael Michel
*/
;(function ($, window, document, undefined) {
let pluginName = 'metisMenu',
defaults = {
toggle: true,
}
;(function($, window, document, undefined) {
function Plugin (element, options) {
this.element = $(element)
this.settings = $.extend({}, defaults, options)
this._defaults = defaults
this._name = pluginName
this.init()
}
var pluginName = "metisMenu",
defaults = {
toggle: true,
};
Plugin.prototype = {
init: function () {
let $this = this.element,
$toggle = this.settings.toggle,
obj = this
function Plugin(element, options) {
this.element = $(element);
this.settings = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
this.init();
}
if (this.isIE() <= 9) {
$this.find('li.active').has('ul').children('ul').collapse('show')
$this.find('li').not('.active').has('ul').children('ul').collapse('hide')
} else {
$this.find('li.active').has('ul').children('ul').addClass('collapse in')
$this.find('li').not('.active').has('ul').children('ul').addClass('collapse')
}
Plugin.prototype = {
init: function() {
$this.find('li').has('ul').children('a.arrow').on('click' + '.' + pluginName, function (e) {
e.preventDefault()
$(this).blur()
var $this = this.element,
$toggle = this.settings.toggle,
obj = this;
$(this).parent('li').toggleClass('active').children('ul').collapse('toggle')
if (this.isIE() <= 9) {
$this.find("li.active").has("ul").children("ul").collapse("show");
$this.find("li").not(".active").has("ul").children("ul").collapse("hide");
} else {
$this.find("li.active").has("ul").children("ul").addClass("collapse in");
$this.find("li").not(".active").has("ul").children("ul").addClass("collapse");
}
if ($toggle) {
$(this).parent('li').siblings().removeClass('active').children('ul.in').collapse('hide')
}
})
},
$this.find("li").has("ul").children("a.arrow").on("click" + "." + pluginName, function(e) {
e.preventDefault();
$(this).blur();
isIE: function () { // https://gist.github.com/padolsey/527683
let undef,
v = 3,
div = document.createElement('div'),
all = div.getElementsByTagName('i')
$(this).parent("li").toggleClass("active").children("ul").collapse("toggle");
while (
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
all[0]
) {
return v > 4 ? v : undef
}
},
if ($toggle) {
$(this).parent("li").siblings().removeClass("active").children("ul.in").collapse("hide");
}
remove: function () {
this.element.off('.' + pluginName)
this.element.removeData(pluginName)
}
});
},
}
isIE: function() { //https://gist.github.com/padolsey/527683
var undef,
v = 3,
div = document.createElement("div"),
all = div.getElementsByTagName("i");
$.fn[pluginName] = function (options) {
this.each(function () {
let el = $(this)
if (el.data(pluginName)) {
el.data(pluginName).remove()
}
el.data(pluginName, new Plugin(this, options))
})
return this
}
})(jQuery, window, document)
while (
div.innerHTML = "<!--[if gt IE " + (++v) + "]><i></i><![endif]-->",
all[0]
) {
return v > 4 ? v : undef;
}
},
remove: function() {
this.element.off("." + pluginName);
this.element.removeData(pluginName);
}
};
$.fn[pluginName] = function(options) {
this.each(function () {
var el = $(this);
if (el.data(pluginName)) {
el.data(pluginName).remove();
}
el.data(pluginName, new Plugin(this, options));
});
return this;
};
})(jQuery, window, document);
@@ -1,34 +1,35 @@
/*global $ */
/*
Based on https://github.com/BlackrockDigital/startbootstrap-sb-admin-2
Copyright 2013-2016 Blackrock Digital LLC
MIT License
Modified by Raphael Michel
*/
// Loads the correct sidebar on window load,
// collapses the sidebar on window resize.
//Loads the correct sidebar on window load,
//collapses the sidebar on window resize.
// Sets the min-height of #page-wrapper to window size
$(window).bind('load resize', function () {
'use strict'
let topOffset = 50,
width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width
if (width < 768) {
$('div.navbar-collapse').addClass('collapse')
topOffset = 100 // 2-row-menu
} else {
$('div.navbar-collapse').removeClass('collapse')
}
$(window).bind("load resize", function () {
'use strict';
var topOffset = 50,
width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
if (width < 768) {
$('div.navbar-collapse').addClass('collapse');
topOffset = 100; // 2-row-menu
} else {
$('div.navbar-collapse').removeClass('collapse');
}
let height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1
height = height - topOffset
if (height < 1) height = 1
if (height > topOffset) {
$('#page-wrapper').css('min-height', (height) + 'px')
}
})
var height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1;
height = height - topOffset;
if (height < 1) height = 1;
if (height > topOffset) {
$("#page-wrapper").css("min-height", (height) + "px");
}
});
$(function () {
'use strict'
$('ul.nav ul.nav-second-level a.active').parent().parent().addClass('in').parent().addClass('active')
$('#side-menu').metisMenu({
toggle: false,
})
})
'use strict';
$('ul.nav ul.nav-second-level a.active').parent().parent().addClass('in').parent().addClass('active');
$('#side-menu').metisMenu({
'toggle': false,
});
});
@@ -1 +1 @@
document.forms[0].submit()
document.forms[0].submit();
@@ -1,25 +1,26 @@
/* global add_log_expand_handlers */
/*global $,gettext*/
$(function () {
if ($('div[data-lazy-id]').length == 0) {
return
}
$.getJSON('widgets.json' + ($('select[name=\'subevent\']').val() ? '?subevent=' + $('select[name=\'subevent\']').val() : ''), function (data) {
$.each(data.widgets, function (_k, v) {
$('[data-lazy-id=' + v.lazy + ']').removeClass('widget-lazy-loading')
$('[data-lazy-id=' + v.lazy + '] .widget').html(v.content)
})
})
})
if ($("div[data-lazy-id]").length == 0) {
return;
}
$.getJSON("widgets.json" + ($("select[name='subevent']").val() ? "?subevent=" + $("select[name='subevent']").val() : ""), function (data) {
$.each(data.widgets, function (k, v) {
$("[data-lazy-id=" + v.lazy + "]").removeClass("widget-lazy-loading");
$("[data-lazy-id=" + v.lazy + "] .widget").html(v.content);
});
});
});
$(function () {
if ($('#logs_target').length == 0) {
return
}
$.get('dashboard/partials/logs', function (data) {
$('#logs_target').html(data)
add_log_expand_handlers($('#logs_target'))
})
$.get('dashboard/partials/warnings', function (data) {
$('#warnings_loading').remove()
$('#warnings_target').html(data)
})
})
if ($("#logs_target").length == 0) {
return;
}
$.get("dashboard/partials/logs", function (data) {
$("#logs_target").html(data)
add_log_expand_handlers($("#logs_target"))
});
$.get("dashboard/partials/warnings", function (data) {
$("#warnings_loading").remove()
$("#warnings_target").html(data)
});
});
@@ -1,12 +1,14 @@
/*globals $, Morris, gettext, RRule, RRuleSet*/
$(function () {
let update = function () {
$.getJSON(location.href + '?ajax=true', {}, function (data) {
if (data.initialized) {
location.reload()
} else {
window.setTimeout(update, 500)
}
})
}
window.setTimeout(update, 500)
})
var update = function () {
$.getJSON(location.href + '?ajax=true', {}, function (data) {
if (data.initialized) {
location.reload();
} else {
window.setTimeout(update, 500);
}
});
};
window.setTimeout(update, 500);
});
@@ -1,95 +1,91 @@
/* global Sortable */
/*global $, Sortable*/
$(function () {
const allContainers = $('[data-dnd-url]')
function updateAllSortButtonStates () {
allContainers.each(function () {
updateSortButtonState($(this))
})
}
function updateSortButtonState (container) {
let disabledUp = container.find('.sortable-up:disabled'),
firstUp = container.find('>tr[data-dnd-id] .sortable-up').first()
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop('disabled', false)
firstUp.prop('disabled', true)
}
const allContainers = $("[data-dnd-url]");
function updateAllSortButtonStates() {
allContainers.each(function() { updateSortButtonState($(this)); });
}
function updateSortButtonState(container) {
var disabledUp = container.find(".sortable-up:disabled"),
firstUp = container.find(">tr[data-dnd-id] .sortable-up").first();
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop("disabled", false);
firstUp.prop("disabled", true);
}
let disabledDown = container.find('.sortable-down:disabled'),
lastDown = container.find('>tr[data-dnd-id] .sortable-down').last()
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop('disabled', false)
lastDown.prop('disabled', true)
}
}
var disabledDown = container.find(".sortable-down:disabled"),
lastDown = container.find(">tr[data-dnd-id] .sortable-down").last();
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop("disabled", false);
lastDown.prop("disabled", true);
}
}
let didSort = false, lastClick = 0
allContainers.each(function () {
const container = $(this),
url = container.data('dnd-url'),
handle = $('<span class="btn btn-default btn-sm dnd-sort-handle"><i class="fa fa-arrows"></i></span>')
let didSort = false, lastClick = 0;
allContainers.each(function(){
const container = $(this),
url = container.data("dnd-url"),
handle = $('<span class="btn btn-default btn-sm dnd-sort-handle"><i class="fa fa-arrows"></i></span>');
container.find('.dnd-container').append(handle)
if (!sessionStorage.dndShowMoveButtons) {
container.find('.sortable-up, .sortable-down').addClass('sr-only').on('click', function () {
sessionStorage.dndShowMoveButtons = 'true'
})
}
if (container.find('[data-dnd-id]').length < 2 && !container.data('dnd-group')) {
handle.addClass('disabled')
return
}
function maybeShowSortButtons () {
if (Date.now() - lastClick < 3000) {
$('[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down').removeClass('sr-only')
updateAllSortButtonStates()
}
lastClick = Date.now()
}
container.find('.dnd-sort-handle').on('mouseup', maybeShowSortButtons)
const group = container.data('dnd-group')
const containers = group ? container.parent().find('[data-dnd-group="' + group + '"]') : container
Sortable.create(container.get(0), {
filter: '.sortable-disabled',
handle: '.dnd-sort-handle',
group: group,
onMove: function (evt) {
return evt.related.className.indexOf('sortable-disabled') === -1
},
onStart: function () {
containers.addClass('sortable-dragarea')
container.parent().addClass('sortable-sorting')
didSort = false
},
onEnd: function () {
containers.removeClass('sortable-dragarea')
container.parent().removeClass('sortable-sorting')
if (!didSort) {
maybeShowSortButtons()
} else {
$('[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down').addClass('sr-only')
delete sessionStorage.dndShowMoveButtons
}
},
onSort: function (evt) {
if (evt.target !== evt.to) return
didSort = true
container.find(".dnd-container").append(handle);
if (!sessionStorage.dndShowMoveButtons) {
container.find(".sortable-up, .sortable-down").addClass("sr-only").on("click", function () {
sessionStorage.dndShowMoveButtons = 'true';
});
}
if (container.find("[data-dnd-id]").length < 2 && !container.data("dnd-group")) {
handle.addClass("disabled");
return;
}
function maybeShowSortButtons() {
if (Date.now() - lastClick < 3000) {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").removeClass("sr-only");
updateAllSortButtonStates();
}
lastClick = Date.now();
}
container.find(".dnd-sort-handle").on("mouseup", maybeShowSortButtons);
const group = container.data("dnd-group");
const containers = group ? container.parent().find('[data-dnd-group="' + group + '"]') : container;
Sortable.create(container.get(0), {
filter: ".sortable-disabled",
handle: ".dnd-sort-handle",
group: group,
onMove: function (evt) {
return evt.related.className.indexOf('sortable-disabled') === -1;
},
onStart: function (evt) {
containers.addClass("sortable-dragarea");
container.parent().addClass("sortable-sorting");
didSort = false;
},
onEnd: function (evt) {
containers.removeClass("sortable-dragarea");
container.parent().removeClass("sortable-sorting");
if (!didSort) {
maybeShowSortButtons();
} else {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").addClass("sr-only");
delete sessionStorage.dndShowMoveButtons;
}
},
onSort: function (evt){
if (evt.target !== evt.to) return;
didSort = true;
const ids = container.find('[data-dnd-id]').toArray().map(function (e) {
return e.dataset.dndId
})
$.ajax(
{
type: 'POST',
url: url,
headers: { 'X-CSRFToken': $('input[name=csrfmiddlewaretoken]').val() },
data: JSON.stringify({
ids: ids
}),
contentType: 'application/json',
timeout: 30000
}
)
}
})
})
})
const ids = container.find("[data-dnd-id]").toArray().map(function (e) { return e.dataset.dndId; });
$.ajax(
{
'type': 'POST',
'url': url,
'headers': {'X-CSRFToken': $("input[name=csrfmiddlewaretoken]").val()},
'data': JSON.stringify({
ids: ids
}),
'contentType': "application/json",
'timeout': 30000
}
);
}
});
});
});
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,3 @@
$(function () {
$('input, select, textarea').not(':disabled').focus()
})
$("input, select, textarea").not(":disabled").focus();
});
+182 -179
View File
@@ -1,198 +1,201 @@
/* globals $ */
/*globals $*/
$(document).on('pretix:bind-forms', function () {
function cleanup (l) {
return $.trim(l.replace(/\n/g, ', '))
}
function combine ($sel) {
let parts = [
$sel.filter('[name*=street]').val(),
$sel.filter('[name*=zipcode]').val(),
$sel.filter('[name*=city]').val(),
$sel.filter('[name*=state]').val(),
$sel.filter('[name*=country]').find('option:selected').text(),
$sel.filter('[name*=location]').val(),
]
let res = ''
for (let val of parts) {
if (val) {
if (res) {
res += ', '
}
res += val
}
}
return cleanup(res)
}
$('.geodata-section').each(function () {
// Geocoding
// detach notifications and append them to first label (should be from location)
let $notifications = $('.geodata-autoupdate', this).detach().appendTo($('label', this).first())
let $lat = $('input[name$=geo_lat]', this).first()
let $lon = $('input[name$=geo_lon]', this).first()
let lat
let lon
let $updateButton = $('[data-action=update]', this)
$(document).on("pretix:bind-forms", function () {
function cleanup(l) {
return $.trim(l.replace(/\n/g, ", "));
}
function combine($sel) {
var parts = [
$sel.filter("[name*=street]").val(),
$sel.filter("[name*=zipcode]").val(),
$sel.filter("[name*=city]").val(),
$sel.filter("[name*=state]").val(),
$sel.filter("[name*=country]").find("option:selected").text(),
$sel.filter("[name*=location]").val(),
]
var res = "";
for (var val of parts) {
if (val) {
if (res) {
res += ", "
}
res += val
}
}
return cleanup(res)
}
$(".geodata-section").each(function () {
// Geocoding
// detach notifications and append them to first label (should be from location)
var $notifications = $(".geodata-autoupdate", this).detach().appendTo($("label", this).first());
var $lat = $("input[name$=geo_lat]", this).first();
var $lon = $("input[name$=geo_lon]", this).first();
var lat;
var lon;
var $updateButton = $("[data-action=update]", this);
let $location
// The .geodata-section is expected to include either...
// ... an English "location" field
if ($('textarea[lang=en], input[lang=en]', this).length) {
$location = $('textarea[lang=en], input[lang=en], select', this).not('[name*=geo_]')
}
var $location;
// The .geodata-section is expected to include either...
// ... an English "location" field
if ($("textarea[lang=en], input[lang=en]", this).length) {
$location = $("textarea[lang=en], input[lang=en], select", this).not("[name*=geo_]");
}
// ... a "location" field in any other language
if (!$location || !$location.length) {
let lang = $('textarea, input[type=text]', this).not('[name*=geo_]').first().attr('lang')
if (lang) {
$location = $('textarea[lang=' + lang + '], input[lang=' + lang + '], select', this)
}
}
// ... a "location" field in any other language
if (!$location || !$location.length) {
var lang = $("textarea, input[type=text]", this).not("[name*=geo_]").first().attr("lang");
if (lang) {
$location = $("textarea[lang=" + lang + "], input[lang=" + lang + "], select", this);
}
}
// ... or a set of fields like a full address form
if (!$location || !$location.length) {
$location = $('textarea, input, select', this).not('[name*=geo_]')
}
// ... or a set of fields like a full address form
if (!$location || !$location.length) {
$location = $("textarea, input, select", this).not("[name*=geo_]");
}
if (!$lat.length || !$lon.length || !$location.length) {
return
}
if (!$lat.length || !$lon.length || !$location.length) {
return;
}
let debounceLoad, debounceLatLonChange, delayUpdateDismissal
let touched = $lat.val() !== ''
let xhr
let lastLocation = combine($location)
var debounceLoad, debounceLatLonChange, delayUpdateDismissal;
var touched = $lat.val() !== "";
var xhr;
var lastLocation = combine($location);
function load () {
window.clearTimeout(debounceLoad)
if (xhr) {
xhr.abort()
xhr = null
}
function load() {
window.clearTimeout(debounceLoad);
if (xhr) {
xhr.abort();
xhr = null;
}
let q = combine($location)
if (q === '' || q === lastLocation) return
var q = combine($location);
if (q === "" || q === lastLocation) return;
lastLocation = q
$notifications.attr('data-notify', 'loading')
lastLocation = q;
$notifications.attr("data-notify", "loading");
xhr = $.getJSON('/control/geocode/?q=' + encodeURIComponent(q), function (res) {
if (!res.results || !res.results.length) {
$notifications.attr('data-notify', 'error')
return
}
xhr = $.getJSON('/control/geocode/?q=' + encodeURIComponent(q), function (res) {
if (!res.results || !res.results.length) {
$notifications.attr("data-notify", "error");
return;
}
lat = res.results[0].lat
lon = res.results[0].lon
if ($lat.val() == lat && $lon.val() == lon) {
$notifications.attr('data-notify', '')
} else if (touched) {
$notifications.attr('data-notify', 'confirm')
} else {
$notifications.attr('data-notify', '')
$lat.val(lat)
$lon.val(lon)
center(13)
}
})
}
lat = res.results[0].lat;
lon = res.results[0].lon;
if ($lat.val() == lat && $lon.val() == lon) {
$notifications.attr("data-notify", "");
}
else if (touched) {
$notifications.attr("data-notify", "confirm");
}
else {
$notifications.attr("data-notify", "");
$lat.val(lat);
$lon.val(lon);
center(13);
}
})
}
$lat.add($lon).change(function () {
if (this.value !== '') touched = true
center(13)
}).keyup(function () {
window.clearTimeout(debounceLatLonChange)
debounceLatLonChange = window.setTimeout(center, 300)
})
$lat.add($lon).change(function () {
if (this.value !== "") touched = true;
center(13);
}).keyup(function () {
window.clearTimeout(debounceLatLonChange);
debounceLatLonChange = window.setTimeout(center, 300);
});
$location.change(load)
$location.keyup(function () {
window.clearTimeout(debounceLoad)
debounceLoad = window.setTimeout(load, 1000)
if ($notifications.attr('data-notify') == 'confirm' && lastLocation !== cleanup(this.value)) $notifications.attr('data-notify', '')
})
$location.change(load);
$location.keyup(function () {
window.clearTimeout(debounceLoad);
debounceLoad = window.setTimeout(load, 1000);
if ($notifications.attr("data-notify") == "confirm" && lastLocation !== cleanup(this.value)) $notifications.attr("data-notify", "");
});
$updateButton.click(function () {
$lat.val(lat)
$lon.val(lon).trigger('change')// change-event is needed by bulk-edit
touched = false
center(13)
$notifications.attr('data-notify', 'updated')
delayUpdateDismissal = window.setTimeout(function () {
if ($notifications.attr('data-notify') == 'updated') $notifications.attr('data-notify', '')
}, 2500)
})
$updateButton.click(function() {
$lat.val(lat);
$lon.val(lon).trigger("change");// change-event is needed by bulk-edit
touched = false;
center(13);
$notifications.attr("data-notify", "updated");
delayUpdateDismissal = window.setTimeout(function() {
if ($notifications.attr("data-notify") == "updated") $notifications.attr("data-notify", "");
}, 2500);
});
// Map
let $grp = $('.geodata-group', this)
let tiles = $grp.attr('data-tiles')
let attrib = $grp.attr('data-attrib')
if (tiles) {
let $map = $('<div>')
$grp.append($('<div>').addClass('col-md-9 col-md-offset-3').append($map))
let map = L.map($map.get(0))
L.tileLayer(tiles, {
attribution: attrib,
maxZoom: 18,
}).addTo(map)
// Map
var $grp = $(".geodata-group", this);
var tiles = $grp.attr("data-tiles");
var attrib = $grp.attr("data-attrib");
if (tiles) {
var $map = $("<div>");
$grp.append($("<div>").addClass("col-md-9 col-md-offset-3").append($map));
var map = L.map($map.get(0));
L.tileLayer(tiles, {
attribution: attrib,
maxZoom: 18,
}).addTo(map);
function getpoint () {
if ($lat.val() !== '' && $lon.val() !== '') {
let p = [parseFloat($lat.val().replace(',', '.')), parseFloat($lon.val().replace(',', '.'))]
// Clip to valid ranges. Very invalid lon/lat values can even lead to browser crashes in leaflet apparently
if (p[0] < -90) p[0] = -90
if (p[0] > 90) p[0] = 90
if (p[1] < -180) p[1] = -180
if (p[1] > 180) p[1] = 180
return p
} else {
return [0.0, 0.0]
}
}
function getpoint() {
if ($lat.val() !== "" && $lon.val() !== "") {
var p = [parseFloat($lat.val().replace(",", ".")), parseFloat($lon.val().replace(",", "."))];
// Clip to valid ranges. Very invalid lon/lat values can even lead to browser crashes in leaflet apparently
if (p[0] < -90) p[0] = -90
if (p[0] > 90) p[0] = 90
if (p[1] < -180) p[1] = -180
if (p[1] > 180) p[1] = 180
return p
} else {
return [0.0, 0.0];
}
}
let marker = L.marker(getpoint(), {
draggable: 'true',
icon: L.icon({
iconUrl: $grp.attr('data-icon'),
shadowUrl: $grp.attr('data-shadow'),
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
})
})
marker.addTo(map)
marker.on('dragend', function (event) {
let position = marker.getLatLng()
marker.setLatLng(position, {
draggable: 'true'
}).bindPopup(position).update()
$lat.val(position.lat.toFixed(7))
$lon.val(position.lng.toFixed(7))
touched = true
center(null)
})
var marker = L.marker(getpoint(), {
draggable: 'true',
icon: L.icon({
iconUrl: $grp.attr("data-icon"),
shadowUrl: $grp.attr("data-shadow"),
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
})
});
marker.addTo(map);
marker.on("dragend", function (event) {
var position = marker.getLatLng();
marker.setLatLng(position, {
draggable: 'true'
}).bindPopup(position).update();
$lat.val(position.lat.toFixed(7));
$lon.val(position.lng.toFixed(7));
touched = true;
center(null);
});
function center (zoom) {
if ($lat.val() !== '' && $lon.val() !== '') {
if (zoom) {
map.setView(getpoint(), zoom)
} else {
map.panTo(getpoint())
}
marker.setLatLng(getpoint(), {
draggable: 'true'
}).bindPopup(getpoint()).update()
} else {
map.fitWorld()
}
}
function center(zoom) {
if ($lat.val() !== "" && $lon.val() !== "") {
if (zoom) {
map.setView(getpoint(), zoom);
} else {
map.panTo(getpoint());
}
marker.setLatLng(getpoint(), {
draggable: 'true'
}).bindPopup(getpoint()).update();
} else {
map.fitWorld();
}
}
center(13)
} else {
function center (zoom) {
}
}
})
})
center(13);
} else {
function center(zoom) {
}
}
});
});
@@ -1,39 +1,39 @@
$(function () {
hideDeselected(false)
hideDeselected(false);
function hideDeselected (animate) {
let v = $('input[name=\'quota_option\']:checked').val(),
fn = animate ? 'slideDown' : 'show'
if (v === 'existing') {
hideAll(animate)
$('#existing-quota-group').children()[fn]()
} else if (v === 'new') {
hideAll(animate)
if ($('#id_quota_add_new_name').val() === '') {
$('#id_quota_add_new_name').val($('input[name^=name_]').first().val())
}
$('#new-quota-group').children()[fn]()
} else {
hideAll(animate)
}
}
function hideDeselected(animate) {
var v = $("input[name='quota_option']:checked").val(),
fn = animate ? 'slideDown' : 'show';
if (v === "existing") {
hideAll(animate);
$("#existing-quota-group").children()[fn]();
} else if (v === "new") {
hideAll(animate);
if ($("#id_quota_add_new_name").val() === "") {
$("#id_quota_add_new_name").val($("input[name^=name_]").first().val());
}
$("#new-quota-group").children()[fn]();
} else {
hideAll(animate);
}
}
function hideAll (animate) {
let fn = animate ? 'slideUp' : 'hide'
$('#new-quota-group').children()[fn]()
$('#existing-quota-group').children()[fn]()
}
function hideAll(animate) {
var fn = animate ? 'slideUp' : 'hide';
$("#new-quota-group").children()[fn]();
$("#existing-quota-group").children()[fn]();
}
$('input[name=\'quota_option\']').on('change',
function () {
hideDeselected(true)
}
)
$("input[name='quota_option']").on('change',
function () {
hideDeselected(true);
}
);
function toggleblock () {
$('#new-quota-group').closest('fieldset').toggle(!$('[name=has_variations][value=on]').prop('checked'))
}
function toggleblock() {
$("#new-quota-group").closest('fieldset').toggle(!$("[name=has_variations][value=on]").prop('checked'));
}
$('[name=has_variations]').change(toggleblock)
toggleblock()
})
$("[name=has_variations]").change(toggleblock);
toggleblock();
});
+65 -65
View File
@@ -1,75 +1,75 @@
/* global gettext */
function preview_task_callback (data, _jqXHR, _status) {
'use strict'
if (data.item) {
$('#' + data.item + '_panel').data('ajaxing', false)
for (let m in data.msgs) {
let target = $('div[for=' + data.item + '][lang=' + m + ']')
if (target.length === 1) {
target.html(data.msgs[m])
target.find('.placeholder').tooltip()
}
}
}
function preview_task_callback(data, jqXHR, status) {
"use strict";
if (data.item) {
$('#' + data.item + '_panel').data('ajaxing', false);
for (var m in data.msgs){
var target = $('div[for=' + data.item + '][lang=' + m +']');
if (target.length === 1){
target.html(data.msgs[m]);
target.find('.placeholder').tooltip();
}
}
}
}
function preview_task_error (item) {
'use strict'
return function (jqXHR, textStatus, _errorThrown) {
$('#' + item + '_panel').data('ajaxing', false)
$('#' + item + '_preview div').text(gettext('An error has occurred.'))
if (textStatus === 'timeout') {
alert(gettext('The request took too long. Please try again.'))
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
} else {
alert(gettext('We currently cannot reach the server. Please try again. '
+ 'Error code: {code}').replace(/\{code\}/, jqXHR.status))
}
}
}
function preview_task_error(item) {
"use strict";
return function(jqXHR, textStatus, errorThrown) {
$('#' + item + '_panel').data('ajaxing', false);
$('#' + item + '_preview div').text(gettext('An error has occurred.'));
if (textStatus === "timeout") {
alert(gettext("The request took too long. Please try again."));
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else {
alert(gettext('We currently cannot reach the server. Please try again. ' +
'Error code: {code}').replace(/\{code\}/, jqXHR.status));
}
}
}
}
function mail_preview_setup ($el) {
$el.find('.mail-preview .placeholder').tooltip()
$el.find('a[type=preview]').on('click', function () {
let itemName = $(this).closest('.preview-panel').attr('for')
if ($('#' + itemName + '_panel').data('ajaxing') || $(this).parent('.active').length !== 0) {
return
}
function mail_preview_setup($el) {
$el.find('.mail-preview .placeholder').tooltip();
$el.find('a[type=preview]').on('click', function () {
var itemName = $(this).closest('.preview-panel').attr('for');
if ($('#' + itemName + '_panel').data('ajaxing') || $(this).parent('.active').length !== 0) {
return;
}
// gathering data
let parentForm = $(this).closest('form')
let previewUrl = $(parentForm).attr('mail-preview-url')
let token = $(parentForm).find('input[name=csrfmiddlewaretoken]').val()
let dataString = 'item=' + itemName + '&csrfmiddlewaretoken=' + token
$('#' + itemName + '_edit textarea, #' + itemName + '_edit input').each(function () {
dataString += '&' + $(this).serialize()
})
// gathering data
var parentForm = $(this).closest('form');
var previewUrl = $(parentForm).attr('mail-preview-url');
var token = $(parentForm).find('input[name=csrfmiddlewaretoken]').val();
var dataString = 'item=' + itemName + '&csrfmiddlewaretoken=' + token;
$('#' + itemName + '_edit textarea, #' + itemName + '_edit input').each(function () {
dataString += '&' + $(this).serialize();
});
// prepare for ajax
$('#' + itemName + '_panel').data('ajaxing', true)
$('#' + itemName + '_preview div').text(gettext('Generating messages …'))
// prepare for ajax
$('#' + itemName + '_panel').data('ajaxing', true);
$('#' + itemName + '_preview div').text(gettext('Generating messages …'));
$.ajax(
{
type: 'POST',
url: previewUrl,
data: dataString,
success: preview_task_callback,
error: preview_task_error(itemName),
dataType: 'json',
timeout: 60000,
}
)
})
$.ajax(
{
'type': 'POST',
'url': previewUrl,
'data': dataString,
'success': preview_task_callback,
'error': preview_task_error(itemName),
'dataType': 'json',
'timeout': 60000,
}
);
});
}
$(function () {
'use strict'
mail_preview_setup($('body'))
$(document).on('pretix:bind-forms', function () {
mail_preview_setup($('body'))
})
})
"use strict";
mail_preview_setup($("body"));
$(document).on("pretix:bind-forms", function () {
mail_preview_setup($("body"));
});
});
File diff suppressed because it is too large Load Diff
@@ -1,62 +1,59 @@
/* global gettext */
/*global $, gettext*/
$(function () {
if (!$('.form-order-change').length) {
return
}
$('.form-order-change').each(function () {
let url = $(this).attr('data-pricecalc-endpoint')
let $itemvar = $(this).find('[name*=itemvar]')
let $subevent = $(this).find('[name*=subevent]')
let $tax_rule = $(this).find('[name*=tax_rule]')
let $price = $(this).find('[name*=price]')
let update_price = function () {
console.log(url)
let itemvar = $itemvar.val()
let item
let variation = null
if (itemvar.indexOf('-')) {
item = parseInt(itemvar.split('-')[0])
variation = parseInt(itemvar.split('-')[1])
} else {
item = parseInt(itemvar)
}
$price.closest('.field-container').append('<small class="loading-indicator"><span class="fa fa-cog fa-spin"></span> '
+ gettext('Calculating default price…') + '</small>')
$.ajax(
{
type: 'POST',
url: url,
headers: { 'X-CSRFToken': $('input[name=csrfmiddlewaretoken]').val() },
data: JSON.stringify({
item: item,
variation: variation,
subevent: $subevent.val(),
tax_rule: $tax_rule.val(),
locale: $('body').attr('data-pretixlocale'),
}),
contentType: 'application/json',
success: function (data) {
$price.val(data.gross_formatted)
$tax_rule.val(data.tax_rule)
$price.closest('.field-container').find('.loading-indicator').remove()
},
// 'error': …
context: this,
dataType: 'json',
timeout: 30000
}
)
}
$itemvar.on('change', function () {
$tax_rule.val(null)
update_price()
})
$tax_rule.on('change', update_price)
$subevent.on('change', update_price).on('change', function () {
let seat = $(this).closest('.form-order-change').find('[id$=seat]')
if (seat.length) {
seat.prop('required', !!$subevent.val())
}
})
})
})
if (!$(".form-order-change").length) {
return;
}
$(".form-order-change").each(function () {
var url = $(this).attr("data-pricecalc-endpoint");
var $itemvar = $(this).find("[name*=itemvar]");
var $subevent = $(this).find("[name*=subevent]");
var $tax_rule = $(this).find("[name*=tax_rule]");
var $price = $(this).find("[name*=price]");
var update_price = function () {
console.log(url);
var itemvar = $itemvar.val();
var item = null;
var variation = null;
if (itemvar.indexOf("-")) {
item = parseInt(itemvar.split("-")[0]);
variation = parseInt(itemvar.split("-")[1]);
} else {
item = parseInt(itemvar);
}
$price.closest(".field-container").append("<small class=\"loading-indicator\"><span class=\"fa fa-cog fa-spin\"></span> " +
gettext("Calculating default price…") + "</small>");
$.ajax(
{
'type': 'POST',
'url': url,
'headers': {'X-CSRFToken': $("input[name=csrfmiddlewaretoken]").val()},
'data': JSON.stringify({
'item': item,
'variation': variation,
'subevent': $subevent.val(),
'tax_rule': $tax_rule.val(),
'locale': $("body").attr("data-pretixlocale"),
}),
'contentType': "application/json",
'success': function (data) {
$price.val(data.gross_formatted);
$tax_rule.val(data.tax_rule);
$price.closest(".field-container").find(".loading-indicator").remove();
},
// 'error': …
'context': this,
'dataType': 'json',
'timeout': 30000
}
);
};
$itemvar.on("change", function () { $tax_rule.val(null); update_price() });
$tax_rule.on("change", update_price);
$subevent.on("change", update_price).on("change", function () {
var seat = $(this).closest(".form-order-change").find("[id$=seat]");
if (seat.length) {
seat.prop("required", !!$subevent.val());
}
});
});
});
@@ -1,43 +1,43 @@
function is_sandbox_supported () {
const iframe = document.createElement('iframe')
return 'sandbox' in iframe
function is_sandbox_supported() {
const iframe = document.createElement('iframe');
return 'sandbox' in iframe;
}
function safe_render (url, parent) {
// Estimate the height that prevents the user from having to scroll on two levels to see the full email
const height = (
Math.max(400, window.innerHeight - parent.parent().get(0).getBoundingClientRect().top - document.querySelector('footer').getBoundingClientRect().height - 20)
) + 'px'
function safe_render(url, parent) {
// Estimate the height that prevents the user from having to scroll on two levels to see the full email
const height = (
Math.max(400, window.innerHeight - parent.parent().get(0).getBoundingClientRect().top - document.querySelector("footer").getBoundingClientRect().height - 20)
) + "px";
const iframe = (
// Per the HTML spec, a data: URL in an iframe is treated as its own origin:
// https://github.com/whatwg/html/pull/1756
// It is unclear, if Firefox complies, and the behaviour around data URLs is quite wild:
// https://github.com/whatwg/html/issues/12091
// Together with the sandbox attribute disallowing all JavaScript, and the fact
// that we sanitize the HTML before we even save it to the database, this should
// still be the safest way to render HTML in the context of our backend.
$('<iframe>')
.height(height)
.attr('class', 'html-email')
.attr('src', url)
.attr('sandbox', 'allow-popups allow-popups-to-escape-sandbox')
.attr('csp', 'script-src \'none\'; font-src \'none\'; connect-src \'none\'; form-action \'none\'; style-src \'unsafe-inline\'') // respected only by chrome
.prop('credentialless', true) // respected only by chrome
)
const iframe = (
// Per the HTML spec, a data: URL in an iframe is treated as its own origin:
// https://github.com/whatwg/html/pull/1756
// It is unclear, if Firefox complies, and the behaviour around data URLs is quite wild:
// https://github.com/whatwg/html/issues/12091
// Together with the sandbox attribute disallowing all JavaScript, and the fact
// that we sanitize the HTML before we even save it to the database, this should
// still be the safest way to render HTML in the context of our backend.
$("<iframe>")
.height(height)
.attr("class", "html-email")
.attr("src", url)
.attr("sandbox", "allow-popups allow-popups-to-escape-sandbox")
.attr("csp", "script-src 'none'; font-src 'none'; connect-src 'none'; form-action 'none'; style-src 'unsafe-inline'") // respected only by chrome
.prop("credentialless", true) // respected only by chrome
);
console.log(parent, iframe)
parent.append(iframe)
console.log(parent, iframe);
parent.append(iframe);
}
$(function () {
const script_element = $('#mail_body_html')
if (!script_element.length) return
if (!is_sandbox_supported()) {
// Browser is too old for <iframe sandbox>
$(script_element.parent()).text('Please switch to a modern browser to view HTML content safely.')
return
}
const script_element = $("#mail_body_html");
if (!script_element.length) return;
if (!is_sandbox_supported()) {
// Browser is too old for <iframe sandbox>
$(script_element.parent()).text("Please switch to a modern browser to view HTML content safely.");
return;
}
safe_render(JSON.parse(script_element.html()), script_element.parent())
})
safe_render(JSON.parse(script_element.html()), script_element.parent());
});
@@ -1,89 +1,84 @@
/* global gettext */
$(function () {
let plugins = $('.plugin-container').toArray().map(function (el) {
return {
sortName: el.getAttribute('data-plugin-name').toLowerCase().replace(/pretix /g, ''),
name: el.getAttribute('data-plugin-name').toLowerCase(),
module: el.getAttribute('data-plugin-module').toLowerCase(),
description: $(el).find('.plugin-description').text().toLowerCase(),
html: el.outerHTML,
category: $(el).closest('[data-plugin-category]').attr('data-plugin-category'),
categoryLabel: $(el).closest('[data-plugin-category]').attr('data-plugin-category-label'),
active: !!$(el).has('[data-is-active]').length,
}
})
function SearchMatcher (term, fields) {
this.searchFor = term.toLowerCase().split(/\s+/)
this.fields = fields
}
function inStringRanked (haystack, needle) {
let pos = -1, rank = 0
do {
pos = haystack.indexOf(needle, pos + 1)
if (pos !== -1) rank = 10
if (pos === 0 || haystack.charCodeAt(pos - 1) <= 47)
return 15 // string start or word start (=char before match is special char)
} while (pos !== -1)
return rank
}
SearchMatcher.prototype.isMatch = function (obj) {
let rank = 0
for (let j = 0; j < this.searchFor.length; j++) {
let searchFor = this.searchFor[j]
for (let i = this.fields.length - 1; i >= 0; i--) {
let result = inStringRanked(obj[this.fields[i]], searchFor)
if (result) {
rank += (i + 1) * result
break
}
}
}
return rank
}
function strcmp (a, b) {
return a > b ? 1 : a < b ? -1 : 0
}
let $results_box = $('#plugin_search_results')
let $plugin_tabs = $('#plugin_tabs')
let $results = $('#plugin_search_results .plugin-list')
function search () {
$results.html('')
let value = $('#plugin_search_input').val()
let only_active = $('input[name=plugin_state_filter][value=active]').prop('checked')
if (!value && !only_active) {
$results_box.hide()
$plugin_tabs.show()
return
}
$results_box.show()
$plugin_tabs.hide()
let matcher = new SearchMatcher(value, ['description', 'module', 'name'])
let matches = []
for (const plugin of plugins) {
if (only_active && !plugin.active)
continue
let rank = matcher.isMatch(plugin)
if (!rank)
continue
matches.push([rank, plugin])
}
matches.sort(function (a, b) { return (b[0] - a[0]) || strcmp(a[1].sortName, b[1].sortName) })
$results.append(matches.map(function (res) { return $(res[1].html).prepend('<span class="pull-right">' + res[1].categoryLabel + '</span>') }))
$results.find('.panel-body, .panel, .featured-plugin, .btn-lg').removeClass('panel-body panel featured-plugin btn-lg')
if (matches.length === 0) {
$results.append(gettext('No results'))
}
}
$('#plugin_search_input').on('input', search)
$('input[name=plugin_state_filter]').on('change', search)
$results_box.find('button.close').on('click', function () {
$('input[name=plugin_state_filter][value=all]').prop('checked', true).trigger('click')
$('#plugin_search_input').val('').trigger('input')
})
if (location.search) {
var search = new URLSearchParams(location.search)
if (search.has('q')) {
$('#plugin_search_input').val(search.get('q')).trigger('input')
}
}
$(function() {
var plugins = $(".plugin-container").toArray().map(function(el) {
return {
sortName: el.getAttribute('data-plugin-name').toLowerCase().replace(/pretix /g, ''),
name: el.getAttribute('data-plugin-name').toLowerCase(),
module: el.getAttribute('data-plugin-module').toLowerCase(),
description: $(el).find('.plugin-description').text().toLowerCase(),
html: el.outerHTML,
category: $(el).closest('[data-plugin-category]').attr('data-plugin-category'),
categoryLabel: $(el).closest('[data-plugin-category]').attr('data-plugin-category-label'),
active: !!$(el).has('[data-is-active]').length,
}
});
function SearchMatcher(term, fields) {
this.searchFor = term.toLowerCase().split(/\s+/);
this.fields = fields;
}
function inStringRanked(haystack, needle) {
let pos = -1, rank = 0;
do {
pos = haystack.indexOf(needle, pos + 1);
if (pos !== -1) rank = 10;
if (pos === 0 || haystack.charCodeAt(pos - 1) <= 47)
return 15; // string start or word start (=char before match is special char)
} while (pos !== -1);
return rank;
}
SearchMatcher.prototype.isMatch = function(obj) {
let rank = 0;
for(let j = 0; j < this.searchFor.length; j++) {
var searchFor = this.searchFor[j];
for(let i = this.fields.length - 1; i >= 0; i--) {
var result = inStringRanked(obj[this.fields[i]], searchFor);
if (result) {
rank += (i + 1) * result;
break;
}
}
}
return rank;
}
function strcmp(a, b) {
return a > b ? 1 : a < b ? -1 : 0;
}
var $results_box = $("#plugin_search_results");
var $plugin_tabs = $("#plugin_tabs");
var $results = $("#plugin_search_results .plugin-list");
function search() {
$results.html("");
var value = $("#plugin_search_input").val();
var only_active = $("input[name=plugin_state_filter][value=active]").prop("checked");
if (!value && !only_active) {
$results_box.hide(); $plugin_tabs.show();
return;
}
$results_box.show(); $plugin_tabs.hide();
var matcher = new SearchMatcher(value, ["description", "module", "name"]);
var matches = [];
for(const plugin of plugins) {
if (only_active && !plugin.active) continue;
var rank = matcher.isMatch(plugin);
if (!rank) continue;
matches.push([rank, plugin]);
}
matches.sort(function (a,b) { return (b[0]-a[0]) || strcmp(a[1].sortName, b[1].sortName); })
$results.append(matches.map(function(res) { return $(res[1].html).prepend('<span class="pull-right">' + res[1].categoryLabel + '</span>'); }))
$results.find(".panel-body, .panel, .featured-plugin, .btn-lg").removeClass("panel-body panel featured-plugin btn-lg");
if (matches.length === 0) {
$results.append(gettext("No results"));
}
}
$("#plugin_search_input").on("input", search);
$("input[name=plugin_state_filter]").on("change", search);
$results_box.find("button.close").on("click", function() {
$("input[name=plugin_state_filter][value=all]").prop("checked", true).trigger("click");
$("#plugin_search_input").val("").trigger("input");
});
if (location.search) {
var search = new URLSearchParams(location.search);
if (search.has('q')) {
$("#plugin_search_input").val(search.get("q")).trigger("input");
}
}
})
+137 -137
View File
@@ -1,156 +1,156 @@
/* global Morris, gettext, apiGET, i18nToString */
/*global $, Morris, gettext*/
$(function () {
// Question view
if (!$('#question_chart').length) {
return
}
// Question view
if (!$("#question_chart").length) {
return;
}
$('.chart').css('height', '250px')
let data_type = $('#question_chart').attr('data-type'),
data = JSON.parse($('#question-chart-data').text() || '[]'),
others_sum = 0,
max_num = 8
$(".chart").css("height", "250px");
var data_type = $("#question_chart").attr("data-type"),
data = JSON.parse($("#question-chart-data").text() || "[]"),
others_sum = 0,
max_num = 8;
data = data.map(function (d) {
return {
value: d.count,
label: d.answer.length > 20 ? d.answer.substring(0, 20) + '…' : d.answer,
}
})
data = data.map(function (d) {
return {
'value': d.count,
'label': d.answer.length > 20 ? d.answer.substring(0, 20) + '…' : d.answer,
}
});
if (data_type == 'N') {
// Sort
data.sort(function (a, b) {
if (parseFloat(a.label) > parseFloat(b.label)) {
return 1
} else if (parseFloat(a.label) < parseFloat(b.label)) {
return -1
} else {
return 0
}
})
max_num = 20
}
if (data_type == 'N') {
// Sort
data.sort(function (a, b) {
if (parseFloat(a.label) > parseFloat(b.label)) {
return 1;
} else if (parseFloat(a.label) < parseFloat(b.label)) {
return -1;
} else {
return 0;
}
});
max_num = 20;
}
// Limit shown options
if (data.length > max_num) {
for (let i = max_num; i < data.length; i++) {
others_sum += data[i].value
}
data = data.slice(0, max_num)
data.push({ value: others_sum, label: gettext('Others') })
}
// Limit shown options
if (data.length > max_num) {
for (var i = max_num; i < data.length; i++) {
others_sum += data[i].value;
}
data = data.slice(0, max_num);
data.push({'value': others_sum, 'label': gettext('Others')});
}
if (data_type === 'B') {
let colors
if (data[0].answer_bool) {
colors = ['#50A167', '#C44F4F']
} else {
colors = ['#C44F4F', '#50A167']
}
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: colors
})
} else if (data_type === 'C') {
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: [
'#7F4A91',
'#50A167',
'#FFB419',
'#5F9CD4',
'#C44F4F',
'#83FFFA',
'#FF6C38',
'#1f5b8e',
'#2d683c',
]
})
} else { // M, N, S, T
new Morris.Bar({
element: 'question_chart',
data: data,
resize: true,
xkey: 'label',
ykeys: ['value'],
labels: [gettext('Count')]
})
}
if (data_type === 'B') {
var colors;
if (data[0].answer_bool) {
colors = ['#50A167', '#C44F4F'];
} else {
colors = ['#C44F4F', '#50A167'];
}
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: colors
});
} else if (data_type === 'C') {
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: [
'#7F4A91',
'#50A167',
'#FFB419',
'#5F9CD4',
'#C44F4F',
'#83FFFA',
'#FF6C38',
'#1f5b8e',
'#2d683c',
]
});
} else { // M, N, S, T
new Morris.Bar({
element: 'question_chart',
data: data,
resize: true,
xkey: 'label',
ykeys: ['value'],
labels: [gettext('Count')]
});
}
// N, S, T
})
// N, S, T
});
$(function () {
// Question editor
// Question editor
if (!$('#answer-options').length) {
return
}
if (!$("#answer-options").length) {
return;
}
// Question editor
$('#id_type').change(question_page_toggle_view)
$('#id_required').change(question_page_toggle_view)
question_page_toggle_view()
// Question editor
$("#id_type").change(question_page_toggle_view);
$("#id_required").change(question_page_toggle_view);
question_page_toggle_view();
function question_page_toggle_view () {
let show = $('#id_type').val() == 'C' || $('#id_type').val() == 'M'
$('#answer-options').toggle(show)
function question_page_toggle_view() {
var show = $("#id_type").val() == "C" || $("#id_type").val() == "M";
$("#answer-options").toggle(show);
$('#valid-date').toggle($('#id_type').val() == 'D')
$('#valid-datetime').toggle($('#id_type').val() == 'W')
$('#valid-string').toggle($('#id_type').val() == 'T' || $('#id_type').val() == 'S')
$('#valid-number').toggle($('#id_type').val() == 'N')
$('#valid-file').toggle($('#id_type').val() == 'F')
$("#valid-date").toggle($("#id_type").val() == "D");
$("#valid-datetime").toggle($("#id_type").val() == "W");
$("#valid-string").toggle($("#id_type").val() == "T" || $("#id_type").val() == "S");
$("#valid-number").toggle($("#id_type").val() == "N");
$("#valid-file").toggle($("#id_type").val() == "F");
show = $('#id_type').val() == 'B' && $('#id_required').prop('checked')
$('.alert-required-boolean').toggle(show)
}
show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
$(".alert-required-boolean").toggle(show);
}
let $val = $('#id_dependency_values')
let $dq = $('#id_dependency_question')
let oldval = JSON.parse($('#dependency_value_val').text())
function update_dependency_options () {
$val.parent().find('.loading-indicator').remove()
$('#id_dependency_values option').remove()
$('#id_dependency_values').prop('required', false)
var $val = $("#id_dependency_values");
var $dq = $("#id_dependency_question");
var oldval = JSON.parse($("#dependency_value_val").text());
function update_dependency_options() {
$val.parent().find(".loading-indicator").remove();
$("#id_dependency_values option").remove();
$("#id_dependency_values").prop("required", false);
let val = $dq.children('option:selected').val()
if (!val) {
$('#id_dependency_values').show()
$val.show()
return
}
var val = $dq.children("option:selected").val();
if (!val) {
$("#id_dependency_values").show();
$val.show();
return;
}
$('#id_dependency_values').prop('required', true)
$val.hide()
$val.parent().append('<div class="help-block loading-indicator"><span class="fa'
+ ' fa-cog fa-spin"></span></div>')
$("#id_dependency_values").prop("required", true);
$val.hide();
$val.parent().append("<div class=\"help-block loading-indicator\"><span class=\"fa" +
" fa-cog fa-spin\"></span></div>");
// the container_type parameter is undocumented. this API is going to change in a later release.
apiGET('/api/v1/organizers/' + $('body').attr('data-organizer') + '/events/' + $('body').attr('data-event') + '/questions/' + val + '/?container_type=' + encodeURIComponent($dq.data('container-type')), function (data) {
if (data.type === 'B') {
$val.append($('<option>').attr('value', 'True').text(gettext('Yes')))
$val.append($('<option>').attr('value', 'False').text(gettext('No')))
} else {
for (let i = 0; i < data.options.length; i++) {
let opt = data.options[i]
let $opt = $('<option>').attr('value', opt.identifier).text(i18nToString(opt.answer))
$val.append($opt)
}
}
if (oldval) {
$val.val(oldval)
}
$val.parent().find('.loading-indicator').remove()
$val.show()
})
}
// the container_type parameter is undocumented. this API is going to change in a later release.
apiGET('/api/v1/organizers/' + $("body").attr("data-organizer") + '/events/' + $("body").attr("data-event") + '/questions/' + val + '/?container_type=' + encodeURIComponent($dq.data('container-type')), function (data) {
if (data.type === "B") {
$val.append($("<option>").attr("value", "True").text(gettext("Yes")));
$val.append($("<option>").attr("value", "False").text(gettext("No")));
} else {
for (var i = 0; i < data.options.length; i++) {
var opt = data.options[i];
var $opt = $("<option>").attr("value", opt.identifier).text(i18nToString(opt.answer));
$val.append($opt);
}
}
if (oldval) {
$val.val(oldval);
}
$val.parent().find(".loading-indicator").remove();
$val.show();
});
}
update_dependency_options()
$dq.change(update_dependency_options)
})
update_dependency_options();
$dq.change(update_dependency_options);
});
@@ -1,50 +1,50 @@
$(function () {
'use strict'
"use strict";
let ticket_type_quota_calculation = function () {
let sum = 0
$('#ticket-type-formset div[data-formset-form]').each(function () {
if (!$(this).find('input[name$=DELETE]').prop('checked')) {
let val = $(this).find('input[name$=quota]').val()
if (val === '') {
sum = '∞'
} else if (sum !== '∞') {
sum += parseInt(val)
}
}
})
$('#total-capacity').text(sum)
}
var ticket_type_quota_calculation = function () {
var sum = 0;
$("#ticket-type-formset div[data-formset-form]").each(function () {
if (!$(this).find("input[name$=DELETE]").prop("checked")) {
var val = $(this).find("input[name$=quota]").val();
if (val === "") {
sum = "∞";
} else if (sum !== "∞") {
sum += parseInt(val);
}
}
});
$("#total-capacity").text(sum);
};
let toggle_payment = function () {
let any = false
$('#ticket-type-formset div[data-formset-form]').each(function () {
if (!$(this).find('input[name$=DELETE]').prop('checked')) {
let val = $(this).find('input[name$=default_price]').val()
if (/.*[1-9].*/.test(val)) {
any = true
}
}
})
if ($('#quick-setup-step-payment:visible').length && !any) {
$('#quick-setup-step-payment').stop().slideUp()
} else if (!$('#quick-setup-step-payment:visible').length && any) {
$('#quick-setup-step-payment').stop().slideDown()
}
}
var toggle_payment = function () {
var any = false;
$("#ticket-type-formset div[data-formset-form]").each(function () {
if (!$(this).find("input[name$=DELETE]").prop("checked")) {
var val = $(this).find("input[name$=default_price]").val();
if (/.*[1-9].*/.test(val)) {
any = true;
}
}
});
if ($("#quick-setup-step-payment:visible").length && !any) {
$("#quick-setup-step-payment").stop().slideUp();
} else if (!$("#quick-setup-step-payment:visible").length && any) {
$("#quick-setup-step-payment").stop().slideDown();
}
};
$('#ticket-type-formset').bind('formAdded', ticket_type_quota_calculation)
$('#ticket-type-formset').on('change keyup keydown keypress', 'input', function () {
ticket_type_quota_calculation()
toggle_payment()
})
ticket_type_quota_calculation()
toggle_payment()
$("#ticket-type-formset").bind("formAdded", ticket_type_quota_calculation);
$("#ticket-type-formset").on("change keyup keydown keypress", "input", function () {
ticket_type_quota_calculation();
toggle_payment();
});
ticket_type_quota_calculation();
toggle_payment();
$('#total-capacity-edit').click(function () {
$('#id_total_quota').val(parseInt($('#total-capacity').text()))
$('#total-capacity').hide()
$('#id_total_quota').closest('div').removeClass('sr-only')
$('#total-capacity-edit').hide()
})
})
$("#total-capacity-edit").click(function () {
$("#id_total_quota").val(parseInt($("#total-capacity").text()));
$("#total-capacity").hide();
$("#id_total_quota").closest("div").removeClass("sr-only");
$("#total-capacity-edit").hide();
});
});
+37 -37
View File
@@ -1,43 +1,43 @@
/* globals Morris */
/*globals $, Morris, gettext*/
$(function () {
if (!$('#quota-stats').length) {
return
}
if (!$("#quota-stats").length) {
return;
}
$('.chart').css('height', '250px')
new Morris.Donut({
element: 'quota_chart',
data: JSON.parse($('#quota-chart-data').html()),
resize: true,
colors: [
'#0044CC', // paid
'#0088CC', // pending
'#BD362F', // vouchers
'#F89406', // carts
'#51A351' // available
]
})
})
$(".chart").css("height", "250px");
new Morris.Donut({
element: 'quota_chart',
data: JSON.parse($("#quota-chart-data").html()),
resize: true,
colors: [
'#0044CC', // paid
'#0088CC', // pending
'#BD362F', // vouchers
'#F89406', // carts
'#51A351' // available
]
});
});
$(function () {
if (!$('input[name=itemvars]').length) {
return
}
let autofill = ($('#id_name').val() === '')
if (!$("input[name=itemvars]").length) {
return;
}
var autofill = ($("#id_name").val() === "");
$('#id_name').on('change keyup keydown keypress', function () {
autofill = false
})
$("#id_name").on("change keyup keydown keypress", function () {
autofill = false;
})
function do_autofill () {
if (autofill) {
let names = []
$('input[name=itemvars]:checked').each(function () {
names.push($.trim($(this).closest('label').text()))
})
$('#id_name').val(names.join(', '))
}
}
$('input[name=itemvars]').change(do_autofill)
do_autofill()
})
function do_autofill() {
if (autofill) {
var names = [];
$("input[name=itemvars]:checked").each(function () {
names.push($.trim($(this).closest("label").text()))
});
$("#id_name").val(names.join(', '));
}
}
$("input[name=itemvars]").change(do_autofill);
do_autofill();
});
+17 -15
View File
@@ -1,19 +1,21 @@
function rrule_form_toggles ($form) {
let freq = $form.find('select[name*=freq]').val()
$form.find('.repeat-yearly').toggle(freq === 'yearly')
$form.find('.repeat-monthly').toggle(freq === 'monthly')
$form.find('.repeat-weekly').toggle(freq === 'weekly')
/*globals $, Morris, gettext, RRule, RRuleSet*/
function rrule_form_toggles($form) {
var freq = $form.find("select[name*=freq]").val();
$form.find(".repeat-yearly").toggle(freq === "yearly");
$form.find(".repeat-monthly").toggle(freq === "monthly");
$form.find(".repeat-weekly").toggle(freq === "weekly");
}
function rrule_bind_form ($form) {
$form.find('select[name*=freq]').change(function () {
rrule_form_toggles($form)
})
rrule_form_toggles($form)
function rrule_bind_form($form) {
$form.find("select[name*=freq]").change(function () {
rrule_form_toggles($form);
});
rrule_form_toggles($form);
}
$(document).on('pretix:bind-forms', function () {
$('.rrule-form').each(function () {
rrule_bind_form($(this))
})
})
$(document).on("pretix:bind-forms", function () {
$(".rrule-form").each(function () {
rrule_bind_form($(this));
});
});
+204 -203
View File
@@ -1,225 +1,226 @@
/* globals ngettext, rrule */
/*globals $, Morris, gettext, RRule, RRuleSet*/
$(document).on('pretix:bind-forms', function () {
if (!$('div[data-formset-prefix=checkinlist_set]').length) {
return
}
$(document).on("pretix:bind-forms", function () {
if (!$("div[data-formset-prefix=checkinlist_set]").length) {
return;
}
function parse_weekday (wd) {
map = {
MO: 0,
TU: 1,
WE: 2,
TH: 3,
FR: 4,
SA: 5,
SU: 6
}
if (wd.indexOf(',') > 0) {
let wds = []
$.each(wd.split(','), function (k, v) {
wds.push(map[v])
})
return wds
} else {
return map[wd]
}
}
function parse_weekday(wd) {
map = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if (wd.indexOf(",") > 0) {
var wds = [];
$.each(wd.split(","), function (k, v) {
wds.push(map[v]);
});
return wds;
} else {
return map[wd];
}
}
// RRule editor
function rrule_preview () {
let ruleset = new rrule.RRuleSet()
// RRule editor
function rrule_preview() {
var ruleset = new rrule.RRuleSet();
$('.rrule-form').each(function () {
if ($(this).find('input[name$=DELETE]').prop('checked')) {
return
}
$(".rrule-form").each(function () {
if ($(this).find("input[name$=DELETE]").prop("checked")) {
return;
}
let rule_args = {}
let $form = $(this)
let freq = $form.find('select[name*=freq]').val()
if (!$form.find('input[name*=dtstart]').data('DateTimePicker')) {
// uninitialized
return
}
let dtstart = $form.find('input[name*=dtstart]').data('DateTimePicker').date()
dtstart = dtstart.add(dtstart.utcOffset(), 'm').add(12, 'h').utcOffset(0)
rule_args.dtstart = dtstart.toDate()
rule_args.interval = parseInt($form.find('input[name*=interval]').val()) || 1
var rule_args = {};
var $form = $(this);
var freq = $form.find("select[name*=freq]").val();
if (!$form.find("input[name*=dtstart]").data("DateTimePicker")) {
// uninitialized
return;
}
var dtstart = $form.find("input[name*=dtstart]").data("DateTimePicker").date();
dtstart = dtstart.add(dtstart.utcOffset(), 'm').add(12, 'h').utcOffset(0);
rule_args.dtstart = dtstart.toDate();
rule_args.interval = parseInt($form.find("input[name*=interval]").val()) || 1;
if (freq === 'yearly') {
rule_args.freq = rrule.RRule.YEARLY
if (freq === 'yearly') {
rule_args.freq = rrule.RRule.YEARLY;
var same = $form.find('input[name*=yearly_same]:checked').val()
if (same === 'off') {
rule_args.bysetpos = parseInt($form.find('select[name*=yearly_bysetpos]').val())
rule_args.byweekday = parse_weekday($form.find('select[name*=yearly_byweekday]').val())
rule_args.bymonth = parseInt($form.find('select[name*=yearly_bymonth]').val())
}
} else if (freq === 'monthly') {
rule_args.freq = rrule.RRule.MONTHLY
var same = $form.find("input[name*=yearly_same]:checked").val();
if (same === "off") {
rule_args.bysetpos = parseInt($form.find("select[name*=yearly_bysetpos]").val());
rule_args.byweekday = parse_weekday($form.find("select[name*=yearly_byweekday]").val());
rule_args.bymonth = parseInt($form.find("select[name*=yearly_bymonth]").val());
}
} else if (freq === 'monthly') {
rule_args.freq = rrule.RRule.MONTHLY;
var same = $form.find('input[name*=monthly_same]:checked').val()
if (same === 'off') {
rule_args.bysetpos = parseInt($form.find('select[name*=monthly_bysetpos]').val())
rule_args.byweekday = parse_weekday($form.find('select[name*=monthly_byweekday]').val())
}
} else if (freq === 'weekly') {
rule_args.freq = rrule.RRule.WEEKLY
var same = $form.find("input[name*=monthly_same]:checked").val();
if (same === "off") {
rule_args.bysetpos = parseInt($form.find("select[name*=monthly_bysetpos]").val());
rule_args.byweekday = parse_weekday($form.find("select[name*=monthly_byweekday]").val());
}
} else if (freq === 'weekly') {
rule_args.freq = rrule.RRule.WEEKLY;
let days = []
$form.find('input[name*=weekly_byweekday]:checked').each(function () {
days.push(parse_weekday($(this).val()))
})
if (days.length !== 0) {
rule_args.byweekday = days
}
} else if (freq === 'daily') {
rule_args.freq = rrule.RRule.DAILY
}
var days = [];
$form.find("input[name*=weekly_byweekday]:checked").each(function () {
days.push(parse_weekday($(this).val()));
});
if (days.length !== 0) {
rule_args.byweekday = days;
}
} else if (freq === 'daily') {
rule_args.freq = rrule.RRule.DAILY;
}
let end = $form.find('input[name*=end]:checked').val()
if (end === 'count') {
rule_args.count = Math.max(parseInt($form.find('input[name*=count]').val()) || 1, 1)
} else {
let date = $form.find('input[name*=until]').data('DateTimePicker').date()
if (date !== null) {
// rrule.until is non-inclusive, whereas in pretix-backend "until" is inclusive => add 1 day
// date is a Moment-object. Moment.add() mutates, but is save to do here
date.add(1, 'days')
rule_args.until = date
}
}
var end = $form.find("input[name*=end]:checked").val();
if (end === "count") {
rule_args.count = Math.max(parseInt($form.find("input[name*=count]").val()) || 1, 1);
} else {
var date = $form.find("input[name*=until]").data("DateTimePicker").date();
if (date !== null) {
// rrule.until is non-inclusive, whereas in pretix-backend "until" is inclusive => add 1 day
// date is a Moment-object. Moment.add() mutates, but is save to do here
date.add(1, 'days');
rule_args.until = date;
}
}
if ($form.find('input[name*=exclude]').prop('checked')) {
ruleset.exrule(new rrule.RRule(rule_args))
$form.closest('.panel').addClass('panel-danger').removeClass('panel-default')
} else {
ruleset.rrule(new rrule.RRule(rule_args))
$form.closest('.panel').addClass('panel-default').removeClass('panel-danger')
}
})
if ($form.find("input[name*=exclude]").prop("checked")) {
ruleset.exrule(new rrule.RRule(rule_args));
$form.closest(".panel").addClass("panel-danger").removeClass("panel-default");
} else {
ruleset.rrule(new rrule.RRule(rule_args));
$form.closest(".panel").addClass("panel-default").removeClass("panel-danger");
}
});
let all_dates = ruleset.all()
let format = $('body').attr('data-longdateformat') + ' (dddd)'
$('#rrule-preview').html('')
if (all_dates.length > 20) {
$('#rrule-preview').html('')
all_dates.slice(0, 10).forEach(function (element) {
$('#rrule-preview').append($('<li>').text(moment(element).utc().format(format)))
})
$('#rrule-preview').append($('<li>').text(ngettext(
'(one more date)',
'({num} more dates)',
all_dates.length - 20
).replace(/\{num\}/g, all_dates.length - 20)))
all_dates.slice(-10).forEach(function (element) {
$('#rrule-preview').append($('<li>').text(moment(element).utc().format(format)))
})
} else {
all_dates.forEach(function (element) {
$('#rrule-preview').append($('<li>').text(moment(element).utc().format(format)))
})
}
}
$('#rrule-formset').on('change keydown keyup keypress dp.change', 'input, select', function () {
rrule_preview()
})
rrule_preview()
var all_dates = ruleset.all();
var format = $("body").attr("data-longdateformat") + " (dddd)";
$("#rrule-preview").html("");
if (all_dates.length > 20) {
$("#rrule-preview").html("");
all_dates.slice(0, 10).forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
$("#rrule-preview").append($("<li>").text(ngettext(
"(one more date)",
"({num} more dates)",
all_dates.length - 20
).replace(/\{num\}/g, all_dates.length - 20)));
all_dates.slice(-10).forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
} else {
all_dates.forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
}
}
$("#rrule-formset").on("change keydown keyup keypress dp.change", "input, select", function () {
rrule_preview();
});
rrule_preview();
$('#rrule-formset').on('formAdded', 'div', function (event) { rrule_bind_form($(event.target)) })
$("#rrule-formset").on("formAdded", "div", function (event) {rrule_bind_form($(event.target)); });
// Timeslot editor
$('#subevent_add_many_slots_go').on('click', function () {
$('#time-formset [data-formset-form]').each(function () {
let tf = $(this).find('[name$=time_from]').val()
if (!tf) {
$(this).remove()
}
})
// Timeslot editor
$("#subevent_add_many_slots_go").on("click", function () {
$("#time-formset [data-formset-form]").each(function () {
var tf = $(this).find("[name$=time_from]").val()
if (!tf) {
$(this).remove();
}
})
let first = $('#subevent_add_many_slots_first').data('DateTimePicker').date()
let end = $('#subevent_add_many_slots_end').data('DateTimePicker').date()
let length_m = parseFloat($('#subevent_add_many_slots_length').val()) || 0
let break_m = parseFloat($('#subevent_add_many_slots_break').val()) || 0
if (!first || !end || !length_m) {
console.log('invalid', first, end, length_m)
return
}
var first = $("#subevent_add_many_slots_first").data('DateTimePicker').date();
var end = $("#subevent_add_many_slots_end").data('DateTimePicker').date();
var length_m = parseFloat($("#subevent_add_many_slots_length").val()) || 0;
var break_m = parseFloat($("#subevent_add_many_slots_break").val()) || 0;
if (!first || !end || !length_m) {
console.log("invalid", first, end, length_m)
return
}
function closure ($form, time) {
return function () {
console.log('setting value', time)
$form.find('[name$=time_from]').data('DateTimePicker').date(time)
time.add(length_m, 'minutes')
$form.find('[name$=time_to]').data('DateTimePicker').date(time)
}
}
function closure($form, time) {
return function () {
console.log("setting value", time)
$form.find("[name$=time_from]").data('DateTimePicker').date(time);
time.add(length_m, 'minutes');
$form.find("[name$=time_to]").data('DateTimePicker').date(time);
}
}
let pointer = first.clone()
while (pointer.isBefore(end)) {
let $form = $('#time-formset').formset('getOrCreate').addForm()
$form.attr('data-formset-created-at-runtime', 'false') // prevents animation
let time = pointer.clone()
window.setTimeout(closure($form, time), 1)
// jquery.formset.js only calls trigger("formAdded") after a setTimeout of 0,
// but we need to run after that to make sure the date pickers are initialized
pointer.add(break_m + length_m, 'minutes')
}
$('#subevent_add_many_slots').addClass('hidden')
$('#subevent_add_many_slots_start').removeClass('hidden')
})
$('#subevent_add_many_slots_start').on('click', function () {
$('#subevent_add_many_slots').removeClass('hidden')
$(this).addClass('hidden')
})
var pointer = first.clone();
while (pointer.isBefore(end)) {
var $form = $("#time-formset").formset("getOrCreate").addForm();
$form.attr("data-formset-created-at-runtime", "false"); // prevents animation
var time = pointer.clone();
window.setTimeout(closure($form, time), 1);
// jquery.formset.js only calls trigger("formAdded") after a setTimeout of 0,
// but we need to run after that to make sure the date pickers are initialized
pointer.add(break_m + length_m, 'minutes');
}
$("#subevent_add_many_slots").addClass("hidden");
$("#subevent_add_many_slots_start").removeClass("hidden");
// Hide config for products that are not for sale
function quota_form_handlers (el) {
// searchable_selection = True
el.find('[id^="id_quotas-"]').on('select2:select select2:unselect', () => {
update_item_visibility()
})
// searchable_selection = False
el.find('input[id^="id_quotas-"][id*=itemvars_]').on('change', () => {
update_item_visibility()
})
}
function update_item_visibility () {
const itemvars = []
});
$("#subevent_add_many_slots_start").on("click", function () {
$("#subevent_add_many_slots").removeClass("hidden");
$(this).addClass("hidden");
});
// searchable_selection = True
$('select[id^=id_quotas-][id$=-itemvars]').filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]')
}).each((_, e) => itemvars.push(...$(e).val()))
// searchable_selection = False
$('input[id^=id_quotas-][id*=itemvars_]:checked').filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]')
}).each((_, e) => itemvars.push($(e).val()))
// Hide config for products that are not for sale
function quota_form_handlers(el) {
// searchable_selection = True
el.find('[id^="id_quotas-"]').on("select2:select select2:unselect", () => {
update_item_visibility();
});
// searchable_selection = False
el.find('input[id^="id_quotas-"][id*=itemvars_]').on("change", () => {
update_item_visibility();
});
}
function update_item_visibility() {
const itemvars = [];
$('div[data-itemvar]').each(function (idx, e) {
const el = $(e)
el.prop('hidden', !itemvars.includes(el.attr('data-itemvar')) && !el.find('.has-error, .alert-danger').length)
})
}
// searchable_selection = True
$("select[id^=id_quotas-][id$=-itemvars]").filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
}).each((_, e) => itemvars.push(...$(e).val()));
// searchable_selection = False
$("input[id^=id_quotas-][id*=itemvars_]:checked").filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
}).each((_, e) => itemvars.push($(e).val()));
$('[data-formset-prefix="quotas"]').on('formDeleted', 'div', () => {
update_item_visibility()
}).on('formAdded', 'div', (event) => {
quota_form_handlers($(event.target))
update_item_visibility()
})
quota_form_handlers($('body'))
update_item_visibility()
$("div[data-itemvar]").each(function (idx, e) {
const el = $(e);
el.prop("hidden", !itemvars.includes(el.attr("data-itemvar")) && !el.find(".has-error, .alert-danger").length);
});
}
// Auto-set name of check-in list
let $namef = $('input[id^=id_name]').first()
let lastValue = $namef.val()
$namef.change(function () {
let field = $('div[data-formset-prefix=checkinlist_set] input[id$=name]').first()
if (field.val() === lastValue) {
lastValue = $(this).val()
field.val(lastValue)
}
})
})
$('[data-formset-prefix="quotas"]').on("formDeleted", "div", () => {
update_item_visibility();
}).on("formAdded", "div", (event) => {
quota_form_handlers($(event.target));
update_item_visibility();
})
quota_form_handlers($("body"));
update_item_visibility();
// Auto-set name of check-in list
var $namef = $("input[id^=id_name]").first();
var lastValue = $namef.val();
$namef.change(function () {
var field = $("div[data-formset-prefix=checkinlist_set] input[id$=name]").first();
if (field.val() === lastValue) {
lastValue = $(this).val();
field.val(lastValue);
}
});
});
+55 -53
View File
@@ -1,54 +1,56 @@
$(function () {
let j = 0
$('.tabbed-form').each(function () {
let $form = $(this)
let $tabs = $('<ul>').addClass('nav nav-tabs').insertBefore($form)
$form.addClass('tab-content')
/*globals $*/
let i = 0
let preselect = null
let validity_error = false
$form.find('fieldset').each(function () {
let $fieldset = $(this)
let tid = $fieldset.attr('id')
if (!tid) tid = 'tab-' + j + '-' + i
let $tabli = $('<li>').appendTo($tabs)
let $tablink = $('<a>').attr('role', 'tab')
.attr('data-toggle', 'tab')
.attr('href', '#' + tid)
.text($fieldset.find('legend').text())
.appendTo($tabli)
if ($fieldset.find('.has-error, .alert-danger:not(.dynamic)').length > 0) {
$tablink.append(' ')
$tablink.append($('<span>').addClass('fa fa-warning text-danger'))
if (preselect === null) {
preselect = i
}
}
$fieldset.find('input, select, textarea').on('invalid', function () {
if ($tablink.find('.fa-warning').length === 0) {
$tablink.append(' ')
$tablink.append($('<span>').addClass('fa fa-warning text-danger'))
if (!validity_error) {
validity_error = true
$tablink.click()
}
}
})
$fieldset.find('legend').remove()
$fieldset.addClass('tab-pane').attr('id', tid)
if (location.hash && ($fieldset.find(location.hash).length || location.hash === '#' + tid + '-open') && preselect === null) {
preselect = i
}
i++
})
$tabs.find('a').get(preselect != null ? preselect : 0).click()
$tabs.find('a').on('shown.bs.tab', function (e) {
history.replaceState(null, null, e.target.getAttribute('href') + '-open')
})
$form.closest('form').on('submit', function () {
validity_error = false
})
j++
})
})
$(function () {
var j = 0;
$(".tabbed-form").each(function () {
var $form = $(this);
var $tabs = $("<ul>").addClass("nav nav-tabs").insertBefore($form);
$form.addClass("tab-content");
var i = 0;
var preselect = null;
var validity_error = false;
$form.find("fieldset").each(function () {
var $fieldset = $(this);
var tid = $fieldset.attr("id");
if (!tid) tid = "tab-" + j + "-" + i;
var $tabli = $("<li>").appendTo($tabs);
var $tablink = $("<a>").attr("role", "tab")
.attr("data-toggle", "tab")
.attr("href", "#" + tid)
.text($fieldset.find("legend").text())
.appendTo($tabli);
if ($fieldset.find(".has-error, .alert-danger:not(.dynamic)").length > 0) {
$tablink.append(" ");
$tablink.append($("<span>").addClass("fa fa-warning text-danger"));
if (preselect === null) {
preselect = i;
}
}
$fieldset.find("input, select, textarea").on("invalid", function () {
if ($tablink.find(".fa-warning").length === 0) {
$tablink.append(" ");
$tablink.append($("<span>").addClass("fa fa-warning text-danger"));
if (!validity_error) {
validity_error = true;
$tablink.click();
}
}
});
$fieldset.find("legend").remove();
$fieldset.addClass("tab-pane").attr("id", tid);
if (location.hash && ($fieldset.find(location.hash).length || location.hash === "#" + tid + "-open") && preselect === null) {
preselect = i;
}
i++;
});
$tabs.find("a").get(preselect != null ? preselect : 0).click();
$tabs.find("a").on('shown.bs.tab', function (e) {
history.replaceState(null, null, e.target.getAttribute("href") + "-open");
});
$form.closest("form").on("submit", function () {
validity_error = false;
});
j++;
});
});
+158 -157
View File
@@ -1,163 +1,164 @@
/*global $,u2f */
$(function () {
$('.context-selector.dropdown').on('shown.bs.collapse shown.bs.dropdown', function () {
$(this).parent().find('input').val('').trigger('forceRunQuery').focus()
})
$('.dropdown-menu .form-box input').click(function (e) {
e.stopPropagation()
})
$('.context-selector.dropdown').on('shown.bs.collapse shown.bs.dropdown', function () {
$(this).parent().find("input").val("").trigger('forceRunQuery').focus();
});
$('.dropdown-menu .form-box input').click(function (e) {
e.stopPropagation();
});
$('[data-event-typeahead]').each(function () {
let $container = $(this)
let $query = $(this).find('[data-typeahead-query]').length ? $(this).find('[data-typeahead-query]') : $($(this).attr('data-typeahead-field'))
$container.find('li:not(.query-holder)').remove()
let lastQuery = null
let runQueryTimeout = null
let loadIndicatorTimeout = null
let focusOutTimeout = null
function showLoadIndicator () {
$container.find('li:not(.query-holder)').remove()
$container.append('<li class=\'loading\'><span class=\'fa fa-4x fa-cog fa-spin\'></span></li>')
$container.toggleClass('focused', $query.is(':focus') && $container.children().length > 0)
}
function runQuery () {
let thisQuery = $query.val()
if (thisQuery === lastQuery) return
lastQuery = $query.val()
$("[data-event-typeahead]").each(function () {
var $container = $(this);
var $query = $(this).find('[data-typeahead-query]').length ? $(this).find('[data-typeahead-query]') : $($(this).attr("data-typeahead-field"));
$container.find("li:not(.query-holder)").remove();
var lastQuery = null;
var runQueryTimeout = null;
var loadIndicatorTimeout = null;
var focusOutTimeout = null;
function showLoadIndicator() {
$container.find("li:not(.query-holder)").remove();
$container.append("<li class='loading'><span class='fa fa-4x fa-cog fa-spin'></span></li>");
$container.toggleClass('focused', $query.is(":focus") && $container.children().length > 0);
}
function runQuery() {
var thisQuery = $query.val();
if (thisQuery === lastQuery) return;
lastQuery = $query.val();
window.clearTimeout(loadIndicatorTimeout)
loadIndicatorTimeout = window.setTimeout(showLoadIndicator, 80)
window.clearTimeout(loadIndicatorTimeout)
loadIndicatorTimeout = window.setTimeout(showLoadIndicator, 80)
$.getJSON(
$container.attr('data-source') + '?query=' + encodeURIComponent($query.val()) + (typeof $container.attr('data-organizer') !== 'undefined' ? '&organizer=' + $container.attr('data-organizer') : ''),
function (data) {
if (thisQuery !== lastQuery) {
// Lost race condition
return
}
window.clearTimeout(loadIndicatorTimeout)
$container.find('li:not(.query-holder)').remove()
$.each(data.results, function (_i, res) {
let $linkContent = $('<div>')
if (res.type === 'organizer') {
$linkContent.append(
$('<span>').addClass('event-name-full').append(
$('<span>').addClass('fa fa-users fa-fw')
).append(' ').append($('<div>').text(res.name).html())
)
} else if (res.type === 'order' || res.type === 'voucher') {
$linkContent.append(
$('<span>').addClass('event-name-full').append($('<div>').text(res.title).html())
).append(
$('<span>').addClass('event-organizer').append(
$('<span>').addClass('fa fa-calendar fa-fw')
).append(' ').append($('<div>').text(res.event).html())
)
} else if (res.type === 'user') {
$linkContent.append(
$('<span>').addClass('event-name-full').append(
$('<span>').addClass('fa fa-user fa-fw')
).append(' ').append($('<div>').text(res.name).html())
)
} else {
$linkContent.append(
$('<span>').addClass('event-name-full').append($('<div>').text(res.name).html())
).append(
$('<span>').addClass('event-organizer').append(
$('<span>').addClass('fa fa-users fa-fw')
).append(' ').append($('<div>').text(res.organizer).html())
).append(
$('<span>').addClass('event-daterange').append(
$('<span>').addClass('fa fa-calendar fa-fw')
).append(' ').append(res.date_range)
)
}
$.getJSON(
$container.attr("data-source") + "?query=" + encodeURIComponent($query.val()) + (typeof $container.attr("data-organizer") !== "undefined" ? "&organizer=" + $container.attr("data-organizer") : ""),
function (data) {
if (thisQuery !== lastQuery) {
// Lost race condition
return;
}
window.clearTimeout(loadIndicatorTimeout);
$container.find("li:not(.query-holder)").remove();
$.each(data.results, function (i, res) {
let $linkContent = $("<div>");
if (res.type === "organizer") {
$linkContent.append(
$("<span>").addClass("event-name-full").append(
$("<span>").addClass("fa fa-users fa-fw")
).append(" ").append($("<div>").text(res.name).html())
)
} else if (res.type === "order" || res.type === "voucher") {
$linkContent.append(
$("<span>").addClass("event-name-full").append($("<div>").text(res.title).html())
).append(
$("<span>").addClass("event-organizer").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append($("<div>").text(res.event).html())
)
} else if (res.type === "user") {
$linkContent.append(
$("<span>").addClass("event-name-full").append(
$("<span>").addClass("fa fa-user fa-fw")
).append(" ").append($("<div>").text(res.name).html())
)
} else {
$linkContent.append(
$("<span>").addClass("event-name-full").append($("<div>").text(res.name).html())
).append(
$("<span>").addClass("event-organizer").append(
$("<span>").addClass("fa fa-users fa-fw")
).append(" ").append($("<div>").text(res.organizer).html())
).append(
$("<span>").addClass("event-daterange").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append(res.date_range)
)
}
$container.append(
$('<li>').append(
$('<a>').attr('href', res.url).append(
$linkContent
)
)
)
})
$container.toggleClass('focused', $query.is(':focus') && $container.children().length > 0)
}
)
}
$query.on('forceRunQuery', function () {
runQuery()
})
$query.on('input', function () {
if ($container.attr('data-typeahead-field') && $query.val() === '') {
$container.removeClass('focused')
$container.find('li:not(.query-holder)').remove()
lastQuery = null
return
}
window.clearTimeout(runQueryTimeout)
runQueryTimeout = window.setTimeout(runQuery, 250)
})
$query.on('keydown', function (event) {
let $selected = $container.find('.active')
if (event.which === 13) { // enter
let $link = $selected.find('a')
if ($link.length) {
location.href = $link.attr('href')
}
event.preventDefault()
event.stopPropagation()
}
})
$container.add($query).on('keydown', function (event) {
if (event.which === 27) { // escape
$container.removeClass('focused')
}
}).on('focusin', function () {
window.clearTimeout(focusOutTimeout)
$(document.body).one('focusout', function () {
focusOutTimeout = window.setTimeout(function () {
$container.removeClass('focused')
}, 100)
})
})
$query.on('keyup', function (event) {
let $first = $container.find('li:not(.query-holder)').first()
let $last = $container.find('li:not(.query-holder)').last()
let $selected = $container.find('.active')
$container.append(
$("<li>").append(
$("<a>").attr("href", res.url).append(
$linkContent
)
)
);
});
$container.toggleClass('focused', $query.is(":focus") && $container.children().length > 0);
}
);
}
$query.on("forceRunQuery", function () {
runQuery();
});
$query.on("input", function () {
if ($container.attr("data-typeahead-field") && $query.val() === "") {
$container.removeClass('focused');
$container.find("li:not(.query-holder)").remove();
lastQuery = null;
return;
}
window.clearTimeout(runQueryTimeout)
runQueryTimeout = window.setTimeout(runQuery, 250)
});
$query.on("keydown", function (event) {
var $selected = $container.find(".active");
if (event.which === 13) { // enter
var $link = $selected.find("a");
if ($link.length) {
location.href = $link.attr("href");
}
event.preventDefault();
event.stopPropagation();
}
});
$container.add($query).on("keydown", function (event) {
if (event.which === 27) { // escape
$container.removeClass('focused');
}
}).on("focusin", function (event) {
window.clearTimeout(focusOutTimeout);
$(document.body).one("focusout", function (event) {
focusOutTimeout = window.setTimeout(function () {
$container.removeClass('focused');
}, 100);
})
});
$query.on("keyup", function (event) {
var $first = $container.find("li:not(.query-holder)").first();
var $last = $container.find("li:not(.query-holder)").last();
var $selected = $container.find(".active");
if (event.which === 13) { // enter
event.preventDefault()
event.stopPropagation()
return true
} else if (event.which === 40) { // down
let $next
if ($selected.length === 0) {
$next = $first
} else {
$next = $selected.next()
}
if ($next.length === 0) {
$next = $first
}
$selected.removeClass('active')
$next.addClass('active')
event.preventDefault()
event.stopPropagation()
return true
} else if (event.which === 38) { // up
if ($selected.length === 0) {
$selected = $first
}
let $prev = $selected.prev()
if ($prev.length === 0 || $prev.find('input').length > 0) {
$prev = $last
}
$selected.removeClass('active')
$prev.addClass('active')
event.preventDefault()
event.stopPropagation()
return true
}
})
})
})
if (event.which === 13) { // enter
event.preventDefault();
event.stopPropagation();
return true;
} else if (event.which === 40) { // down
var $next;
if ($selected.length === 0) {
$next = $first;
} else {
$next = $selected.next();
}
if ($next.length === 0) {
$next = $first;
}
$selected.removeClass("active");
$next.addClass("active");
event.preventDefault();
event.stopPropagation();
return true;
} else if (event.which === 38) { // up
if ($selected.length === 0) {
$selected = $first;
}
var $prev = $selected.prev();
if ($prev.length === 0 || $prev.find("input").length > 0) {
$prev = $last;
}
$selected.removeClass("active");
$prev.addClass("active");
event.preventDefault();
event.stopPropagation();
return true;
}
});
});
});
@@ -1,78 +1,78 @@
/* global i18nToString, formatPrice */
/*global $, Morris, gettext, formatPrice*/
$(function () {
// Question view
if (!$('#item_variations').length) {
return
}
// Question view
if (!$("#item_variations").length) {
return;
}
function update_variation_summary ($el) {
let var_names = Object.fromEntries(
$el
.find('input[name*=-value_]')
.filter(function () {
return !!this.value
})
.map(function () {
return [[this.getAttribute('lang'), this.value]]
})
.get()
)
let var_name = i18nToString(var_names)
let price = $el.find('input[name*=-default_price]').val()
if (price) {
let currency = $el.find('[name*=-default_price] + .input-group-addon').text()
price = formatPrice(price, currency)
}
function update_variation_summary($el) {
var var_names = Object.fromEntries(
$el
.find("input[name*=-value_]")
.filter(function () {
return !!this.value;
})
.map(function () {
return [[this.getAttribute("lang"), this.value]];
})
.get()
);
var var_name = i18nToString(var_names);
var price = $el.find("input[name*=-default_price]").val();
if (price) {
var currency = $el.find("[name*=-default_price] + .input-group-addon").text();
price = formatPrice(price, currency);
}
$el.find('.variation-name').text(var_name)
$el.find('.variation-price').text(price)
$el.find('.variation-timeframe').toggleClass('variation-icon-hidden', !(
!!$el.find('input[name$=-available_from_0]').val()
|| !!$el.find('input[name$=-available_until_0]').val()
))
$el.find('.variation-name').toggleClass('variation-disabled', !(
$el.find('input[name$=-active]').prop('checked')
))
$el.find('.variation-voucher').toggleClass('variation-icon-hidden', !(
$el.find('input[name$=-hide_without_voucher]').prop('checked')
))
$el.find('.variation-membership').toggleClass('variation-icon-hidden', !(
$el.find('input[name$=-require_membership]').prop('checked')
))
$el.find('.variation-warning').toggleClass('hidden', !(
$el.find('.alert-warning').length
))
$el.find('.variation-error').toggleClass('hidden', !(
$el.find('.alert-danger, .has-error').length
))
$el.find('input[name$=-limit_sales_channels]').each(function () {
$el.find('.variation-channel-' + $(this).val()).toggleClass('variation-icon-hidden', !(
(
$(this).closest('[data-formset-form]').find('input[name$=-all_sales_channels]').prop('checked')
|| $(this).prop('checked')
) && (
$('input[name=all_sales_channels]').prop('checked')
|| $('input[name=limit_sales_channels][value=' + $(this).val() + ']').prop('checked')
)
))
})
}
$el.find(".variation-name").text(var_name);
$el.find(".variation-price").text(price);
$el.find(".variation-timeframe").toggleClass("variation-icon-hidden", !(
!!$el.find("input[name$=-available_from_0]").val() ||
!!$el.find("input[name$=-available_until_0]").val()
));
$el.find(".variation-name").toggleClass("variation-disabled", !(
!!$el.find("input[name$=-active]").prop("checked")
));
$el.find(".variation-voucher").toggleClass("variation-icon-hidden", !(
!!$el.find("input[name$=-hide_without_voucher]").prop("checked")
));
$el.find(".variation-membership").toggleClass("variation-icon-hidden", !(
!!$el.find("input[name$=-require_membership]").prop("checked")
));
$el.find(".variation-warning").toggleClass("hidden", !(
$el.find(".alert-warning").length
));
$el.find(".variation-error").toggleClass("hidden", !(
$el.find(".alert-danger, .has-error").length
));
$el.find("input[name$=-limit_sales_channels]").each(function () {
$el.find(".variation-channel-" + $(this).val()).toggleClass("variation-icon-hidden", !(
(
$(this).closest("[data-formset-form]").find("input[name$=-all_sales_channels]").prop("checked") ||
$(this).prop("checked")
) && (
$("input[name=all_sales_channels]").prop("checked") ||
$("input[name=limit_sales_channels][value=" + $(this).val() + "]").prop("checked")
)
));
})
}
$('#item_variations [data-formset-form]').each(function () {
let $el = $(this)
update_variation_summary($el)
$(this).on('change dp.change', 'input', function () { update_variation_summary($el) })
})
$('input[name=limit_sales_channels] input[name=all_sales_channels]').on('change', function () {
$('#item_variations [data-formset-form]').each(function () {
update_variation_summary($(this))
})
})
$('#item_variations').on('formAdded', 'details', function (event) {
let $el = $(event.target)
update_variation_summary($el)
$(this).on('change dp.change', 'input', function () { update_variation_summary($el) })
setup_collapsible_details($('#item_variations'))
form_handlers($(event.target))
})
})
$("#item_variations [data-formset-form]").each(function () {
var $el = $(this);
update_variation_summary($el);
$(this).on("change dp.change", "input", function () {update_variation_summary($el)});
});
$("input[name=limit_sales_channels] input[name=all_sales_channels]").on("change", function() {
$("#item_variations [data-formset-form]").each(function () {
update_variation_summary($(this));
});
});
$("#item_variations").on("formAdded", "details", function (event) {
var $el = $(event.target);
update_variation_summary($el);
$(this).on("change dp.change", "input", function () {update_variation_summary($el)});
setup_collapsible_details($("#item_variations"));
form_handlers($(event.target));
});
});
+145 -142
View File
@@ -1,32 +1,32 @@
/* global base64js */
/*global $,u2f */
function b64enc (buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
function b64enc(buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function b64RawEnc (buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, '-')
.replace(/\//g, '_')
function b64RawEnc(buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function hexEncode (buf) {
return Array.from(buf)
.map(function (x) {
return ('0' + x.toString(16)).substr(-2)
})
.join('')
function hexEncode(buf) {
return Array.from(buf)
.map(function(x) {
return ("0" + x.toString(16)).substr(-2);
})
.join("");
}
async function fetch_json (url, options) {
const response = await fetch(url, options)
const body = await response.json()
if (body.fail)
throw body.fail
return body
async function fetch_json(url, options) {
const response = await fetch(url, options);
const body = await response.json();
if (body.fail)
throw body.fail;
return body;
}
/**
@@ -35,26 +35,27 @@ async function fetch_json (url, options) {
* @param {Object} credentialCreateOptionsFromServer
*/
const transformCredentialCreateOptions = function (credentialCreateOptionsFromServer) {
let { challenge, user, excludeCredentials } = credentialCreateOptionsFromServer
user.id = user.id.replace(/_/g, '/').replace(/-/g, '+')
user.id = Uint8Array.from(atob(user.id), c => c.charCodeAt(0))
let {challenge, user, excludeCredentials} = credentialCreateOptionsFromServer;
user.id = user.id.replace(/\_/g, "/").replace(/\-/g, "+");
user.id = Uint8Array.from(atob(user.id), c => c.charCodeAt(0));
challenge = challenge.replace(/_/g, '/').replace(/-/g, '+')
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0))
challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+");
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
excludeCredentials = excludeCredentials.map(credentialDescriptor => {
let { id } = credentialDescriptor
id = id.replace(/_/g, '/').replace(/-/g, '+')
id = Uint8Array.from(atob(id), c => c.charCodeAt(0))
return Object.assign({}, credentialDescriptor, { id })
})
excludeCredentials = excludeCredentials.map(credentialDescriptor => {
let {id} = credentialDescriptor;
id = id.replace(/\_/g, "/").replace(/\-/g, "+");
id = Uint8Array.from(atob(id), c => c.charCodeAt(0));
return Object.assign({}, credentialDescriptor, {id});
});
const transformedCredentialCreateOptions = Object.assign(
{}, credentialCreateOptionsFromServer,
{ challenge, user, excludeCredentials })
const transformedCredentialCreateOptions = Object.assign(
{}, credentialCreateOptionsFromServer,
{challenge, user, excludeCredentials});
return transformedCredentialCreateOptions;
};
return transformedCredentialCreateOptions
}
/**
* Transforms the binary data in the credential into base64 strings
@@ -62,132 +63,134 @@ const transformCredentialCreateOptions = function (credentialCreateOptionsFromSe
* @param {PublicKeyCredential} newAssertion
*/
const transformNewAssertionForServer = (newAssertion) => {
const attObj = new Uint8Array(newAssertion.response.attestationObject)
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON)
const rawId = new Uint8Array(newAssertion.rawId)
const transports = newAssertion.response.getTransports()
const authenticatorAttachment = newAssertion.authenticatorAttachment
const attObj = new Uint8Array(newAssertion.response.attestationObject);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const transports = newAssertion.response.getTransports();
const authenticatorAttachment = newAssertion.authenticatorAttachment;
const registrationClientExtensions = newAssertion.getClientExtensionResults()
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: b64enc(rawId),
response: {
attestationObject: b64enc(attObj),
clientDataJSON: b64enc(clientDataJSON),
transports: transports,
},
type: newAssertion.type,
clientExtensionResults: JSON.stringify(registrationClientExtensions),
authenticatorAttachment: authenticatorAttachment,
};
};
return {
id: newAssertion.id,
rawId: b64enc(rawId),
response: {
attestationObject: b64enc(attObj),
clientDataJSON: b64enc(clientDataJSON),
transports: transports,
},
type: newAssertion.type,
clientExtensionResults: JSON.stringify(registrationClientExtensions),
authenticatorAttachment: authenticatorAttachment,
}
}
const transformCredentialRequestOptions = (credentialRequestOptionsFromServer) => {
let { challenge, allowCredentials } = credentialRequestOptionsFromServer
let {challenge, allowCredentials} = credentialRequestOptionsFromServer;
challenge = challenge.replace(/_/g, '/').replace(/-/g, '+')
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0))
challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+");
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
allowCredentials = allowCredentials.map(credentialDescriptor => {
let { id } = credentialDescriptor
id = id.replace(/_/g, '/').replace(/-/g, '+')
id = Uint8Array.from(atob(id), c => c.charCodeAt(0))
return Object.assign({}, credentialDescriptor, { id })
})
allowCredentials = allowCredentials.map(credentialDescriptor => {
let {id} = credentialDescriptor;
id = id.replace(/\_/g, "/").replace(/\-/g, "+");
id = Uint8Array.from(atob(id), c => c.charCodeAt(0));
return Object.assign({}, credentialDescriptor, {id});
});
const transformedCredentialRequestOptions = Object.assign(
{},
credentialRequestOptionsFromServer,
{ challenge, allowCredentials })
const transformedCredentialRequestOptions = Object.assign(
{},
credentialRequestOptionsFromServer,
{challenge, allowCredentials});
return transformedCredentialRequestOptions
}
return transformedCredentialRequestOptions;
};
/**
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
const transformAssertionForServer = (newAssertion) => {
const authData = new Uint8Array(newAssertion.response.authenticatorData)
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON)
const rawId = new Uint8Array(newAssertion.rawId)
const sig = new Uint8Array(newAssertion.response.signature)
const userHandle = new Uint8Array(newAssertion.response.userHandle)
const assertionClientExtensions = newAssertion.getClientExtensionResults()
const authenticatorAttachment = newAssertion.authenticatorAttachment
const authData = new Uint8Array(newAssertion.response.authenticatorData);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(newAssertion.response.signature);
const userHandle = new Uint8Array(newAssertion.response.userHandle);
const assertionClientExtensions = newAssertion.getClientExtensionResults();
const authenticatorAttachment = newAssertion.authenticatorAttachment;
return {
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
response: {
authenticatorData: b64RawEnc(authData),
clientDataJSON: b64RawEnc(clientDataJSON),
signature: b64RawEnc(sig),
userHandle: b64RawEnc(userHandle),
},
authenticatorAttachment: authenticatorAttachment,
clientExtensionResults: JSON.stringify(assertionClientExtensions)
}
}
return {
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
response: {
authenticatorData: b64RawEnc(authData),
clientDataJSON: b64RawEnc(clientDataJSON),
signature: b64RawEnc(sig),
userHandle: b64RawEnc(userHandle),
},
authenticatorAttachment: authenticatorAttachment,
clientExtensionResults: JSON.stringify(assertionClientExtensions)
};
};
const startRegister = async () => {
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(JSON.parse($('#webauthn-enroll').text()))
const startRegister = async (e) => {
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(JSON.parse($("#webauthn-enroll").text()));
// request the authenticator(s) to create a new credential keypair.
let credential
try {
credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions
})
} catch (err) {
$('#webauthn-error').removeClass('hidden')
return console.error('Error creating credential:', err)
}
// request the authenticator(s) to create a new credential keypair.
let credential;
try {
credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions
});
} catch (err) {
$("#webauthn-error").removeClass("hidden");
return console.error("Error creating credential:", err);
}
// we now have a new credential! We now need to encode the byte arrays
// in the credential into strings, for posting to our server.
const newAssertionForServer = transformNewAssertionForServer(credential)
// we now have a new credential! We now need to encode the byte arrays
// in the credential into strings, for posting to our server.
const newAssertionForServer = transformNewAssertionForServer(credential);
$('#webauthn-response').val(JSON.stringify(newAssertionForServer))
$('#webauthn-form').submit()
}
$("#webauthn-response").val(JSON.stringify(newAssertionForServer));
$("#webauthn-form").submit();
};
const startLogin = async () => {
const transformedCredentialRequestOptions = transformCredentialRequestOptions(JSON.parse($('#webauthn-login').text()))
console.log(transformedCredentialRequestOptions)
// request the authenticator to create an assertion signature using the
// credential private key
let assertion
try {
assertion = await navigator.credentials.get({
publicKey: transformedCredentialRequestOptions,
})
} catch (err) {
$('#webauthn-error').removeClass('hidden')
return console.error('Error when creating credential:', err)
}
const startLogin = async (e) => {
const transformedCredentialRequestOptions = transformCredentialRequestOptions(JSON.parse($("#webauthn-login").text()));
console.log(transformedCredentialRequestOptions);
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer = transformAssertionForServer(assertion)
// request the authenticator to create an assertion signature using the
// credential private key
let assertion;
try {
assertion = await navigator.credentials.get({
publicKey: transformedCredentialRequestOptions,
});
} catch (err) {
$("#webauthn-error").removeClass("hidden");
return console.error("Error when creating credential:", err);
}
// post the assertion to the server for verification.
$('input, select, textarea').prop('required', false)
$('#webauthn-response, #id_password').val(JSON.stringify(transformedAssertionForServer))
$('#webauthn-form').submit()
}
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer = transformAssertionForServer(assertion);
// post the assertion to the server for verification.
$("input, select, textarea").prop("required", false);
$("#webauthn-response, #id_password").val(JSON.stringify(transformedAssertionForServer));
$("#webauthn-form").submit();
};
$(function () {
$('#webauthn-progress').hide()
if ($('#webauthn-enroll').length) {
$('#webauthn-progress').show()
startRegister()
} else if ($('#webauthn-login').length) {
$('#webauthn-progress').show()
startLogin()
}
})
$("#webauthn-progress").hide();
if ($("#webauthn-enroll").length) {
$("#webauthn-progress").show();
startRegister();
} else if ($("#webauthn-login").length) {
$("#webauthn-progress").show();
startLogin();
}
});

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