mirror of
https://github.com/pretix/pretix.git
synced 2026-09-24 18:04:42 +00:00
Compare commits
81
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
488746044b | ||
|
|
81d7dfbbc4 | ||
|
|
ed68719731 | ||
|
|
9aeb4b9731 | ||
|
|
9324887790 | ||
|
|
d353384555 | ||
|
|
b8f8e49cce | ||
|
|
806d0a5748 | ||
|
|
69e541e5af | ||
|
|
0723e6015c | ||
|
|
b8e1ab8258 | ||
|
|
81764278ae | ||
|
|
f3068e627a | ||
|
|
993a3f96e9 | ||
|
|
1af0b5e442 | ||
|
|
71b6bf630f | ||
|
|
59026da06b | ||
|
|
b3e0892d76 | ||
|
|
3029a6839b | ||
|
|
a826b0a1bd | ||
|
|
7d7474c07d | ||
|
|
58023381b9 | ||
|
|
e5c44e7aed | ||
|
|
d77a179606 | ||
|
|
29614db1e2 | ||
|
|
365051cc21 | ||
|
|
5c6b0eef6f | ||
|
|
bc0a6b662b | ||
|
|
0bc1aed1a0 | ||
|
|
0eaa5a081c | ||
|
|
688c434c4f | ||
|
|
c0474604a1 | ||
|
|
1741d08c2c | ||
|
|
6bbd808aaa | ||
|
|
aa90c5b3b1 | ||
|
|
bfbabe0e58 | ||
|
|
aa14505d2c | ||
|
|
df69656364 | ||
|
|
82cffd1519 | ||
|
|
7fc527135d | ||
|
|
8e39b67ee9 | ||
|
|
289cbe5bfc | ||
|
|
7143b877c8 | ||
|
|
e2a78100af | ||
|
|
ca77467e48 | ||
|
|
d3cbfcd4da | ||
|
|
3648ef28d7 | ||
|
|
689b99c3aa | ||
|
|
6c40daa863 | ||
|
|
efdc83f72a | ||
|
|
8b10534c74 | ||
|
|
bcacff2ee7 | ||
|
|
47096f7f1b | ||
|
|
c240ebcb8e | ||
|
|
ce3e59e020 | ||
|
|
4a9da40028 | ||
|
|
eab1efd5f4 | ||
|
|
c7290b614e | ||
|
|
1aea5633eb | ||
|
|
86cc719651 | ||
|
|
742a69f09f | ||
|
|
c05dee7af5 | ||
|
|
e399a49c8b | ||
|
|
b9b951e56b | ||
|
|
88bb1f4008 | ||
|
|
24e7db7ddd | ||
|
|
96ed6fe1a6 | ||
|
|
d85e52c83e | ||
|
|
d9b5fa5b16 | ||
|
|
fb948cc7f4 | ||
|
|
c358f412fd | ||
|
|
b78de6386b | ||
|
|
50c00b47ae | ||
|
|
6025c7942f | ||
|
|
23a3412154 | ||
|
|
3635f6fb0c | ||
|
|
2aadfa2ce4 | ||
|
|
dd0d78242e | ||
|
|
42c5de895c | ||
|
|
e91718e73b | ||
|
|
2c15d8b074 |
@@ -0,0 +1,2 @@
|
||||
# Format pre-vue code with eslint where possible (2026-09-10)
|
||||
d85e52c83ed3639e040372fd0052e842e4899d90
|
||||
@@ -16,8 +16,6 @@ recursive-include src/pretix/plugins/banktransfer/templates *
|
||||
recursive-include src/pretix/plugins/banktransfer/static *
|
||||
recursive-include src/pretix/plugins/manualpayment/templates *
|
||||
recursive-include src/pretix/plugins/manualpayment/static *
|
||||
recursive-include src/pretix/plugins/paypal/templates *
|
||||
recursive-include src/pretix/plugins/paypal/static *
|
||||
recursive-include src/pretix/plugins/paypal2/templates *
|
||||
recursive-include src/pretix/plugins/paypal2/static *
|
||||
recursive-include src/pretix/plugins/src/pretixdroid/templates *
|
||||
|
||||
@@ -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.
|
||||
@@ -110,7 +110,7 @@ Endpoints
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.paypal2",
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
],
|
||||
"all_sales_channels": false,
|
||||
@@ -199,7 +199,7 @@ Endpoints
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.paypal2",
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
],
|
||||
"valid_keys": {
|
||||
@@ -262,7 +262,7 @@ Endpoints
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
"pretix.plugins.paypal2"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
@@ -299,7 +299,7 @@ Endpoints
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
"pretix.plugins.paypal2"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
@@ -364,7 +364,7 @@ Endpoints
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
"pretix.plugins.paypal2"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
@@ -401,7 +401,7 @@ Endpoints
|
||||
"item_meta_properties": {},
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
"pretix.plugins.paypal2"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
@@ -438,7 +438,7 @@ Endpoints
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.paypal2",
|
||||
"pretix.plugins.pretixdroid"
|
||||
]
|
||||
}
|
||||
@@ -475,7 +475,7 @@ Endpoints
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.paypal2",
|
||||
"pretix.plugins.pretixdroid"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
@@ -566,7 +566,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://pretix.eu",
|
||||
"region": "DE",
|
||||
…
|
||||
}
|
||||
|
||||
@@ -579,12 +579,14 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url":
|
||||
"region":
|
||||
{
|
||||
"value": "https://pretix.eu",
|
||||
"label": "Imprint URL",
|
||||
"value": "DE",
|
||||
"label": "Region",
|
||||
"readonly": false,
|
||||
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
|
||||
"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)."
|
||||
}
|
||||
},
|
||||
…
|
||||
@@ -620,7 +622,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/"
|
||||
"region": "DE"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -632,7 +634,7 @@ organizer level.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"imprint_url": "https://example.org/imprint/",
|
||||
"region": "DE",
|
||||
…
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ at :ref:`plugin-docs`.
|
||||
organizers
|
||||
events
|
||||
subevents
|
||||
event_meta_properties
|
||||
taxrules
|
||||
categories
|
||||
items
|
||||
|
||||
@@ -116,6 +116,7 @@ Endpoints
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string code: Only show the voucher with the given voucher code.
|
||||
:query string search: Only show the voucher with the given query found in the code, tag, or comment.
|
||||
:query integer max_usages: Only show vouchers with the given maximal number of usages.
|
||||
:query integer redeemed: Only show vouchers with the given number of redemptions. Note that this doesn't tell you if
|
||||
the voucher can still be redeemed, as this also depends on ``max_usages``. See the
|
||||
|
||||
+58
-1
@@ -8,7 +8,64 @@ import vuePug from 'eslint-plugin-vue-pug'
|
||||
|
||||
const ignores = globalIgnores([
|
||||
'**/node_modules',
|
||||
'**/dist'
|
||||
'**/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',
|
||||
])
|
||||
|
||||
export default defineConfig([
|
||||
|
||||
Generated
+27
-13
@@ -294,29 +294,43 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/types": "^0.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node": {
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
|
||||
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.1",
|
||||
"@humanfs/core": "^0.19.2",
|
||||
"@humanfs/types": "^0.15.0",
|
||||
"@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",
|
||||
@@ -3380,9 +3394,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz",
|
||||
"integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4148,9 +4162,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
|
||||
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz",
|
||||
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
|
||||
+9
-9
@@ -33,14 +33,14 @@ dependencies = [
|
||||
"bleach==6.4.*",
|
||||
"celery==5.6.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=50.0.0",
|
||||
"cryptography>=50.0.1",
|
||||
"css-inline==0.21.*",
|
||||
"defusedcsv>=3.0.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==5.2.*",
|
||||
"Django[argon2]==5.2.*,>=5.2.17",
|
||||
"django-bootstrap3==26.2",
|
||||
"django-compressor==4.6.0",
|
||||
"django-countries==9.0.*",
|
||||
"django-countries==9.1.*",
|
||||
"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.7.*",
|
||||
"django-statici18n==2.8.*",
|
||||
"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.13.*",
|
||||
"PyJWT==2.14.*",
|
||||
"phonenumberslite==9.0.*",
|
||||
"Pillow==12.3.*",
|
||||
"pretix-plugin-build",
|
||||
@@ -84,7 +84,7 @@ dependencies = [
|
||||
"pycountry",
|
||||
"pycparser==3.0",
|
||||
"pycryptodome==3.23.*",
|
||||
"pypdf==6.5.*",
|
||||
"pypdf==6.19.*",
|
||||
"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.68.*",
|
||||
"sentry-sdk==2.69.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -112,10 +112,10 @@ dev = [
|
||||
"aiohttp==3.14.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.37.*",
|
||||
"fakeredis==2.38.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==8.0.*",
|
||||
"isort==9.0.*",
|
||||
"pep8-naming==0.15.*",
|
||||
"potypo",
|
||||
"pytest-asyncio>=1.4.0",
|
||||
|
||||
@@ -26,7 +26,6 @@ ignore =
|
||||
src/tests/plugins/*
|
||||
src/tests/plugins/badges/*
|
||||
src/tests/plugins/banktransfer/*
|
||||
src/tests/plugins/paypal/*
|
||||
src/tests/plugins/paypal2/*
|
||||
src/tests/plugins/pretixdroid/*
|
||||
src/tests/plugins/stripe/*
|
||||
|
||||
@@ -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 value option 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 property value options must be a dict.")
|
||||
|
||||
if not isinstance(data.get("key"), str):
|
||||
raise ValidationError("Meta property value options must have a key of type string.")
|
||||
|
||||
if any(k not in {"key", "label"} for k in data.keys()):
|
||||
raise ValidationError("Meta property value options 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,7 @@ with scopes_disabled():
|
||||
class VoucherFilter(FilterSet):
|
||||
active = BooleanFilter(method='filter_active')
|
||||
code = CharFilter(lookup_expr='iexact')
|
||||
search = CharFilter(method='search_qs')
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
@@ -54,6 +55,9 @@ with scopes_disabled():
|
||||
return queryset.filter(Q(redeemed__gte=F('max_usages')) |
|
||||
(Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
|
||||
|
||||
def search_qs(self, qs, name, value):
|
||||
return qs.filter(Q(code__icontains=value) | Q(tag__icontains=value) | Q(comment__icontains=value))
|
||||
|
||||
|
||||
class VoucherViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = VoucherSerializer
|
||||
|
||||
@@ -27,7 +27,7 @@ from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import List, Optional, Protocol
|
||||
|
||||
import sentry_sdk
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError, transaction
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -236,7 +236,9 @@ class OutboundSyncProvider:
|
||||
# model changes saved by set_sync_error / clear_in_flight calls below
|
||||
if sq.failed_attempts >= self.max_attempts:
|
||||
logger.exception('Failed to sync order (max attempts exceeded)')
|
||||
sentry_sdk.capture_exception(e)
|
||||
if settings.SENTRY_ENABLED:
|
||||
import sentry_sdk
|
||||
sentry_sdk.capture_exception(e)
|
||||
sq.set_sync_error("exceeded", e.messages, e.full_message)
|
||||
else:
|
||||
logger.info(
|
||||
@@ -247,7 +249,9 @@ class OutboundSyncProvider:
|
||||
sq.clear_in_flight()
|
||||
except Exception as e:
|
||||
logger.exception('Failed to sync order (unhandled exception)')
|
||||
sentry_sdk.capture_exception(e)
|
||||
if settings.SENTRY_ENABLED:
|
||||
import sentry_sdk
|
||||
sentry_sdk.capture_exception(e)
|
||||
sq.set_sync_error("internal", [], str(e))
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -54,6 +54,7 @@ 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 (
|
||||
@@ -197,7 +198,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
def iterate_sheet(self, form_data, sheet):
|
||||
_ = gettext
|
||||
if sheet == 'invoices':
|
||||
yield [
|
||||
headers = [
|
||||
_('Invoice number'),
|
||||
_('Date'),
|
||||
_('Order code'),
|
||||
@@ -230,8 +231,18 @@ 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,
|
||||
@@ -242,7 +253,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'
|
||||
@@ -280,7 +291,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
if mid:
|
||||
pmis.append(mid)
|
||||
pmi = '\n'.join(pmis)
|
||||
yield [
|
||||
line = [
|
||||
i.full_invoice_no,
|
||||
date_format(i.date, "SHORT_DATE_FORMAT"),
|
||||
i.order.code,
|
||||
@@ -315,8 +326,20 @@ 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'),
|
||||
|
||||
@@ -350,16 +350,22 @@ class WrappedPhonePrefixSelect(Select):
|
||||
return super().render(name, value or self.initial, *args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
if value and self.choices[1][0] != value:
|
||||
matching_choices = len([1 for p, c in self.choices if p == value])
|
||||
# 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 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 aditional choice value with
|
||||
# the US. As a workaround, we detect this case and add an additional choice value with
|
||||
# just <option value="+1">+1</option> without an explicit country.
|
||||
self.choices.insert(1, (value, value))
|
||||
self.choices = [
|
||||
choices[0],
|
||||
(value, value),
|
||||
*choices[1:],
|
||||
]
|
||||
context = super().get_context(name, value, attrs)
|
||||
return context
|
||||
|
||||
@@ -1196,8 +1202,9 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
return field
|
||||
|
||||
def clean(self):
|
||||
from pretix.base.addressvalidation import \
|
||||
validate_address # local import to prevent impact on startup time
|
||||
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time
|
||||
validate_address,
|
||||
)
|
||||
|
||||
d = super().clean()
|
||||
|
||||
@@ -1438,8 +1445,9 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.fields['transmission_type'].widget.attrs['data-trigger-address-info'] = 'on'
|
||||
|
||||
def clean(self):
|
||||
from pretix.base.addressvalidation import \
|
||||
validate_address # local import to prevent impact on startup time
|
||||
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time
|
||||
validate_address,
|
||||
)
|
||||
|
||||
data = self.cleaned_data
|
||||
|
||||
@@ -1493,7 +1501,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
"vat_id": _("This field is required.")
|
||||
})
|
||||
|
||||
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
|
||||
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')):
|
||||
pass # Skip re-validation if it is validated
|
||||
elif self.validate_vat_id and vat_id_applicable:
|
||||
try:
|
||||
|
||||
@@ -82,8 +82,8 @@ class UserSettingsForm(forms.ModelForm):
|
||||
class User2FADeviceAddForm(forms.Form):
|
||||
name = forms.CharField(label=_('Device name'), max_length=64)
|
||||
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
|
||||
('totp', _('Smartphone with the Authenticator application')),
|
||||
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
||||
('otp_totp.totpdevice', _('Smartphone with the Authenticator application')),
|
||||
('pretixbase.webauthndevice', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -107,6 +107,9 @@ 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 = []
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# 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,
|
||||
),
|
||||
]
|
||||
@@ -166,6 +166,7 @@ class Device(LoggedModel):
|
||||
)
|
||||
security_profile = models.CharField(
|
||||
max_length=190,
|
||||
verbose_name=_('Security profile'),
|
||||
default='full',
|
||||
null=True,
|
||||
blank=False
|
||||
|
||||
+40
-34
@@ -801,6 +801,18 @@ 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):
|
||||
@@ -1153,11 +1165,10 @@ class Renderer:
|
||||
elif o['type'] == "poweredby":
|
||||
self._draw_poweredby(canvas, op, o)
|
||||
if self.bg_pdf:
|
||||
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):
|
||||
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):
|
||||
# swap dimensions due to pdf being rotated
|
||||
page_size = page_size[::-1]
|
||||
canvas.setPageSize(page_size)
|
||||
@@ -1232,9 +1243,7 @@ class Renderer:
|
||||
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = self.bg_pdf.pages[i]
|
||||
_correct_page_media_box(bg_page)
|
||||
page.merge_page(bg_page, over=False)
|
||||
output.add_page(page)
|
||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
@@ -1299,39 +1308,36 @@ 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]
|
||||
_correct_page_media_box(bg_page)
|
||||
page.merge_page(bg_page, over=False)
|
||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
def _correct_page_media_box(page: pypdf.PageObject):
|
||||
if page.rotation != 0:
|
||||
page.transfer_rotation_to_content()
|
||||
media_box = page.mediabox
|
||||
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."""
|
||||
trsf = pypdf.Transformation()
|
||||
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]),
|
||||
))
|
||||
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)
|
||||
|
||||
|
||||
@deconstructible
|
||||
|
||||
File diff suppressed because one or more lines are too long
+22
-10
@@ -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, InvoiceAddress, OrderPayment,
|
||||
OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
|
||||
CachedCombinedTicket, CachedTicket, Event, Invoice, InvoiceAddress,
|
||||
OrderPayment, OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
|
||||
)
|
||||
from pretix.base.services.invoices import invoice_pdf_task
|
||||
from pretix.base.signals import register_data_shredders
|
||||
@@ -598,18 +598,30 @@ 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
|
||||
i.introductory_text = "█"
|
||||
i.additional_text = "█"
|
||||
i.invoice_to = "█"
|
||||
i.payment_provider_text = "█"
|
||||
i.transmission_info = {"_shredded": True}
|
||||
i.save()
|
||||
i.lines.update(description="█")
|
||||
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="█")
|
||||
|
||||
|
||||
class CachedTicketShredder(BaseDataShredder):
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <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
|
||||
@@ -35,32 +36,32 @@ register = template.Library()
|
||||
|
||||
|
||||
@register.filter("money")
|
||||
def money_filter(value: Decimal, arg='', hide_currency=False):
|
||||
if isinstance(value, (float, int)):
|
||||
def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_currency=False):
|
||||
if isinstance(value, (float, int, str)):
|
||||
if value == '':
|
||||
return value
|
||||
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()
|
||||
|
||||
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 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)
|
||||
|
||||
if hide_currency:
|
||||
return floatformat(value, f"{places}g")
|
||||
return floatformat(value, f"{render_places}g")
|
||||
|
||||
try:
|
||||
locale = Locale(get_babel_locale())
|
||||
@@ -68,14 +69,29 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
||||
locale = "en"
|
||||
|
||||
try:
|
||||
return format_currency(value, arg, locale=locale)
|
||||
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,
|
||||
)
|
||||
except:
|
||||
return '{} {}'.format(arg, floatformat(value, f"{places}g"))
|
||||
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)
|
||||
|
||||
|
||||
@register.filter("money_numberfield")
|
||||
def money_numberfield_filter(value: Decimal, arg=''):
|
||||
if isinstance(value, (float, int)):
|
||||
def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg=''):
|
||||
if isinstance(value, (float, int, str)):
|
||||
value = Decimal(value)
|
||||
if not isinstance(value, Decimal):
|
||||
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
|
||||
@@ -87,15 +103,28 @@ def money_numberfield_filter(value: Decimal, arg=''):
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def tax_rate_format(number):
|
||||
def tax_rate_format(number: Optional[Decimal | float | int | str]):
|
||||
"""
|
||||
Display a Decimal to its significant decimal places, used for tax rates.
|
||||
"""
|
||||
assert isinstance(number, Decimal)
|
||||
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()
|
||||
return mark_safe(
|
||||
formats.number_format(
|
||||
number.normalize(),
|
||||
-number.as_tuple().exponent,
|
||||
number,
|
||||
-number.normalize().as_tuple().exponent,
|
||||
use_l10n=True,
|
||||
force_grouping=False,
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.conf import settings
|
||||
from django.http import (
|
||||
HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError,
|
||||
)
|
||||
@@ -27,7 +28,6 @@ from django.template import TemplateDoesNotExist, loader
|
||||
from django.template.loader import get_template
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.translation import gettext as _
|
||||
from sentry_sdk import last_event_id
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.middleware import get_language_from_request
|
||||
@@ -106,9 +106,14 @@ def server_error(request):
|
||||
template = loader.get_template('500.html')
|
||||
except TemplateDoesNotExist:
|
||||
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
|
||||
if settings.SENTRY_ENABLED:
|
||||
from sentry_sdk import last_event_id
|
||||
sentry_id = last_event_id()
|
||||
else:
|
||||
sentry_id = None
|
||||
r = HttpResponseServerError(template.render({
|
||||
'request': request,
|
||||
'sentry_event_id': last_event_id(),
|
||||
'sentry_event_id': sentry_id,
|
||||
}))
|
||||
r.xframe_options_exempt = True
|
||||
return r
|
||||
|
||||
@@ -172,7 +172,9 @@ 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
|
||||
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
|
||||
if not v.allowed_for_session(self.request):
|
||||
v = None
|
||||
return v
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
@@ -244,6 +246,11 @@ 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
|
||||
|
||||
@@ -271,6 +278,8 @@ 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
|
||||
@@ -294,6 +303,8 @@ 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
|
||||
|
||||
@@ -400,10 +400,10 @@ class EventMetaValueForm(forms.ModelForm):
|
||||
if self.disabled:
|
||||
self.fields['value'].widget.attrs['readonly'] = 'readonly'
|
||||
|
||||
def clean_slug(self):
|
||||
def clean_value(self):
|
||||
if self.disabled:
|
||||
return self.instance.value if self.instance else None
|
||||
return self.cleaned_data['slug']
|
||||
return self.cleaned_data['value']
|
||||
|
||||
class Meta:
|
||||
model = EventMetaValue
|
||||
@@ -1635,10 +1635,15 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
|
||||
|
||||
for k, v in list(self.fields.items()):
|
||||
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
||||
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
|
||||
# the user interface with it
|
||||
del self.fields[k]
|
||||
if k.endswith('_attendee'):
|
||||
if not event.settings.attendee_emails_asked:
|
||||
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
|
||||
# the user interface with it
|
||||
del self.fields[k]
|
||||
elif 'subject' in k and k.replace("subject", "send") in self.fields:
|
||||
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("subject", "send")}'
|
||||
elif 'text' in k and k.replace("text", "send") in self.fields:
|
||||
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("text", "send")}'
|
||||
|
||||
|
||||
class TicketSettingsForm(SettingsForm):
|
||||
|
||||
@@ -105,12 +105,12 @@ class GlobalSettingsForm(SettingsForm):
|
||||
domain=settings.SITE_URL
|
||||
)
|
||||
)),
|
||||
('widget_vite_origins', forms.CharField(
|
||||
('widget_vue2_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="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.",
|
||||
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.",
|
||||
))
|
||||
])
|
||||
responses = register_global_settings.send(self)
|
||||
|
||||
@@ -87,6 +87,7 @@ 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
|
||||
@@ -134,6 +135,7 @@ 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
|
||||
|
||||
@@ -435,10 +435,10 @@ class SubEventMetaValueForm(forms.ModelForm):
|
||||
if self.disabled:
|
||||
self.fields['value'].widget.attrs['readonly'] = 'readonly'
|
||||
|
||||
def clean_slug(self):
|
||||
def clean_value(self):
|
||||
if self.disabled:
|
||||
return self.instance.value if self.instance else None
|
||||
return self.cleaned_data['slug']
|
||||
return self.cleaned_data['value']
|
||||
|
||||
class Meta:
|
||||
model = SubEventMetaValue
|
||||
|
||||
@@ -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.'),
|
||||
@@ -774,6 +778,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
||||
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
|
||||
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
|
||||
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
|
||||
'pretix.user.settings.2fa.resetdrift': _('Drift and throttle values for two-factor devices have been reset.'),
|
||||
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
|
||||
'your account.'),
|
||||
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
|
||||
|
||||
@@ -85,8 +85,8 @@ class PermissionMiddleware:
|
||||
"user.settings.2fa.enable",
|
||||
"user.settings.2fa.disable",
|
||||
"user.settings.2fa.regenemergency",
|
||||
"user.settings.2fa.confirm.totp",
|
||||
"user.settings.2fa.confirm.webauthn",
|
||||
"user.settings.2fa.confirm.otp_totp.totpdevice",
|
||||
"user.settings.2fa.confirm.pretixbase.webauthndevice",
|
||||
"user.settings.2fa.delete",
|
||||
"user.settings.2fa.leaveteams",
|
||||
"auth.logout",
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</p>
|
||||
|
||||
<div class="form-group buttons">
|
||||
<input type="submit" class="btn btn-large btn-default" value="Cancel"/>
|
||||
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/>
|
||||
<input type="submit" class="btn btn-large btn-default" value="{% trans "Cancel" %}"/>
|
||||
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
|
||||
<input type="radio" required value="otp_totp.totpdevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "otp_totp.totpdevice" %}checked{% endif %}>
|
||||
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
|
||||
<input type="radio" required value="pretixbase.webauthndevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "pretixbase.webauthndevice" %}checked{% endif %}>
|
||||
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -116,14 +116,14 @@
|
||||
{% for d in devices %}
|
||||
<li class="list-group-item">
|
||||
<a class="btn btn-danger btn-xs pull-right flip"
|
||||
href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}">
|
||||
href="{% url "control:user.settings.2fa.delete" devicetype=d.model_label device=d.pk %}">
|
||||
Delete
|
||||
</a>
|
||||
{% if d.devicetype == "totp" %}
|
||||
{% if d.model_label == "otp_totp.totpdevice" %}
|
||||
<span class="fa fa-mobile"></span>
|
||||
{% elif d.devicetype == "webauthn" %}
|
||||
{% elif d.model_label == "pretixbase.webauthndevice" %}
|
||||
<span class="fa fa-usb"></span>
|
||||
{% elif d.devicetype == "u2f" %}
|
||||
{% elif d.model_label == "pretixbase.u2fdevice" %}
|
||||
<span class="fa fa-usb"></span>
|
||||
{% endif %}
|
||||
{{ d.name }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load icon %}
|
||||
{% block title %}{% trans "User" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "User" %} {{ user.email }}</h1>
|
||||
@@ -59,8 +60,83 @@
|
||||
{% bootstrap_field form.is_verified layout='control' %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.last_login layout='control' %}
|
||||
{% bootstrap_field form.require_2fa layout='control' %}
|
||||
{% bootstrap_field form.needs_password_change layout='control' %}
|
||||
{% bootstrap_field form.require_2fa layout='control' %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<button class="btn btn-default btn-xs pull-right" type="submit" form="resetdriftthrottle">
|
||||
{% trans "Reset drift and throttle" %}
|
||||
</button>
|
||||
<h3 class="panel-title">
|
||||
{% trans "Available two-factor authentication methods" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if d.model_label == 'otp_totp.totpdevice' %}
|
||||
TOTP
|
||||
{% elif d.model_label == 'pretixbase.u2fdevice' %}
|
||||
U2F
|
||||
{% elif d.model_label == 'pretixbase.webauthndevice' %}
|
||||
WebAuthn
|
||||
{% elif d.model_label == 'otp_static.staticdevice' %}
|
||||
{% trans "Emergency tokens" %}
|
||||
{% endif %}
|
||||
{% if d.confirmed %}
|
||||
{% icon "check" %}
|
||||
{% else %}
|
||||
{% icon "warning" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ d.name }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.throttling_failure_timestamp %}
|
||||
{% blocktrans trimmed with date=d.throttling_failure_timestamp|date:"SHORT_DATETIME_FORMAT" count cnt=d.throttling_failure_count %}
|
||||
1 failed attempt since {{ date }}
|
||||
{% plural %}
|
||||
{{ cnt }} failed attempts since {{ date }}
|
||||
{% endblocktrans %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if d.throttling_enabled and not d.verify_is_allowed.0 %}
|
||||
<strong>
|
||||
{% blocktrans trimmed with date=d.verify_is_allowed.1.locked_until|date:"SHORT_DATETIME_FORMAT" %}
|
||||
Currently locked until {{ date }}
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if d.model_label == 'otp_totp.totpdevice' %}
|
||||
<small>
|
||||
<code>step = {{ d.step }},
|
||||
t0 = {{ d.t0 }},
|
||||
digits = {{ d.digits }},
|
||||
tolerance = {{ d.tolerance }},
|
||||
drift = {{ d.drift }},
|
||||
last_t = {{ d.last_t }}</code>
|
||||
</small>
|
||||
{% elif d.model_label == 'pretixbase.u2fdevice' %}
|
||||
<small>
|
||||
<code>sign_count = {{ d.sign_count }}</code>
|
||||
</small>
|
||||
{% elif d.model_label == 'otp_static.staticdevice' %}
|
||||
<small>
|
||||
<code>token_count = {{ d.token_set.count }}</code>
|
||||
</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Team memberships" %}</legend>
|
||||
@@ -102,4 +178,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{% url "control:users.resetdriftthrottle" id=user.pk %}" id="resetdriftthrottle" method="post">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -78,6 +78,7 @@ urlpatterns = [
|
||||
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
|
||||
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
|
||||
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
|
||||
re_path(r'^users/(?P<id>\d+)/resetdriftthrottle$', users.Reset2FADriftThrottleView.as_view(), name='users.resetdriftthrottle'),
|
||||
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
|
||||
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
|
||||
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
|
||||
@@ -106,9 +107,9 @@ urlpatterns = [
|
||||
re_path(r'^settings/2fa/regenemergency', user.User2FARegenerateEmergencyView.as_view(),
|
||||
name='user.settings.2fa.regenemergency'),
|
||||
re_path(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
|
||||
name='user.settings.2fa.confirm.totp'),
|
||||
name='user.settings.2fa.confirm.otp_totp.totpdevice'),
|
||||
re_path(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
|
||||
name='user.settings.2fa.confirm.webauthn'),
|
||||
name='user.settings.2fa.confirm.pretixbase.webauthndevice'),
|
||||
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
|
||||
name='user.settings.2fa.delete'),
|
||||
re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from urllib.parse import quote, urljoin, urlparse
|
||||
|
||||
@@ -50,11 +51,12 @@ 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.translation import gettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, ngettext
|
||||
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 match_token
|
||||
from django_otp import devices_for_user
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from webauthn.helpers import generate_challenge
|
||||
|
||||
@@ -64,6 +66,7 @@ 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
|
||||
@@ -395,15 +398,17 @@ class Recover(TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.form.is_valid():
|
||||
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()
|
||||
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()
|
||||
messages.success(request, _('You can now login using your new password.'))
|
||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||
|
||||
@@ -460,6 +465,7 @@ 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']
|
||||
|
||||
@@ -515,12 +521,28 @@ class Login2FAView(TemplateView):
|
||||
valid = True
|
||||
break
|
||||
else:
|
||||
valid = match_token(self.user, token)
|
||||
if isinstance(valid, StaticDevice):
|
||||
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):
|
||||
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)
|
||||
@@ -533,7 +555,23 @@ class Login2FAView(TemplateView):
|
||||
return redirect('control:index')
|
||||
else:
|
||||
pretix_failed_logins.inc(1, reason="2fa")
|
||||
messages.error(request, _('Invalid code, please try again.'))
|
||||
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)
|
||||
return redirect('control:auth.login.2fa')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -163,6 +163,9 @@ 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)
|
||||
|
||||
|
||||
@@ -1177,6 +1177,8 @@ 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
|
||||
@@ -1206,6 +1208,8 @@ 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
|
||||
@@ -1255,6 +1259,8 @@ 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
|
||||
@@ -1271,6 +1277,9 @@ 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,
|
||||
@@ -1286,10 +1295,13 @@ 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
|
||||
@@ -1313,6 +1325,8 @@ 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
|
||||
@@ -1342,7 +1356,12 @@ class OrderRefundView(OrderView):
|
||||
))
|
||||
|
||||
any_success = False
|
||||
if refund_selected == full_refund and is_valid:
|
||||
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:
|
||||
for r in refunds:
|
||||
r.save()
|
||||
order.log_action('pretix.event.order.refund.created', {
|
||||
@@ -1414,9 +1433,6 @@ 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():
|
||||
|
||||
@@ -778,9 +778,9 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["events"] = self.request.user.get_events_with_permission(
|
||||
"event.settings.general:write", request=self.request
|
||||
).filter(organizer=self.request.organizer)
|
||||
# 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["initial"] = {
|
||||
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ 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
|
||||
@@ -193,6 +194,7 @@ 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()))
|
||||
@@ -218,6 +220,7 @@ 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()
|
||||
@@ -303,5 +306,7 @@ 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
|
||||
|
||||
@@ -1276,6 +1276,11 @@ 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']
|
||||
|
||||
@@ -59,6 +59,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||
from django_otp import devices_for_user
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_scopes import scopes_disabled
|
||||
@@ -85,7 +86,6 @@ from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||
from pretix.helpers.security import session_reauth
|
||||
from pretix.helpers.u2f import websafe_encode
|
||||
|
||||
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -313,17 +313,7 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
except StaticDevice.DoesNotExist:
|
||||
ctx['static_tokens_device'] = None
|
||||
|
||||
ctx['devices'] = []
|
||||
for dt in REAL_DEVICE_TYPES:
|
||||
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
|
||||
for obj in objs:
|
||||
if dt == TOTPDevice:
|
||||
obj.devicetype = 'totp'
|
||||
elif dt == U2FDevice:
|
||||
obj.devicetype = 'u2f'
|
||||
elif dt == WebAuthnDevice:
|
||||
obj.devicetype = 'webauthn'
|
||||
ctx['devices'] += objs
|
||||
ctx['devices'] = [d for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)]
|
||||
|
||||
ctx['obligatory'] = None
|
||||
if settings.PRETIX_OBLIGATORY_2FA is True:
|
||||
@@ -342,9 +332,9 @@ class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
|
||||
template_name = 'pretixcontrol/user/2fa_add.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.cleaned_data['devicetype'] == 'totp':
|
||||
if form.cleaned_data['devicetype'] == 'otp_totp.totpdevice':
|
||||
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
|
||||
elif form.cleaned_data['devicetype'] == 'webauthn':
|
||||
elif form.cleaned_data['devicetype'] == 'pretixbase.webauthndevice':
|
||||
if not self.request.is_secure():
|
||||
messages.error(self.request,
|
||||
_('Security devices are only available if pretix is served via HTTPS.'))
|
||||
@@ -364,11 +354,11 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
|
||||
@cached_property
|
||||
def device(self):
|
||||
if self.kwargs['devicetype'] == 'totp':
|
||||
if self.kwargs['devicetype'] == 'otp_totp.totpdevice':
|
||||
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||
elif self.kwargs['devicetype'] == 'webauthn':
|
||||
elif self.kwargs['devicetype'] == 'pretixbase.webauthndevice':
|
||||
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||
elif self.kwargs['devicetype'] == 'u2f':
|
||||
elif self.kwargs['devicetype'] == 'pretixbase.u2fdevice':
|
||||
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -386,7 +376,7 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
msgs = [
|
||||
_('A two-factor authentication device has been removed from your account.')
|
||||
]
|
||||
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
|
||||
if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)):
|
||||
self.request.user.require_2fa = False
|
||||
self.request.user.save()
|
||||
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
|
||||
@@ -461,7 +451,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
).first()
|
||||
if credential_id_exists:
|
||||
messages.error(request, _('This security device is already registered.'))
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
@@ -475,7 +465,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
self.device.save()
|
||||
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
||||
'id': self.device.pk,
|
||||
'devicetype': 'u2f',
|
||||
'devicetype': 'pretixbase.webauthndevice',
|
||||
'name': self.device.name,
|
||||
})
|
||||
notices = [
|
||||
@@ -503,7 +493,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
except Exception:
|
||||
messages.error(request, _('The registration could not be completed. Please try again.'))
|
||||
logger.exception('WebAuthn registration failed')
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
@@ -537,7 +527,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
|
||||
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
|
||||
'id': self.device.pk,
|
||||
'name': self.device.name,
|
||||
'devicetype': 'totp'
|
||||
'devicetype': 'otp_totp.totpdevice'
|
||||
})
|
||||
notices = [
|
||||
_('A new two-factor authentication device has been added to your account.')
|
||||
@@ -563,7 +553,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
|
||||
else:
|
||||
messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
|
||||
'that the date and time of your phone are configured correctly.'))
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.totp', kwargs={
|
||||
return redirect(reverse('control:user.settings.2fa.confirm.otp_totp.totpdevice', kwargs={
|
||||
'device': self.device.pk
|
||||
}))
|
||||
|
||||
@@ -594,7 +584,7 @@ class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_enable.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
|
||||
if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)):
|
||||
messages.error(request, _('Please configure at least one device before enabling two-factor '
|
||||
'authentication.'))
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
@@ -40,6 +40,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import ListView, TemplateView
|
||||
from django_otp import devices_for_user
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from hijack import signals
|
||||
|
||||
@@ -107,6 +108,9 @@ class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationReq
|
||||
ctx['backend'] = (
|
||||
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
|
||||
)
|
||||
|
||||
ctx['devices'] = devices_for_user(self.object)
|
||||
|
||||
return ctx
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -183,6 +187,25 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
|
||||
return reverse('control:users.edit', kwargs=self.kwargs)
|
||||
|
||||
|
||||
class Reset2FADriftThrottleView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect(reverse('control:users.edit', kwargs=self.kwargs))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
|
||||
self.object.totpdevice_set.update(drift=0, throttling_failure_timestamp=None, throttling_failure_count=0)
|
||||
self.object.staticdevice_set.update(throttling_failure_timestamp=None, throttling_failure_count=0)
|
||||
self.object.log_action('pretix.user.settings.2fa.resetdrift', user=self.request.user)
|
||||
messages.success(request, _(
|
||||
'The drift values for TOTP devices have been reset.'
|
||||
))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:users.edit', kwargs=self.kwargs)
|
||||
|
||||
|
||||
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
|
||||
template_name = "pretixcontrol/users/anonymize.html"
|
||||
|
||||
|
||||
@@ -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')
|
||||
_('Comment'), _('Budget'), _('Budget used')
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
@@ -170,7 +170,9 @@ 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.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 "",
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
@@ -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 ',
|
||||
'Replace this usage with mainreverse_absolute.',
|
||||
DeprecationWarning
|
||||
)
|
||||
return mainreverse_absolute(urlname, args, kwargs)
|
||||
|
||||
@@ -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-06-25 14:00+0000\n"
|
||||
"Last-Translator: Kim Lozano <joaquim.lozano@upc.edu>\n"
|
||||
"PO-Revision-Date: 2026-09-20 14:00+0000\n"
|
||||
"Last-Translator: Rita Gimenez <barcelonamusictech@gmail.com>\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.6.1\n"
|
||||
"X-Generator: Weblate 2026.9.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -37,11 +37,11 @@ msgstr "Àrab"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Basque"
|
||||
msgstr ""
|
||||
msgstr "Basc"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
msgstr "Català"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Chinese (simplified)"
|
||||
@@ -49,11 +49,11 @@ msgstr "Xinès (simplificat)"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Chinese (traditional)"
|
||||
msgstr ""
|
||||
msgstr "Xinès (tradicional)"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
msgstr "Txec"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Croatian"
|
||||
@@ -81,7 +81,7 @@ msgstr "Finlandès"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Galician"
|
||||
msgstr ""
|
||||
msgstr "Gallec"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Greek"
|
||||
@@ -89,15 +89,15 @@ msgstr "Grec"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
msgstr "Hebreu"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
msgstr "Hongarès"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
msgstr ""
|
||||
msgstr "Indonesi"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Italian"
|
||||
@@ -105,7 +105,7 @@ msgstr "Italià"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
msgstr "Japonès"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Latvian"
|
||||
@@ -113,7 +113,7 @@ msgstr "Letó"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Norwegian Bokmål"
|
||||
msgstr ""
|
||||
msgstr "Noruec"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Polish"
|
||||
@@ -129,7 +129,7 @@ msgstr "Portuguès (Brasil)"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Romanian"
|
||||
msgstr ""
|
||||
msgstr "Romanès"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Russian"
|
||||
@@ -137,11 +137,11 @@ msgstr "Rus"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Slovak"
|
||||
msgstr ""
|
||||
msgstr "Eslovac"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
msgstr "Suec"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Spanish"
|
||||
@@ -149,11 +149,11 @@ msgstr "Espanyol"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Spanish (Latin America)"
|
||||
msgstr ""
|
||||
msgstr "Espanyol (Llatinoamèrica)"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Thai"
|
||||
msgstr ""
|
||||
msgstr "Tailandès"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Turkish"
|
||||
@@ -161,13 +161,15 @@ msgstr "Turc"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Ukrainian"
|
||||
msgstr ""
|
||||
msgstr "Ucraïnès"
|
||||
|
||||
#: 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"
|
||||
@@ -309,7 +311,7 @@ msgstr "La cistella és buida."
|
||||
|
||||
#: pretix/api/serializers/item.py
|
||||
msgid "The program end must not be empty."
|
||||
msgstr ""
|
||||
msgstr "El final del programa no ha de ser buit."
|
||||
|
||||
#: pretix/api/serializers/item.py pretix/base/models/items.py
|
||||
#, fuzzy
|
||||
@@ -339,7 +341,7 @@ msgstr ""
|
||||
|
||||
#: pretix/api/serializers/item.py
|
||||
msgid "Only admission products can currently be personalized."
|
||||
msgstr ""
|
||||
msgstr "Actualment, només es poden personalitzar els productes d'accés."
|
||||
|
||||
#: pretix/api/serializers/item.py
|
||||
msgid ""
|
||||
@@ -434,7 +436,7 @@ msgstr ""
|
||||
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
|
||||
#, python-format
|
||||
msgid "You've been invited to join %(organizer)s"
|
||||
msgstr ""
|
||||
msgstr "Has estat convidat/da a unir-te a %(organizer)s"
|
||||
|
||||
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
|
||||
msgid "This user already has been invited for this team."
|
||||
@@ -456,7 +458,7 @@ msgstr ""
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
msgid "Medium connected to other event"
|
||||
msgstr ""
|
||||
msgstr "Mitjà connectat a un altre esdeveniment"
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
#, fuzzy
|
||||
@@ -642,6 +644,8 @@ 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
|
||||
@@ -654,6 +658,9 @@ 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,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-08-25 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-09-14 23: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.8.1\n"
|
||||
"X-Generator: Weblate 2026.9.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"
|
||||
|
||||
@@ -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-08-24 14:30+0000\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+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.8.1\n"
|
||||
"X-Generator: Weblate 2026.9\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 ""
|
||||
msgstr "Eskaeraren xehetasunak"
|
||||
|
||||
#: 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 ""
|
||||
msgstr "Eskaeraren xehetasunak aldatu dira."
|
||||
|
||||
#: 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 ""
|
||||
msgstr "Eskaeraren xehetasunak: %(code)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/index.html
|
||||
|
||||
@@ -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-08-16 22:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"PO-Revision-Date: 2026-09-07 18:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\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.8.1\n"
|
||||
"X-Generator: Weblate 2026.9\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,10 +428,8 @@ 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
|
||||
@@ -5691,7 +5689,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 ""
|
||||
@@ -7340,12 +7338,9 @@ 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."
|
||||
@@ -8105,16 +8100,12 @@ 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"
|
||||
@@ -8143,22 +8134,22 @@ msgstr "未設定"
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
msgstr "相対的な日付は、「{}」に対して「before」で表すことはできません"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
msgstr "相対的な日付は「{}」に対して「after」として表すことはできません"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr ""
|
||||
msgstr "相対時間は「{}」に対して「before」で表すことはできません"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr ""
|
||||
msgstr "相対時間は「{}」に対して「after」で表すことはできません"
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
@@ -10067,12 +10058,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"
|
||||
@@ -10083,6 +10074,10 @@ 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"
|
||||
@@ -13902,26 +13897,20 @@ 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"
|
||||
@@ -22533,28 +22522,24 @@ 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"
|
||||
@@ -22573,28 +22558,24 @@ 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 ""
|
||||
msgstr "注文ごとの質問は現在サポートされておらず、pretixPOSでは表示されません。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "The question has been reordered."
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr "質問の並び順が変更されました。"
|
||||
msgstr "これらの質問は、注文ごとに1回ずつ尋ねられます。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
@@ -23186,10 +23167,8 @@ 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"
|
||||
@@ -24856,14 +24835,13 @@ msgid "Hardware model"
|
||||
msgstr "ハードウェアの機種"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Begin: %(time)s"
|
||||
#, python-format
|
||||
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"
|
||||
@@ -28220,19 +28198,14 @@ 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."
|
||||
@@ -30906,7 +30879,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr ""
|
||||
msgstr "コンプライアンス保留の間、追加の支払いを許可する"
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
@@ -30916,16 +30889,20 @@ 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 --"
|
||||
@@ -31190,6 +31167,10 @@ 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 ""
|
||||
|
||||
@@ -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-03-30 21:00+0000\n"
|
||||
"PO-Revision-Date: 2026-09-02 08:35+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 5.16.2\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -93,7 +93,7 @@ msgstr "Hebraico"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr ""
|
||||
msgstr "Húngaro"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
@@ -153,7 +153,7 @@ msgstr "Espanhol (América latina)"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Thai"
|
||||
msgstr ""
|
||||
msgstr "Tailandês"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Turkish"
|
||||
@@ -3022,11 +3022,12 @@ 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 ""
|
||||
msgstr "O campo \"%(label)s\" não pode conter uma URL (%(url)s)."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgctxt "phonenumber"
|
||||
@@ -8173,7 +8174,7 @@ msgstr "Permitida"
|
||||
#: pretix/base/permissions.py
|
||||
msgctxt "permission_level"
|
||||
msgid "Access existing events"
|
||||
msgstr ""
|
||||
msgstr "Acessar eventos existentes"
|
||||
|
||||
#: pretix/base/permissions.py
|
||||
msgctxt "permission_level"
|
||||
@@ -13607,11 +13608,15 @@ 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
|
||||
|
||||
@@ -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-08-09 06:00+0000\n"
|
||||
"Last-Translator: Julien <julien@circusiloveyou.com>\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
|
||||
"Last-Translator: Linnea Thelander <linnea@coeo.events>\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.8.1\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -19170,8 +19170,9 @@ 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ätta"
|
||||
msgstr "Fortsätt"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
msgid "Authorize an application"
|
||||
|
||||
@@ -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-08-09 06:00+0000\n"
|
||||
"Last-Translator: Julien <julien@circusiloveyou.com>\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
|
||||
"Last-Translator: Linnea Thelander <linnea@coeo.events>\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.8.1\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -133,8 +133,9 @@ 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ätta"
|
||||
msgstr "Fortsätt"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,6 @@ 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
|
||||
@@ -238,15 +237,8 @@ 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_page(page)
|
||||
nup_page.merge_transformed_page(page, Transformation().translate(tx, ty))
|
||||
return nup_page
|
||||
|
||||
|
||||
|
||||
@@ -1,183 +1,183 @@
|
||||
/*global $, gettext*/
|
||||
/* global gettext */
|
||||
var bankimport_transactionlist = {
|
||||
|
||||
_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;
|
||||
},
|
||||
_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
|
||||
},
|
||||
|
||||
_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"));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
_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'))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
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) {
|
||||
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_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_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);
|
||||
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)
|
||||
|
||||
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);
|
||||
});
|
||||
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)
|
||||
})
|
||||
|
||||
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) {
|
||||
var results = object.results;
|
||||
var suggs = [];
|
||||
var reslen = results.length;
|
||||
for (var 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) {
|
||||
let results = object.results
|
||||
let suggs = []
|
||||
let reslen = results.length
|
||||
for (let 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()
|
||||
})
|
||||
|
||||
@@ -19,15 +19,3 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
@@ -1,53 +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/>.
|
||||
#
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from django.core.cache import cache
|
||||
from paypalrestsdk.api import Api as VendorApi
|
||||
|
||||
|
||||
class Api(VendorApi):
|
||||
def get_token_hash(self, authorization_code=None, refresh_token=None, headers=None):
|
||||
if not authorization_code and not refresh_token:
|
||||
checksum = hashlib.sha256(self.basic_auth().encode()).hexdigest()
|
||||
cache_key_hash = f'pretix_paypal_token_hash_{checksum}'
|
||||
cache_key_request_at = f'pretix_paypal_token_request_at_{checksum}'
|
||||
token_hash = cache.get(cache_key_hash)
|
||||
if token_hash:
|
||||
token_request_at = cache.get(cache_key_request_at)
|
||||
if token_request_at:
|
||||
self.token_hash = json.loads(token_hash)
|
||||
self.token_request_at = token_request_at
|
||||
self.validate_token_hash()
|
||||
if self.token_hash is not None:
|
||||
return self.token_hash
|
||||
|
||||
t = super().get_token_hash(authorization_code, refresh_token, headers)
|
||||
|
||||
if self.token_hash:
|
||||
cache.set(cache_key_hash, json.dumps(self.token_hash), 3600 * 4)
|
||||
cache.set(cache_key_request_at, self.token_request_at, 3600 * 4)
|
||||
|
||||
return t
|
||||
|
||||
return super().get_token_hash(authorization_code, refresh_token, headers)
|
||||
@@ -1,69 +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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix import __version__ as version
|
||||
|
||||
|
||||
class PaypalApp(AppConfig):
|
||||
name = 'pretix.plugins.paypal'
|
||||
verbose_name = _("PayPal")
|
||||
|
||||
class PretixPluginMeta:
|
||||
name = _("PayPal")
|
||||
author = _("the pretix team")
|
||||
version = version
|
||||
category = 'PAYMENT'
|
||||
featured = True
|
||||
picture = 'pretixplugins/paypal/paypal_logo.svg'
|
||||
description = _("Accept payments with your PayPal account. PayPal is one of the most popular payment methods "
|
||||
"world-wide.")
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
def is_available(self, event):
|
||||
return 'pretix.plugins.paypal' in event.plugins.split(',')
|
||||
|
||||
@cached_property
|
||||
def compatibility_errors(self):
|
||||
errs = []
|
||||
try:
|
||||
import paypalrestsdk # NOQA
|
||||
except ImportError:
|
||||
errs.append("Python package 'paypalrestsdk' is not installed.")
|
||||
return errs
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.17 on 2026-09-08 08:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("paypal", "0004_bigint"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.DeleteModel(
|
||||
name="ReferencedPayPalObject",
|
||||
),
|
||||
],
|
||||
database_operations=[]
|
||||
)
|
||||
]
|
||||
@@ -1,715 +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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, Tobias Kunze
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
import paypalrestsdk
|
||||
import paypalrestsdk.exceptions
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as __, gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from paypalrestsdk.exceptions import BadRequest, UnauthorizedAccess
|
||||
from paypalrestsdk.openid_connect import Tokeninfo
|
||||
from requests import RequestException
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.views.redirect import safelink
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
from pretix.plugins.paypal.api import Api
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.paypal')
|
||||
|
||||
SUPPORTED_CURRENCIES = ['AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD', 'HUF', 'INR', 'ILS', 'JPY', 'MYR', 'MXN',
|
||||
'TWD', 'NZD', 'NOK', 'PHP', 'PLN', 'GBP', 'RUB', 'SGD', 'SEK', 'CHF', 'THB', 'USD']
|
||||
|
||||
LOCAL_ONLY_CURRENCIES = ['INR']
|
||||
|
||||
|
||||
class Paypal(BasePaymentProvider):
|
||||
identifier = 'paypal'
|
||||
verbose_name = _('PayPal')
|
||||
payment_form_fields = OrderedDict([
|
||||
])
|
||||
|
||||
def __init__(self, event: Event):
|
||||
super().__init__(event)
|
||||
self.settings = SettingsSandbox('payment', 'paypal', event)
|
||||
|
||||
@property
|
||||
def test_mode_message(self):
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
# in OAuth mode, sandbox mode needs to be set global
|
||||
is_sandbox = self.settings.connect_endpoint == 'sandbox'
|
||||
else:
|
||||
is_sandbox = self.settings.get('endpoint') == 'sandbox'
|
||||
if is_sandbox:
|
||||
return _('The PayPal sandbox is being used, you can test without actually sending money but you will need a '
|
||||
'PayPal sandbox user to log in.')
|
||||
return None
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
# PayPal connect
|
||||
if self.settings.connect_user_id:
|
||||
fields = [
|
||||
('connect_user_id',
|
||||
forms.CharField(
|
||||
label=_('PayPal account'),
|
||||
disabled=True
|
||||
)),
|
||||
]
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
fields = [
|
||||
('client_id',
|
||||
forms.CharField(
|
||||
label=_('Client ID'),
|
||||
min_length=80,
|
||||
help_text=format_html(
|
||||
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
|
||||
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html',
|
||||
)
|
||||
)),
|
||||
('secret',
|
||||
SecretKeySettingsField(
|
||||
label=_('Secret'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
)),
|
||||
('endpoint',
|
||||
forms.ChoiceField(
|
||||
label=_('Endpoint'),
|
||||
initial='live',
|
||||
choices=(
|
||||
('live', 'Live'),
|
||||
('sandbox', 'Sandbox'),
|
||||
),
|
||||
)),
|
||||
]
|
||||
|
||||
extra_fields = [
|
||||
('prefix',
|
||||
forms.CharField(
|
||||
label=_('Reference prefix'),
|
||||
help_text=_('Any value entered here will be added in front of the regular booking reference '
|
||||
'containing the order number.'),
|
||||
required=False,
|
||||
)),
|
||||
('postfix',
|
||||
forms.CharField(
|
||||
label=_('Reference postfix'),
|
||||
help_text=_('Any value entered here will be added behind the regular booking reference '
|
||||
'containing the order number.'),
|
||||
required=False,
|
||||
)),
|
||||
]
|
||||
|
||||
d = OrderedDict(
|
||||
fields + extra_fields + list(super().settings_form_fields.items())
|
||||
)
|
||||
|
||||
d.move_to_end('prefix')
|
||||
d.move_to_end('postfix')
|
||||
d.move_to_end('_enabled', False)
|
||||
return d
|
||||
|
||||
def get_connect_url(self, request):
|
||||
request.session['payment_paypal_oauth_event'] = request.event.pk
|
||||
|
||||
self.init_api()
|
||||
return Tokeninfo.authorize_url({'scope': 'openid profile email'})
|
||||
|
||||
def settings_content_render(self, request):
|
||||
settings_content = ""
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
# Use PayPal connect
|
||||
if not self.settings.connect_user_id:
|
||||
# Migrate User to PayPal v2
|
||||
self.event.disable_plugin("pretix.plugins.paypal")
|
||||
self.event.enable_plugin("pretix.plugins.paypal2")
|
||||
self.event.save()
|
||||
else:
|
||||
settings_content = (
|
||||
"<button formaction='{}' class='btn btn-danger'>{}</button>"
|
||||
).format(
|
||||
reverse('plugins:paypal:oauth.disconnect', kwargs={
|
||||
'organizer': self.event.organizer.slug,
|
||||
'event': self.event.slug,
|
||||
}),
|
||||
_('Disconnect from PayPal')
|
||||
)
|
||||
else:
|
||||
# Migrate User to PayPal v2
|
||||
self.event.disable_plugin("pretix.plugins.paypal")
|
||||
self.event.enable_plugin("pretix.plugins.paypal2")
|
||||
self.event.save()
|
||||
|
||||
return settings_content
|
||||
|
||||
def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool:
|
||||
return super().is_allowed(request, total) and self.event.currency in SUPPORTED_CURRENCIES
|
||||
|
||||
def init_api(self):
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
paypalrestsdk.api.__api__ = Api(
|
||||
mode="sandbox" if "sandbox" in self.settings.connect_endpoint else 'live',
|
||||
client_id=self.settings.connect_client_id,
|
||||
client_secret=self.settings.connect_secret_key,
|
||||
openid_client_id=self.settings.connect_client_id,
|
||||
openid_client_secret=self.settings.connect_secret_key
|
||||
)
|
||||
else:
|
||||
paypalrestsdk.api.__api__ = Api(
|
||||
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
|
||||
client_id=self.settings.get('client_id'),
|
||||
client_secret=self.settings.get('secret')
|
||||
)
|
||||
|
||||
def payment_is_valid_session(self, request):
|
||||
return (request.session.get('payment_paypal_id', '') != ''
|
||||
and request.session.get('payment_paypal_payer', '') != '')
|
||||
|
||||
def payment_form_render(self, request) -> str:
|
||||
template = get_template('pretixplugins/paypal/checkout_payment_form.html')
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
|
||||
return template.render(ctx)
|
||||
|
||||
def checkout_prepare(self, request, cart):
|
||||
self.init_api()
|
||||
kwargs = {}
|
||||
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
|
||||
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
|
||||
|
||||
try:
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
if not request.event.settings.payment_paypal_connect_user_id:
|
||||
raise PaymentException('Payment method misconfigured')
|
||||
|
||||
try:
|
||||
tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
|
||||
except BadRequest as ex:
|
||||
ex = json.loads(ex.content)
|
||||
messages.error(request, '{}: {} ({})'.format(
|
||||
_('We had trouble communicating with PayPal'),
|
||||
ex['error_description'],
|
||||
ex['correlation_id'])
|
||||
)
|
||||
return
|
||||
|
||||
# Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
|
||||
# get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
|
||||
try:
|
||||
userinfo = tokeninfo.userinfo()
|
||||
request.event.settings.payment_paypal_connect_user_id = userinfo.email
|
||||
except UnauthorizedAccess:
|
||||
pass
|
||||
|
||||
payee = {
|
||||
"email": request.event.settings.payment_paypal_connect_user_id,
|
||||
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
|
||||
# we should use it instead of the merchant's eMail-address
|
||||
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
|
||||
}
|
||||
else:
|
||||
payee = {}
|
||||
|
||||
payment = paypalrestsdk.Payment({
|
||||
'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
|
||||
'intent': 'sale',
|
||||
'payer': {
|
||||
"payment_method": "paypal",
|
||||
},
|
||||
"redirect_urls": {
|
||||
"return_url": eventreverse_absolute(request.event, 'plugins:paypal:return', kwargs=kwargs),
|
||||
"cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort', kwargs=kwargs),
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"item_list": {
|
||||
"items": [
|
||||
{
|
||||
"name": '{prefix}{orderstring}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
orderstring=__('Order for %s') % str(request.event),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
"quantity": 1,
|
||||
"price": self.format_price(cart['total']),
|
||||
"currency": request.event.currency
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"currency": request.event.currency,
|
||||
"total": self.format_price(cart['total'])
|
||||
},
|
||||
"description": __('Event tickets for {event}').format(event=request.event.name),
|
||||
"payee": payee,
|
||||
"custom": '{prefix}{slug}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
slug=request.event.slug.upper(),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
request.session['payment_paypal_payment'] = None
|
||||
return self._create_payment(request, payment)
|
||||
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('Error on creating payment: ' + str(e))
|
||||
|
||||
def format_price(self, value):
|
||||
return str(round_decimal(value, self.event.currency, {
|
||||
# PayPal behaves differently than Stripe in deciding what currencies have decimal places
|
||||
# Source https://developer.paypal.com/docs/classic/api/currency_codes/
|
||||
'HUF': 0,
|
||||
'JPY': 0,
|
||||
'MYR': 0,
|
||||
'TWD': 0,
|
||||
# However, CLPs are not listed there while PayPal requires us not to send decimal places there. WTF.
|
||||
'CLP': 0,
|
||||
# Let's just guess that the ones listed here are 0-based as well
|
||||
# https://developers.braintreepayments.com/reference/general/currencies
|
||||
'BIF': 0,
|
||||
'DJF': 0,
|
||||
'GNF': 0,
|
||||
'KMF': 0,
|
||||
'KRW': 0,
|
||||
'LAK': 0,
|
||||
'PYG': 0,
|
||||
'RWF': 0,
|
||||
'UGX': 0,
|
||||
'VND': 0,
|
||||
'VUV': 0,
|
||||
'XAF': 0,
|
||||
'XOF': 0,
|
||||
'XPF': 0,
|
||||
}))
|
||||
|
||||
@property
|
||||
def abort_pending_allowed(self):
|
||||
return False
|
||||
|
||||
def _create_payment(self, request, payment):
|
||||
if payment.create():
|
||||
if payment.state not in ('created', 'approved', 'pending'):
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.error('Invalid payment state: ' + str(payment))
|
||||
return
|
||||
request.session['payment_paypal_id'] = payment.id
|
||||
for link in payment.links:
|
||||
if link.method == "REDIRECT" and link.rel == "approval_url":
|
||||
if request.session.get('iframe_session', False):
|
||||
return safelink(link.href, framebreak=True)
|
||||
else:
|
||||
return str(link.href)
|
||||
else:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.error('Error on creating payment: ' + str(payment.error))
|
||||
|
||||
def checkout_confirm_render(self, request) -> str:
|
||||
"""
|
||||
Returns the HTML that should be displayed when the user selected this provider
|
||||
on the 'confirm order' page.
|
||||
"""
|
||||
template = get_template('pretixplugins/paypal/checkout_payment_confirm.html')
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
|
||||
return template.render(ctx)
|
||||
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
if (request.session.get('payment_paypal_id', '') == '' or request.session.get('payment_paypal_payer', '') == ''):
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
self.init_api()
|
||||
pp_payment = paypalrestsdk.Payment.find(request.session.get('payment_paypal_id'))
|
||||
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_payment.id)
|
||||
if str(pp_payment.transactions[0].amount.total) != str(payment.amount) or pp_payment.transactions[0].amount.currency \
|
||||
!= self.event.currency:
|
||||
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_payment)))
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
return self._execute_payment(pp_payment, request, payment)
|
||||
|
||||
def _execute_payment(self, payment, request, payment_obj):
|
||||
if payment.state == 'created':
|
||||
payment.replace([
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/transactions/0/item_list",
|
||||
"value": {
|
||||
"items": [
|
||||
{
|
||||
"name": '{prefix}{orderstring}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
orderstring=__('Order {slug}-{code}').format(
|
||||
slug=self.event.slug.upper(),
|
||||
code=payment_obj.order.code
|
||||
),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
"quantity": 1,
|
||||
"price": self.format_price(payment_obj.amount),
|
||||
"currency": payment_obj.order.event.currency
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/transactions/0/description",
|
||||
"value": '{prefix}{orderstring}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
orderstring=__('Order {order} for {event}').format(
|
||||
event=request.event.name,
|
||||
order=payment_obj.order.code
|
||||
),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
}
|
||||
])
|
||||
try:
|
||||
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
|
||||
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('Error on creating payment: ' + str(e))
|
||||
except RequestException as e:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('Error on creating payment: ' + str(e))
|
||||
|
||||
for trans in payment.transactions:
|
||||
for rr in trans.related_resources:
|
||||
if hasattr(rr, 'sale') and rr.sale:
|
||||
if rr.sale.state == 'pending':
|
||||
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
|
||||
'soon as the payment completed.'))
|
||||
payment_obj.info = json.dumps(payment.to_dict())
|
||||
payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment_obj.save()
|
||||
return
|
||||
|
||||
payment_obj.refresh_from_db()
|
||||
if payment.state == 'pending':
|
||||
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
|
||||
'payment completed.'))
|
||||
payment_obj.info = json.dumps(payment.to_dict())
|
||||
payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment_obj.save()
|
||||
return
|
||||
|
||||
if payment.state != 'approved':
|
||||
payment_obj.fail(info=payment.to_dict())
|
||||
logger.error('Invalid state: %s' % str(payment))
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
if payment_obj.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
logger.warning('PayPal success event even though order is already marked as paid')
|
||||
return
|
||||
|
||||
try:
|
||||
payment_obj.info = json.dumps(payment.to_dict())
|
||||
payment_obj.save(update_fields=['info'])
|
||||
payment_obj.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
return None
|
||||
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
retry = True
|
||||
try:
|
||||
if (
|
||||
payment.info
|
||||
and payment.info_data['transactions'][0]['related_resources'][0]['sale']['state'] == 'pending'
|
||||
):
|
||||
retry = False
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
template = get_template('pretixplugins/paypal/pending.html')
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
|
||||
'retry': retry, 'order': payment.order}
|
||||
return template.render(ctx)
|
||||
|
||||
def matching_id(self, payment: OrderPayment):
|
||||
sale_id = None
|
||||
for trans in payment.info_data.get('transactions', []):
|
||||
for res in trans.get('related_resources', []):
|
||||
if 'sale' in res and 'id' in res['sale']:
|
||||
sale_id = res['sale']['id']
|
||||
return sale_id or payment.info_data.get('id', None)
|
||||
|
||||
def api_payment_details(self, payment: OrderPayment):
|
||||
sale_id = None
|
||||
for trans in payment.info_data.get('transactions', []):
|
||||
for res in trans.get('related_resources', []):
|
||||
if 'sale' in res and 'id' in res['sale']:
|
||||
sale_id = res['sale']['id']
|
||||
return {
|
||||
"payer_email": payment.info_data.get('payer', {}).get('payer_info', {}).get('email'),
|
||||
"payer_id": payment.info_data.get('payer', {}).get('payer_info', {}).get('payer_id'),
|
||||
"cart_id": payment.info_data.get('cart', None),
|
||||
"payment_id": payment.info_data.get('id', None),
|
||||
"sale_id": sale_id,
|
||||
}
|
||||
|
||||
def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
|
||||
template = get_template('pretixplugins/paypal/control.html')
|
||||
sale_id = None
|
||||
for trans in payment.info_data.get('transactions', []):
|
||||
for res in trans.get('related_resources', []):
|
||||
if 'sale' in res and 'id' in res['sale']:
|
||||
sale_id = res['sale']['id']
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
|
||||
'payment_info': payment.info_data, 'order': payment.order, 'sale_id': sale_id}
|
||||
return template.render(ctx)
|
||||
|
||||
def payment_control_render_short(self, payment: OrderPayment) -> str:
|
||||
return payment.info_data.get('payer', {}).get('payer_info', {}).get('email', '')
|
||||
|
||||
def payment_partial_refund_supported(self, payment: OrderPayment):
|
||||
# Paypal refunds are possible for 180 days after purchase:
|
||||
# https://www.paypal.com/lc/smarthelp/article/how-do-i-issue-a-refund-faq780#:~:text=Refund%20after%20180%20days%20of,PayPal%20balance%20of%20the%20recipient.
|
||||
return (now() - payment.payment_date).days <= 180
|
||||
|
||||
def payment_refund_supported(self, payment: OrderPayment):
|
||||
self.payment_partial_refund_supported(payment)
|
||||
|
||||
def execute_refund(self, refund: OrderRefund):
|
||||
self.init_api()
|
||||
|
||||
try:
|
||||
sale = None
|
||||
for res in refund.payment.info_data['transactions'][0]['related_resources']:
|
||||
for k, v in res.items():
|
||||
if k == 'sale':
|
||||
sale = paypalrestsdk.Sale.find(v['id'])
|
||||
break
|
||||
|
||||
if not sale:
|
||||
pp_payment = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
|
||||
for res in pp_payment.transactions[0].related_resources:
|
||||
for k, v in res.to_dict().items():
|
||||
if k == 'sale':
|
||||
sale = paypalrestsdk.Sale.find(v['id'])
|
||||
break
|
||||
|
||||
pp_refund = sale.refund({
|
||||
"amount": {
|
||||
"total": self.format_price(refund.amount),
|
||||
"currency": refund.order.event.currency
|
||||
}
|
||||
})
|
||||
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||
refund.order.log_action('pretix.event.order.refund.failed', {
|
||||
'local_id': refund.local_id,
|
||||
'provider': refund.provider,
|
||||
'error': str(e)
|
||||
})
|
||||
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(str(e)))
|
||||
if not pp_refund.success():
|
||||
refund.order.log_action('pretix.event.order.refund.failed', {
|
||||
'local_id': refund.local_id,
|
||||
'provider': refund.provider,
|
||||
'error': str(pp_refund.error)
|
||||
})
|
||||
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(pp_refund.error))
|
||||
else:
|
||||
sale = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
|
||||
refund.payment.info = json.dumps(sale.to_dict())
|
||||
refund.info = json.dumps(pp_refund.to_dict())
|
||||
refund.done()
|
||||
|
||||
def payment_prepare(self, request, payment_obj):
|
||||
self.init_api()
|
||||
|
||||
try:
|
||||
if request.event.settings.payment_paypal_connect_user_id:
|
||||
try:
|
||||
tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
|
||||
except BadRequest as ex:
|
||||
ex = json.loads(ex.content)
|
||||
messages.error(request, '{}: {} ({})'.format(
|
||||
_('We had trouble communicating with PayPal'),
|
||||
ex['error_description'],
|
||||
ex['correlation_id'])
|
||||
)
|
||||
return
|
||||
|
||||
# Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
|
||||
# get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
|
||||
try:
|
||||
userinfo = tokeninfo.userinfo()
|
||||
request.event.settings.payment_paypal_connect_user_id = userinfo.email
|
||||
except UnauthorizedAccess:
|
||||
pass
|
||||
|
||||
payee = {
|
||||
"email": request.event.settings.payment_paypal_connect_user_id,
|
||||
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
|
||||
# we should use it instead of the merchant's eMail-address
|
||||
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
|
||||
}
|
||||
else:
|
||||
payee = {}
|
||||
|
||||
payment = paypalrestsdk.Payment({
|
||||
'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
|
||||
'intent': 'sale',
|
||||
'payer': {
|
||||
"payment_method": "paypal",
|
||||
},
|
||||
"redirect_urls": {
|
||||
"return_url": eventreverse_absolute(request.event, 'plugins:paypal:return'),
|
||||
"cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort'),
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"item_list": {
|
||||
"items": [
|
||||
{
|
||||
"name": '{prefix}{orderstring}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
orderstring=__('Order {slug}-{code}').format(
|
||||
slug=self.event.slug.upper(),
|
||||
code=payment_obj.order.code
|
||||
),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
"quantity": 1,
|
||||
"price": self.format_price(payment_obj.amount),
|
||||
"currency": payment_obj.order.event.currency
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"currency": request.event.currency,
|
||||
"total": self.format_price(payment_obj.amount)
|
||||
},
|
||||
"description": '{prefix}{orderstring}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
orderstring=__('Order {order} for {event}').format(
|
||||
event=request.event.name,
|
||||
order=payment_obj.order.code
|
||||
),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
"payee": payee,
|
||||
"custom": '{prefix}{slug}-{code}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
slug=self.event.slug.upper(),
|
||||
code=payment_obj.order.code,
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
}
|
||||
]
|
||||
})
|
||||
request.session['payment_paypal_payment'] = payment_obj.pk
|
||||
return self._create_payment(request, payment)
|
||||
except paypalrestsdk.exceptions.ConnectionError as e:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('Error on creating payment: ' + str(e))
|
||||
|
||||
def shred_payment_info(self, obj):
|
||||
if obj.info:
|
||||
d = json.loads(obj.info)
|
||||
new = {
|
||||
'id': d.get('id'),
|
||||
'payer': {
|
||||
'payer_info': {
|
||||
'email': '█'
|
||||
}
|
||||
},
|
||||
'update_time': d.get('update_time'),
|
||||
'transactions': [
|
||||
{
|
||||
'amount': t.get('amount')
|
||||
} for t in d.get('transactions', [])
|
||||
],
|
||||
'_shredded': True
|
||||
}
|
||||
obj.info = json.dumps(new)
|
||||
obj.save(update_fields=['info'])
|
||||
|
||||
for le in obj.order.all_logentries().filter(action_type="pretix.plugins.paypal.event").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
if 'resource' in d:
|
||||
d['resource'] = {
|
||||
'id': d['resource'].get('id'),
|
||||
'sale_id': d['resource'].get('sale_id'),
|
||||
'parent_payment': d['resource'].get('parent_payment'),
|
||||
}
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
def render_invoice_text(self, order: Order, payment: OrderPayment) -> str:
|
||||
if order.status == Order.STATUS_PAID:
|
||||
if payment.info_data.get('id', None):
|
||||
try:
|
||||
return '{}\r\n{}: {}\r\n{}: {}'.format(
|
||||
_('The payment for this invoice has already been received.'),
|
||||
_('PayPal payment ID'),
|
||||
payment.info_data['id'],
|
||||
_('PayPal sale ID'),
|
||||
payment.info_data['transactions'][0]['related_resources'][0]['sale']['id']
|
||||
)
|
||||
except (KeyError, IndexError):
|
||||
return '{}\r\n{}: {}'.format(
|
||||
_('The payment for this invoice has already been received.'),
|
||||
_('PayPal payment ID'),
|
||||
payment.info_data['id']
|
||||
)
|
||||
else:
|
||||
return super().render_invoice_text(order, payment)
|
||||
|
||||
return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
|
||||
@@ -1,31 +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 django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_payment_providers
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_paypal")
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
from .payment import Paypal
|
||||
return Paypal
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="124px" height="33px" viewBox="0 0 124 33" enable-background="new 0 0 124 33" xml:space="preserve">
|
||||
<path fill="#253B80" d="M46.211,6.749h-6.839c-0.468,0-0.866,0.34-0.939,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.564,0.658 h3.265c0.468,0,0.866-0.34,0.939-0.803l0.746-4.73c0.072-0.463,0.471-0.803,0.938-0.803h2.165c4.505,0,7.105-2.18,7.784-6.5 c0.306-1.89,0.013-3.375-0.872-4.415C50.224,7.353,48.5,6.749,46.211,6.749z M47,13.154c-0.374,2.454-2.249,2.454-4.062,2.454 h-1.032l0.724-4.583c0.043-0.277,0.283-0.481,0.563-0.481h0.473c1.235,0,2.4,0,3.002,0.704C47.027,11.668,47.137,12.292,47,13.154z"/>
|
||||
<path fill="#253B80" d="M66.654,13.075h-3.275c-0.279,0-0.52,0.204-0.563,0.481l-0.145,0.916l-0.229-0.332 c-0.709-1.029-2.29-1.373-3.868-1.373c-3.619,0-6.71,2.741-7.312,6.586c-0.313,1.918,0.132,3.752,1.22,5.031 c0.998,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.562,0.66h2.95 c0.469,0,0.865-0.34,0.939-0.803l1.77-11.209C67.271,13.388,67.004,13.075,66.654,13.075z M62.089,19.449 c-0.316,1.871-1.801,3.127-3.695,3.127c-0.951,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.668-1.391-0.514-2.301 c0.295-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C62.034,17.721,62.232,18.543,62.089,19.449z"/>
|
||||
<path fill="#253B80" d="M84.096,13.075h-3.291c-0.314,0-0.609,0.156-0.787,0.417l-4.539,6.686l-1.924-6.425 c-0.121-0.402-0.492-0.678-0.912-0.678h-3.234c-0.393,0-0.666,0.384-0.541,0.754l3.625,10.638l-3.408,4.811 c-0.268,0.379,0.002,0.9,0.465,0.9h3.287c0.312,0,0.604-0.152,0.781-0.408L84.564,13.97C84.826,13.592,84.557,13.075,84.096,13.075z "/>
|
||||
<path fill="#179BD7" d="M94.992,6.749h-6.84c-0.467,0-0.865,0.34-0.938,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.562,0.658 h3.51c0.326,0,0.605-0.238,0.656-0.562l0.785-4.971c0.072-0.463,0.471-0.803,0.938-0.803h2.164c4.506,0,7.105-2.18,7.785-6.5 c0.307-1.89,0.012-3.375-0.873-4.415C99.004,7.353,97.281,6.749,94.992,6.749z M95.781,13.154c-0.373,2.454-2.248,2.454-4.062,2.454 h-1.031l0.725-4.583c0.043-0.277,0.281-0.481,0.562-0.481h0.473c1.234,0,2.4,0,3.002,0.704 C95.809,11.668,95.918,12.292,95.781,13.154z"/>
|
||||
<path fill="#179BD7" d="M115.434,13.075h-3.273c-0.281,0-0.52,0.204-0.562,0.481l-0.145,0.916l-0.23-0.332 c-0.709-1.029-2.289-1.373-3.867-1.373c-3.619,0-6.709,2.741-7.311,6.586c-0.312,1.918,0.131,3.752,1.219,5.031 c1,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.564,0.66h2.949 c0.467,0,0.865-0.34,0.938-0.803l1.771-11.209C116.053,13.388,115.785,13.075,115.434,13.075z M110.869,19.449 c-0.314,1.871-1.801,3.127-3.695,3.127c-0.949,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.666-1.391-0.514-2.301 c0.297-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C110.816,17.721,111.014,18.543,110.869,19.449z"/>
|
||||
<path fill="#179BD7" d="M119.295,7.23l-2.807,17.858c-0.055,0.346,0.213,0.658,0.562,0.658h2.822c0.469,0,0.867-0.34,0.939-0.803 l2.768-17.536c0.055-0.346-0.213-0.659-0.562-0.659h-3.16C119.578,6.749,119.338,6.953,119.295,7.23z"/>
|
||||
<path fill="#253B80" d="M7.266,29.154l0.523-3.322l-1.165-0.027H1.061L4.927,1.292C4.939,1.218,4.978,1.149,5.035,1.1 c0.057-0.049,0.13-0.076,0.206-0.076h9.38c3.114,0,5.263,0.648,6.385,1.927c0.526,0.6,0.861,1.227,1.023,1.917 c0.17,0.724,0.173,1.589,0.007,2.644l-0.012,0.077v0.676l0.526,0.298c0.443,0.235,0.795,0.504,1.065,0.812 c0.45,0.513,0.741,1.165,0.864,1.938c0.127,0.795,0.085,1.741-0.123,2.812c-0.24,1.232-0.628,2.305-1.152,3.183 c-0.482,0.809-1.096,1.48-1.825,2c-0.696,0.494-1.523,0.869-2.458,1.109c-0.906,0.236-1.939,0.355-3.072,0.355h-0.73 c-0.522,0-1.029,0.188-1.427,0.525c-0.399,0.344-0.663,0.814-0.744,1.328l-0.055,0.299l-0.924,5.855l-0.042,0.215 c-0.011,0.068-0.03,0.102-0.058,0.125c-0.025,0.021-0.061,0.035-0.096,0.035H7.266z"/>
|
||||
<path fill="#179BD7" d="M23.048,7.667L23.048,7.667L23.048,7.667c-0.028,0.179-0.06,0.362-0.096,0.55 c-1.237,6.351-5.469,8.545-10.874,8.545H9.326c-0.661,0-1.218,0.48-1.321,1.132l0,0l0,0L6.596,26.83l-0.399,2.533 c-0.067,0.428,0.263,0.814,0.695,0.814h4.881c0.578,0,1.069-0.42,1.16-0.99l0.048-0.248l0.919-5.832l0.059-0.32 c0.09-0.572,0.582-0.992,1.16-0.992h0.73c4.729,0,8.431-1.92,9.513-7.476c0.452-2.321,0.218-4.259-0.978-5.622 C24.022,8.286,23.573,7.945,23.048,7.667z"/>
|
||||
<path fill="#222D65" d="M21.754,7.151c-0.189-0.055-0.384-0.105-0.584-0.15c-0.201-0.044-0.407-0.083-0.619-0.117 c-0.742-0.12-1.555-0.177-2.426-0.177h-7.352c-0.181,0-0.353,0.041-0.507,0.115C9.927,6.985,9.675,7.306,9.614,7.699L8.05,17.605 l-0.045,0.289c0.103-0.652,0.66-1.132,1.321-1.132h2.752c5.405,0,9.637-2.195,10.874-8.545c0.037-0.188,0.068-0.371,0.096-0.55 c-0.313-0.166-0.652-0.308-1.017-0.429C21.941,7.208,21.848,7.179,21.754,7.151z"/>
|
||||
<path fill="#253B80" d="M9.614,7.699c0.061-0.393,0.313-0.714,0.652-0.876c0.155-0.074,0.326-0.115,0.507-0.115h7.352 c0.871,0,1.684,0.057,2.426,0.177c0.212,0.034,0.418,0.073,0.619,0.117c0.2,0.045,0.395,0.095,0.584,0.15 c0.094,0.028,0.187,0.057,0.278,0.086c0.365,0.121,0.704,0.264,1.017,0.429c0.368-2.347-0.003-3.945-1.272-5.392 C20.378,0.682,17.853,0,14.622,0h-9.38c-0.66,0-1.223,0.48-1.325,1.133L0.01,25.898c-0.077,0.49,0.301,0.932,0.795,0.932h5.791 l1.454-9.225L9.614,7.699z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -1,6 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>{% blocktrans trimmed %}
|
||||
The total amount listed above will be withdrawn from your PayPal account after the
|
||||
confirmation of your purchase.
|
||||
{% endblocktrans %}</p>
|
||||
@@ -1,6 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>{% blocktrans trimmed %}
|
||||
After you clicked continue, we will redirect you to PayPal to fill in your payment
|
||||
details. You will then be redirected back here to review and confirm your order.
|
||||
{% endblocktrans %}</p>
|
||||
@@ -1,18 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Payment ID" %}</dt>
|
||||
<dd>{{ payment_info.id }}</dd>
|
||||
<dt>{% trans "Sale ID" %}</dt>
|
||||
<dd>{{ sale_id|default_if_none:"?" }}</dd>
|
||||
<dt>{% trans "Payer" %}</dt>
|
||||
<dd>{{ payment_info.payer.payer_info.email }}</dd>
|
||||
<dt>{% trans "Last update" %}</dt>
|
||||
<dd>{{ payment_info.update_time }}</dd>
|
||||
<dt>{% trans "Total value" %}</dt>
|
||||
<dd>{{ payment_info.transactions.0.amount.total }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ payment_info.transactions.0.amount.currency }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
@@ -1,12 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if retry %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
Our attempt to execute your Payment via PayPal has failed. Please try again or contact us.
|
||||
{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
We're waiting for an answer from PayPal regarding your payment. Please contact us, if this
|
||||
takes more than a few hours.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
@@ -1,39 +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 django.urls import include, re_path
|
||||
|
||||
from .views import abort, oauth_disconnect, success
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^paypal/', include([
|
||||
re_path(r'^abort/$', abort, name='abort'),
|
||||
re_path(r'^return/$', success, name='return'),
|
||||
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
|
||||
])),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/disconnect/',
|
||||
oauth_disconnect, name='oauth.disconnect'),
|
||||
]
|
||||
@@ -1,249 +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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Flavia Bastos
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
import paypalrestsdk
|
||||
import paypalrestsdk.exceptions
|
||||
from django.contrib import messages
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.control.permissions import event_permission_required
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
from pretix.plugins.paypal.payment import Paypal
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.paypal')
|
||||
|
||||
|
||||
def success(request, *args, **kwargs):
|
||||
pid = request.GET.get('paymentId')
|
||||
token = request.GET.get('token')
|
||||
payer = request.GET.get('PayerID')
|
||||
request.session['payment_paypal_token'] = token
|
||||
request.session['payment_paypal_payer'] = payer
|
||||
|
||||
urlkwargs = {}
|
||||
if 'cart_namespace' in kwargs:
|
||||
urlkwargs['cart_namespace'] = kwargs['cart_namespace']
|
||||
|
||||
if request.session.get('payment_paypal_payment'):
|
||||
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
|
||||
else:
|
||||
payment = None
|
||||
|
||||
if pid == request.session.get('payment_paypal_id', None):
|
||||
if payment:
|
||||
prov = Paypal(request.event)
|
||||
try:
|
||||
resp = prov.execute_payment(request, payment)
|
||||
except PaymentException as e:
|
||||
messages.error(request, str(e))
|
||||
urlkwargs['step'] = 'payment'
|
||||
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||
if resp:
|
||||
return resp
|
||||
else:
|
||||
messages.error(request, _('Invalid response from PayPal received.'))
|
||||
logger.error('Session did not contain payment_paypal_id')
|
||||
urlkwargs['step'] = 'payment'
|
||||
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||
|
||||
if payment:
|
||||
return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||
'order': payment.order.code,
|
||||
'secret': payment.order.secret
|
||||
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
|
||||
else:
|
||||
urlkwargs['step'] = 'confirm'
|
||||
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||
|
||||
|
||||
def abort(request, *args, **kwargs):
|
||||
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
||||
|
||||
if request.session.get('payment_paypal_payment'):
|
||||
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
|
||||
else:
|
||||
payment = None
|
||||
|
||||
if payment:
|
||||
return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||
'order': payment.order.code,
|
||||
'secret': payment.order.secret
|
||||
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
|
||||
else:
|
||||
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
@scopes_disabled()
|
||||
def webhook(request, *args, **kwargs):
|
||||
event_body = request.body.decode('utf-8').strip()
|
||||
event_json = json.loads(event_body)
|
||||
|
||||
# We do not check the signature, we just use it as a trigger to look the charge up.
|
||||
if 'resource_type' not in event_json:
|
||||
return HttpResponse("Invalid body, no resource_type given", status=400)
|
||||
if event_json['resource_type'] not in ('sale', 'refund'):
|
||||
return HttpResponse("Not interested in this resource type", status=200)
|
||||
|
||||
if event_json['resource_type'] == 'sale':
|
||||
saleid = event_json['resource']['id']
|
||||
else:
|
||||
saleid = event_json['resource']['sale_id']
|
||||
|
||||
try:
|
||||
refs = [saleid]
|
||||
if event_json['resource'].get('parent_payment'):
|
||||
refs.append(event_json['resource'].get('parent_payment'))
|
||||
|
||||
rso = ReferencedPayPalObject.objects.select_related('order', 'order__event').get(
|
||||
reference__in=refs
|
||||
)
|
||||
event = rso.order.event
|
||||
except ReferencedPayPalObject.DoesNotExist:
|
||||
rso = None
|
||||
if hasattr(request, 'event'):
|
||||
event = request.event
|
||||
else:
|
||||
return HttpResponse("Unable to detect event", status=200)
|
||||
|
||||
prov = Paypal(event)
|
||||
prov.init_api()
|
||||
|
||||
try:
|
||||
sale = paypalrestsdk.Sale.find(saleid)
|
||||
except paypalrestsdk.exceptions.ConnectionError:
|
||||
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||
return HttpResponse('Sale not found', status=500)
|
||||
|
||||
if rso and rso.payment:
|
||||
payment = rso.payment
|
||||
else:
|
||||
payments = OrderPayment.objects.filter(order__event=event, provider='paypal',
|
||||
info__icontains=sale['id'])
|
||||
payment = None
|
||||
for p in payments:
|
||||
payment_info = p.info_data
|
||||
for res in payment_info['transactions'][0]['related_resources']:
|
||||
for k, v in res.items():
|
||||
if k == 'sale' and v['id'] == sale['id']:
|
||||
payment = p
|
||||
break
|
||||
|
||||
if not payment:
|
||||
return HttpResponse('Payment not found', status=200)
|
||||
|
||||
payment.order.log_action('pretix.plugins.paypal.event', data=event_json)
|
||||
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['state'] in ('partially_refunded', 'refunded'):
|
||||
if event_json['resource_type'] == 'refund':
|
||||
try:
|
||||
refund = paypalrestsdk.Refund.find(event_json['resource']['id'])
|
||||
except paypalrestsdk.exceptions.ConnectionError:
|
||||
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||
return HttpResponse('Refund not found', status=500)
|
||||
|
||||
known_refunds = {r.info_data.get('id'): r for r in payment.refunds.all()}
|
||||
if refund['id'] not in known_refunds:
|
||||
payment.create_external_refund(
|
||||
amount=abs(Decimal(refund['amount']['total'])),
|
||||
info=json.dumps(refund.to_dict() if not isinstance(refund, dict) else refund)
|
||||
)
|
||||
elif known_refunds.get(refund['id']).state in (
|
||||
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT) and refund['state'] == 'completed':
|
||||
known_refunds.get(refund['id']).done()
|
||||
|
||||
if 'total_refunded_amount' in refund:
|
||||
known_sum = payment.refunds.filter(
|
||||
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
|
||||
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||
total_refunded_amount = Decimal(refund['total_refunded_amount']['value'])
|
||||
if known_sum < total_refunded_amount:
|
||||
payment.create_external_refund(
|
||||
amount=total_refunded_amount - known_sum
|
||||
)
|
||||
elif sale['state'] == 'refunded':
|
||||
known_sum = payment.refunds.filter(
|
||||
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
|
||||
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||
|
||||
if known_sum < payment.amount:
|
||||
payment.create_external_refund(
|
||||
amount=payment.amount - known_sum
|
||||
)
|
||||
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED,
|
||||
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED) and sale['state'] == 'completed':
|
||||
try:
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
@event_permission_required('event.settings.general:write')
|
||||
@require_POST
|
||||
def oauth_disconnect(request, **kwargs):
|
||||
del request.event.settings.payment_paypal_connect_refresh_token
|
||||
del request.event.settings.payment_paypal_connect_user_id
|
||||
request.event.settings.payment_paypal__enabled = False
|
||||
messages.success(request, _('Your PayPal account has been disconnected.'))
|
||||
|
||||
# Migrate User to PayPal v2
|
||||
event = request.event
|
||||
event.disable_plugin("pretix.plugins.paypal")
|
||||
event.enable_plugin("pretix.plugins.paypal2")
|
||||
event.save()
|
||||
|
||||
return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
'provider': 'paypal_settings'
|
||||
}))
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.2.17 on 2026-09-08 08:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0310_question_valid_string_length_min"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name="ReferencedPayPalObject",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
(
|
||||
"reference",
|
||||
models.CharField(db_index=True, max_length=190, unique=True),
|
||||
),
|
||||
(
|
||||
"order",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.order",
|
||||
),
|
||||
),
|
||||
(
|
||||
"payment",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.orderpayment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "paypal_referencedpaypalobject",
|
||||
},
|
||||
),
|
||||
],
|
||||
database_operations=[]
|
||||
)
|
||||
]
|
||||
@@ -26,3 +26,6 @@ class ReferencedPayPalObject(models.Model):
|
||||
reference = models.CharField(max_length=190, db_index=True, unique=True)
|
||||
order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE)
|
||||
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
db_table = 'paypal_referencedpaypalobject'
|
||||
@@ -68,7 +68,7 @@ from pretix.plugins.paypal2.client.core.paypal_http_client import (
|
||||
from pretix.plugins.paypal2.client.customer.partner_referral_create_request import (
|
||||
PartnerReferralCreateRequest,
|
||||
)
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
from pretix.plugins.paypal2.models import ReferencedPayPalObject
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.paypal2')
|
||||
|
||||
|
||||
@@ -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' +
|
||||
'¤cy=' + pretixpaypal.currency;
|
||||
let sdk_url = 'https://www.paypal.com/sdk/js'
|
||||
+ '?client-id=' + pretixpaypal.client_id
|
||||
+ '&components=buttons,funding-eligibility'
|
||||
+ '¤cy=' + 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;
|
||||
var $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
|
||||
let $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()
|
||||
})()
|
||||
})
|
||||
|
||||
@@ -67,10 +67,10 @@ from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_request import (
|
||||
PartnersMerchantIntegrationsGetRequest,
|
||||
)
|
||||
from pretix.plugins.paypal2.models import ReferencedPayPalObject
|
||||
from pretix.plugins.paypal2.payment import (
|
||||
PaypalMethod, PaypalMethod as Paypal, PaypalWallet,
|
||||
)
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
from pretix.presale.views import get_cart
|
||||
from pretix.presale.views.cart import cart_session
|
||||
|
||||
@@ -350,8 +350,8 @@ def webhook(request, *args, **kwargs):
|
||||
return HttpResponse("Invalid body, no event_type given", status=400)
|
||||
|
||||
if event_json['event_type'].startswith('PAYMENT.SALE.'):
|
||||
from pretix.plugins.paypal.views import webhook
|
||||
return webhook(request, *args, **kwargs)
|
||||
logger.info(f"Received PPv1 webhook: {json.dumps(event_json)}")
|
||||
return HttpResponse("PayPal V1 no longer supported", status=400)
|
||||
# V1/V2 Sorting -- End
|
||||
|
||||
# We do not check the signature, we just use it as a trigger to look the charge up.
|
||||
|
||||
@@ -55,6 +55,8 @@ 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
|
||||
|
||||
@@ -226,10 +228,20 @@ class ReportlabExportMixin:
|
||||
def page_header(self, canvas, doc):
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
canvas.setFont('OpenSans', 10)
|
||||
canvas.drawString(doc.leftMargin, self.pagesize[1] - 15 * mm, self.get_left_header_string())
|
||||
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.drawRightString(self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 15 * mm,
|
||||
self.get_right_header_string())
|
||||
right_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)
|
||||
|
||||
@@ -56,18 +56,11 @@ 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'),
|
||||
@@ -79,6 +72,16 @@ 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)
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ 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')
|
||||
@@ -354,9 +355,9 @@ class OrderSendView(BaseSenderView):
|
||||
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True)
|
||||
orders = qs.filter(statusq)
|
||||
|
||||
opq = OrderPosition.objects.filter(
|
||||
opq = OrderPosition.objects.with_scopes_disabled().filter(
|
||||
Q(item_id__in=[i.pk for i in form.cleaned_data.get('items')]) | Q(Exists(
|
||||
OrderPosition.objects.filter(
|
||||
OrderPosition.objects.with_scopes_disabled().filter(
|
||||
addon_to_id=OuterRef('pk'),
|
||||
item_id__in=[i.pk for i in form.cleaned_data.get('items')]
|
||||
)
|
||||
@@ -366,36 +367,43 @@ class OrderSendView(BaseSenderView):
|
||||
)
|
||||
|
||||
if form.cleaned_data.get('filter_checkins'):
|
||||
ql = []
|
||||
ci_filter = Q(pk__in=[]) # return nothing
|
||||
|
||||
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.all.filter(
|
||||
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
|
||||
successful=True,
|
||||
list__consider_tickets_used=True,
|
||||
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,
|
||||
)
|
||||
)
|
||||
)
|
||||
ql.append(Q(any_checkins=False))
|
||||
ci_filter |= Q(any_checkins=False)
|
||||
|
||||
if form.cleaned_data.get('checkin_lists'):
|
||||
opq = opq.alias(
|
||||
matching_checkins=Exists(
|
||||
Checkin.all.filter(
|
||||
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
|
||||
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'),
|
||||
list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])],
|
||||
successful=True
|
||||
)
|
||||
)
|
||||
)
|
||||
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()
|
||||
ci_filter |= Q(matching_checkins=True)
|
||||
|
||||
opq = opq.filter(ci_filter)
|
||||
|
||||
if form.cleaned_data.get('subevent'):
|
||||
opq = opq.filter(subevent=form.cleaned_data.get('subevent'))
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
/*globals $, Morris, gettext, django*/
|
||||
function gettext(msgid) {
|
||||
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
|
||||
return django.gettext(msgid);
|
||||
}
|
||||
return msgid;
|
||||
/* globals Morris, 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);
|
||||
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
|
||||
});
|
||||
});
|
||||
$('.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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
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.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.paymentRequest.on('paymentmethod', function (ev) {
|
||||
ev.complete('success');
|
||||
pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
|
||||
ev.complete('success')
|
||||
|
||||
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(),
|
||||
});
|
||||
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(),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -873,6 +873,28 @@ 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
|
||||
|
||||
@@ -247,14 +247,15 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
|
||||
continue
|
||||
|
||||
if item.hidden_if_item_available:
|
||||
time_available = item.hidden_if_item_available.is_available()
|
||||
if item.hidden_if_item_available.has_variations:
|
||||
item._dependency_available = any(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
||||
# is_available on variant is evaluated called by available_variations
|
||||
for var in item.hidden_if_item_available.available_variations
|
||||
)
|
||||
) and time_available
|
||||
else:
|
||||
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
time_available = item.hidden_if_item_available.is_available()
|
||||
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
|
||||
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
|
||||
item._remove = True
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
{% endblocktrans %} ::
|
||||
{% endif %}
|
||||
{% elif subevent %}
|
||||
{{ subevent.get_date_range_display }} ::
|
||||
{% 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 }} ::
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -135,9 +139,6 @@
|
||||
{% if event_logo and event_logo_show_title %}
|
||||
<h2 class="content-header">
|
||||
{{ event.name }}
|
||||
{% if request.event.settings.show_dates_on_frontpage %}
|
||||
<small>{{ event.get_date_range_display_as_html }}</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
{% if frontpage_text %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% load escapejson %}
|
||||
|
||||
{% if payment_qr_codes %}
|
||||
<div class="tabcontainer col-md-6 col-sm-6 hidden-xs text-center js-only blank-after">
|
||||
<div class="tabcontainer col-md-6 col-sm-6 col-xs-12 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 }}"
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
{% trans "Change account information" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.customer.password" %}">
|
||||
{% icon "key" %}
|
||||
{% trans "Change password" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if not customer.provider %}
|
||||
<li>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.customer.password" %}">
|
||||
{% icon "key" %}
|
||||
{% trans "Change password" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</dd>
|
||||
<dd>
|
||||
|
||||
@@ -54,6 +54,7 @@ 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
|
||||
@@ -280,6 +281,11 @@ 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()
|
||||
|
||||
@@ -122,19 +122,20 @@ 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_VITE', False) or "beta" in request.GET:
|
||||
return True
|
||||
if getattr(settings, 'PRETIX_WIDGET_VUE', False) or "legacy" in request.GET:
|
||||
return False
|
||||
origin = request.META.get('HTTP_ORIGIN', '')
|
||||
gs = GlobalSettingsObject()
|
||||
vite_origins = gs.settings.get('widget_vite_origins', as_type=str, default='')
|
||||
if vite_origins and not origin:
|
||||
vue_origins = gs.settings.get('widget_vue2_origins', as_type=str, default='')
|
||||
if vue_origins and not origin:
|
||||
referer = request.META.get('HTTP_REFERER', '')
|
||||
origin = '/'.join(referer.split('/', 3)[:3])
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
def widget_js_etag(request, version, lang, **kwargs):
|
||||
|
||||
@@ -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_VITE = os.environ.get('PRETIX_WIDGET_VITE', '') not in ('', '0')
|
||||
PRETIX_WIDGET_VUE = os.environ.get('PRETIX_WIDGET_VUE', '') not in ('', '0')
|
||||
|
||||
if DEBUG:
|
||||
# Reload if settings file changes
|
||||
|
||||
@@ -1,195 +1,193 @@
|
||||
$(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.
|
||||
var 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.
|
||||
let 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 (var k in dependents) {
|
||||
if (dependents[k].prop("disabled")) {
|
||||
dependentsDisabled.push(k);
|
||||
}
|
||||
}
|
||||
const dependentsDisabled = []
|
||||
for (let 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) {
|
||||
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);
|
||||
});
|
||||
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)
|
||||
})
|
||||
|
||||
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 (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 (!data.transmission_type.visible) {
|
||||
selected_transmission_type = "email";
|
||||
}
|
||||
if (!data.transmission_type.visible) {
|
||||
selected_transmission_type = 'email'
|
||||
}
|
||||
|
||||
$.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);
|
||||
});
|
||||
$.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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
for (var k in dependents) {
|
||||
const options = data[k],
|
||||
dependent = dependents[k]
|
||||
let visible = 'visible' in options ? options.visible : true
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
);
|
||||
}
|
||||
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 ('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 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 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 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 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 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 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());
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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 (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()
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
if (!(url in responseCache)) {
|
||||
responseCache[url] = xhr = $.ajax({
|
||||
dataType: 'json',
|
||||
url: url,
|
||||
timeout: 3000,
|
||||
})
|
||||
}
|
||||
|
||||
if (!(url in responseCache)) {
|
||||
responseCache[url] = xhr = $.ajax({
|
||||
dataType: "json",
|
||||
url: url,
|
||||
timeout: 3000,
|
||||
});
|
||||
}
|
||||
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
|
||||
|
||||
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;
|
||||
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)
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
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 @@
|
||||
var check = function () {
|
||||
$.getJSON(location.href + '&ajax=1', function (data, status) {
|
||||
if (data.redirect) {
|
||||
location.href = data.redirect;
|
||||
} else {
|
||||
window.setTimeout(check, 500);
|
||||
}
|
||||
});
|
||||
let 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)
|
||||
|
||||
@@ -1,359 +1,357 @@
|
||||
/*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;
|
||||
/* 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
|
||||
|
||||
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);
|
||||
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.'
|
||||
)
|
||||
}
|
||||
|
||||
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_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_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_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_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_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_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_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_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_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_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_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_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"))
|
||||
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)
|
||||
|
||||
}
|
||||
async_task_update_status(data)
|
||||
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(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
|
||||
var 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
|
||||
let 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.'
|
||||
)
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
);
|
||||
});
|
||||
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,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]', '@').replace('[dot]', '.');
|
||||
link.textContent = link.textContent.replace('[at]', '@').replace('[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]', '@').replaceAll('[dot]', '.')
|
||||
link.textContent = link.textContent.replace('[at]', '@').replaceAll('[dot]', '.')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,152 +1,149 @@
|
||||
/*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')
|
||||
|
||||
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');
|
||||
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'
|
||||
|
||||
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';
|
||||
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 })
|
||||
|
||||
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 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)
|
||||
}
|
||||
})
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
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 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();
|
||||
}
|
||||
});
|
||||
$('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
|
||||
})
|
||||
})
|
||||
|
||||
$('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;
|
||||
});
|
||||
});
|
||||
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
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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)
|
||||
|
||||
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);
|
||||
};
|
||||
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'))
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
var url = JSON.parse(document.getElementById('framebreak-url').innerText)
|
||||
let url = JSON.parse(document.getElementById('framebreak-url').innerText)
|
||||
window.open(url)
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
// 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()) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
function i18nstring_localize(o) {
|
||||
var locale = document.body.attributes['data-pretixlocale'].value
|
||||
var short_locale = locale.split('-')[0]
|
||||
if (o[locale])
|
||||
return o[locale]
|
||||
function i18nstring_localize (o) {
|
||||
let locale = document.body.attributes['data-pretixlocale'].value
|
||||
let 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 (k of Object.keys(o)) {
|
||||
if (k.split('-')[0] === short_locale && o[k]) {
|
||||
return o[k]
|
||||
}
|
||||
}
|
||||
for (let 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 (k of Object.keys(o)) {
|
||||
if (o[k]) {
|
||||
return o[k]
|
||||
}
|
||||
}
|
||||
for (let k of Object.keys(o)) {
|
||||
if (o[k]) {
|
||||
return o[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
var intId = window.setInterval(function () {
|
||||
$.get(location.href + '?ajax=1', function (data, status) {
|
||||
if (data === "1") {
|
||||
window.clearInterval(intId);
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
const intId = window.setInterval(function () {
|
||||
$.get(location.href + '?ajax=1', function (data, _status) {
|
||||
if (data === '1') {
|
||||
window.clearInterval(intId)
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
}, 500)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user