forked from CGM_Public/pretix_original
Compare commits
2 Commits
v4.13.0
...
placeholde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ce40d0ac | ||
|
|
d62c7553c2 |
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Please only create issues for bug reports. Feature requests or general questions
|
||||||
|
should start as a "Discussion" on GitHub.
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Please only create issues for bug reports. Feature requests or general questions should start as a "Discussion" on GitHub. -->
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Bug report
|
|
||||||
description: Please only create issues for bug reports. Feature requests or general questions should start as a "Discussion" on GitHub.
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: Please make sure to search our issues for similar bugs first! If bug has been reported already, react with a thumbs-up, and/or leave a comment providing further details.
|
|
||||||
- type: textarea
|
|
||||||
id: current
|
|
||||||
attributes:
|
|
||||||
label: Problem and impact
|
|
||||||
description: What problem you're running into? What impact does it have on you / your event?
|
|
||||||
placeholder: When trying to do ____, pretix suddenly shows me an error saying "...".
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: Expected behaviour
|
|
||||||
description: Sometimes bugs are subtle and the expected behaviour may need some explanation. Leave empty if it's just "Don't be broken."
|
|
||||||
- type: textarea
|
|
||||||
id: reproduction
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: "Please give as much context as possible: Are there any settings that impact this behaviour?"
|
|
||||||
placeholder: |
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
4.
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: If possible, show screenshots of the problem.
|
|
||||||
- type: input
|
|
||||||
id: link
|
|
||||||
attributes:
|
|
||||||
label: Link
|
|
||||||
description: Link to the page where the bug occurs
|
|
||||||
- type: input
|
|
||||||
id: browser
|
|
||||||
attributes:
|
|
||||||
label: Browser (software, desktop or mobile?) and version
|
|
||||||
description: Leave empty for backend problems
|
|
||||||
- type: input
|
|
||||||
id: os
|
|
||||||
attributes:
|
|
||||||
label: Operating system, dependency versions
|
|
||||||
description: Leave empty for frontend problems
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Version
|
|
||||||
description: The pretix version in use. (Leave empty if unknown.)
|
|
||||||
|
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
|||||||
blank_issues_enabled: true
|
|
||||||
contact_links:
|
|
||||||
- name: Community Support
|
|
||||||
url: https://github.com/pretix/pretix/discussions/categories/q-a
|
|
||||||
about: Not sure how to do Y? Please post your support requests in the Q&A section of our GitHub Discussions instead!
|
|
||||||
- name: Feature ideas
|
|
||||||
url: https://github.com/pretix/pretix/discussions/categories/ideas
|
|
||||||
about: Please post your idea in the Ideas section of our GitHub Discussions instead!
|
|
||||||
20
SECURITY.md
20
SECURITY.md
@@ -1,20 +0,0 @@
|
|||||||
# Security policy
|
|
||||||
|
|
||||||
## Reporting a vulnerability
|
|
||||||
|
|
||||||
If you discover a vulnerability with our software or server systems, please report it to us in private. Do not to attempt to harm our users, customer's data or our system's availability when looking for vulneratbilities.
|
|
||||||
|
|
||||||
Please contact us at security@pretix.eu with full details and steps to reproduce and allow reasonable time for us to resolve the issue before publishing your findings. If you wish to encrypt your email, you can find our GPG key [here](https://pretix.eu/.well-known/security@pretix.eu.asc).
|
|
||||||
|
|
||||||
We're not large enough to run a formal bug bounty program, but if you find a serious vulnerability in our service, we will find a way to show our gratitude.
|
|
||||||
|
|
||||||
## Version support
|
|
||||||
|
|
||||||
Security support is provided for the current stable release as well as the two previous stable releases.
|
|
||||||
Be sure to keep your pretix installation up to date.
|
|
||||||
|
|
||||||
New releases and security issues will be announced on our [blog](https://pretix.eu/about/en/blog/). If you
|
|
||||||
subscribe to our [newsletter](https://pretix.eu/about/en/blog/) in the "News about self-hosting pretix"
|
|
||||||
category, we will also send you an email on security issues.
|
|
||||||
|
|
||||||
Past security issues are listed [on our website](https://pretix.eu/about/en/security).
|
|
||||||
@@ -6067,10 +6067,6 @@ url('../opensans_regular_macroman/OpenSans-Regular-webfont.svg#open_sansregular'
|
|||||||
img.screenshot, a.screenshot img {
|
img.screenshot, a.screenshot img {
|
||||||
box-shadow: 0 4px 18px 0 rgba(0,0,0,0.1), 0 6px 20px 0 rgba(0,0,0,0.09);
|
box-shadow: 0 4px 18px 0 rgba(0,0,0,0.1), 0 6px 20px 0 rgba(0,0,0,0.09);
|
||||||
}
|
}
|
||||||
section > a.screenshot {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Changes */
|
/* Changes */
|
||||||
.versionchanged {
|
.versionchanged {
|
||||||
|
|||||||
@@ -105,37 +105,6 @@ following endpoint:
|
|||||||
|
|
||||||
You will receive a response equivalent to the response of your initialization request.
|
You will receive a response equivalent to the response of your initialization request.
|
||||||
|
|
||||||
Device Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
You can request information about your device and the server with one call:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/device/info HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
|
|
||||||
The response will look like this:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"device": {
|
|
||||||
"organizer": "foo",
|
|
||||||
"device_id": 5,
|
|
||||||
"unique_serial": "HHZ9LW9JWP390VFZ",
|
|
||||||
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
|
|
||||||
"name": "Bar",
|
|
||||||
"gate": {
|
|
||||||
"id": 3,
|
|
||||||
"name": "South entrance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Creating a new API key
|
Creating a new API key
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ The customer resource contains the following public fields:
|
|||||||
Field Type Description
|
Field Type Description
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
identifier string Internal ID of the customer
|
identifier string Internal ID of the customer
|
||||||
external_identifier string External ID of the customer (or ``null``). This field can
|
external_identifier string External ID of the customer (or ``null``)
|
||||||
be changed for customers created manually or through
|
|
||||||
the API, but is read-only for customers created through a
|
|
||||||
SSO integration.
|
|
||||||
email string Customer email address
|
email string Customer email address
|
||||||
name string Name of this customer (or ``null``)
|
name string Name of this customer (or ``null``)
|
||||||
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
||||||
@@ -29,16 +26,10 @@ date_joined datetime Date and time o
|
|||||||
locale string Preferred language of the customer
|
locale string Preferred language of the customer
|
||||||
last_modified datetime Date and time of modification of the record
|
last_modified datetime Date and time of modification of the record
|
||||||
notes string Internal notes and comments (or ``null``)
|
notes string Internal notes and comments (or ``null``)
|
||||||
password string Can only be set during creation of a new customer, will
|
|
||||||
not be included in any responses.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionadded:: 4.0
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
.. versionchanged:: 4.3
|
|
||||||
|
|
||||||
Passwords can now be set through the API during customer creation.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -155,7 +146,6 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"email": "test@example.org",
|
"email": "test@example.org",
|
||||||
"password": "verysecret",
|
|
||||||
"send_email": true
|
"send_email": true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,16 +36,10 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.order.canceled``
|
* ``pretix.event.order.canceled``
|
||||||
* ``pretix.event.order.reactivated``
|
* ``pretix.event.order.reactivated``
|
||||||
* ``pretix.event.order.expired``
|
* ``pretix.event.order.expired``
|
||||||
* ``pretix.event.order.expirychanged``
|
|
||||||
* ``pretix.event.order.modified``
|
* ``pretix.event.order.modified``
|
||||||
* ``pretix.event.order.contact.changed``
|
* ``pretix.event.order.contact.changed``
|
||||||
* ``pretix.event.order.changed.*``
|
* ``pretix.event.order.changed.*``
|
||||||
* ``pretix.event.order.refund.created``
|
|
||||||
* ``pretix.event.order.refund.created.externally``
|
* ``pretix.event.order.refund.created.externally``
|
||||||
* ``pretix.event.order.refund.requested``
|
|
||||||
* ``pretix.event.order.refund.done``
|
|
||||||
* ``pretix.event.order.refund.canceled``
|
|
||||||
* ``pretix.event.order.refund.failed``
|
|
||||||
* ``pretix.event.order.approved``
|
* ``pretix.event.order.approved``
|
||||||
* ``pretix.event.order.denied``
|
* ``pretix.event.order.denied``
|
||||||
* ``pretix.event.checkin``
|
* ``pretix.event.checkin``
|
||||||
@@ -56,10 +50,6 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.subevent.added``
|
* ``pretix.subevent.added``
|
||||||
* ``pretix.subevent.changed``
|
* ``pretix.subevent.changed``
|
||||||
* ``pretix.subevent.deleted``
|
* ``pretix.subevent.deleted``
|
||||||
* ``pretix.event.live.activated``
|
|
||||||
* ``pretix.event.live.deactivated``
|
|
||||||
* ``pretix.event.testmode.activated``
|
|
||||||
* ``pretix.event.testmode.deactivated``
|
|
||||||
|
|
||||||
Installed plugins might register more valid values.
|
Installed plugins might register more valid values.
|
||||||
|
|
||||||
|
|||||||
@@ -92,10 +92,9 @@ If any other status code is returned, we will assume you did not receive the cal
|
|||||||
or ``304 Not Modified`` response will be treated as a failure. pretix will not follow any ``301`` or ``302`` redirect
|
or ``304 Not Modified`` response will be treated as a failure. pretix will not follow any ``301`` or ``302`` redirect
|
||||||
headers and pretix will ignore all other information in your response headers or body.
|
headers and pretix will ignore all other information in your response headers or body.
|
||||||
|
|
||||||
If we do not receive a status code in the range of ``200`` and ``299`` or do not receive any response within a 30 second
|
If we do not receive a status code in the range of ``200`` and ``299``, pretix will retry to deliver for up to three
|
||||||
time frame, pretix will retry to deliver for up to three days with an exponential back off. Therefore, we recommend that
|
days with an exponential back off. Therefore, we recommend that you implement your endpoint in a way where calling it
|
||||||
you implement your endpoint in a way where calling it multiple times for the same event due to a perceived error does
|
multiple times for the same event due to a perceived error does not do any harm.
|
||||||
not do any harm.
|
|
||||||
|
|
||||||
There is only one exception: If status code ``410 Gone`` is returned, we will assume the
|
There is only one exception: If status code ``410 Gone`` is returned, we will assume the
|
||||||
endpoint does not exist any more and automatically disable the webhook.
|
endpoint does not exist any more and automatically disable the webhook.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB |
@@ -66,7 +66,6 @@ iterable
|
|||||||
Jimdo
|
Jimdo
|
||||||
jwt
|
jwt
|
||||||
JWT
|
JWT
|
||||||
JWTs
|
|
||||||
libpretixprint
|
libpretixprint
|
||||||
libsass
|
libsass
|
||||||
linters
|
linters
|
||||||
@@ -89,9 +88,7 @@ nginx
|
|||||||
nodejs
|
nodejs
|
||||||
NotificationType
|
NotificationType
|
||||||
npm
|
npm
|
||||||
OIDC
|
|
||||||
ons
|
ons
|
||||||
OpenID
|
|
||||||
optimizations
|
optimizations
|
||||||
overpayment
|
overpayment
|
||||||
param
|
param
|
||||||
@@ -136,7 +133,6 @@ serializer
|
|||||||
serializers
|
serializers
|
||||||
sexualized
|
sexualized
|
||||||
SQL
|
SQL
|
||||||
SSO
|
|
||||||
startup
|
startup
|
||||||
stdout
|
stdout
|
||||||
stylesheet
|
stylesheet
|
||||||
@@ -163,8 +159,6 @@ untrusted
|
|||||||
uptime
|
uptime
|
||||||
username
|
username
|
||||||
url
|
url
|
||||||
URI
|
|
||||||
URIs
|
|
||||||
validator
|
validator
|
||||||
versa
|
versa
|
||||||
versioning
|
versioning
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
.. _customers:
|
|
||||||
|
|
||||||
Customer accounts
|
|
||||||
=================
|
|
||||||
|
|
||||||
By default, pretix only offers guest checkout, i.e. ticket buyers do not sign up and sign back in, but create a new
|
|
||||||
checkout session every time. In some situations it may be convenient to allow ticket buyers to create
|
|
||||||
accounts that they can later log in to again. Working with customer accounts is even required for some advanced
|
|
||||||
use cases such as described in the :ref:`seasontickets` article.
|
|
||||||
|
|
||||||
Enabling customer accounts
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
To enable customer accounts, head to your organizer page in the backend and then select "Settings" → "General" →
|
|
||||||
"Customer accounts" and turn on the checkbox "Allow customers to create accounts".
|
|
||||||
|
|
||||||
Using the other settings on the same tab you can fine-tune how the customer account system behaves:
|
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/edit_customer.png
|
|
||||||
:align: center
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
Allow customers to log in with email address and password
|
|
||||||
In all simple setups, this option should be checked. If this checkbox is removed, it is impossible to log in or
|
|
||||||
sign up unless you connect a SSO provider (see below).
|
|
||||||
|
|
||||||
Match orders based on email address
|
|
||||||
If this option is selected, customers will see orders made with their email address within their account even if
|
|
||||||
they did not make those orders while logged in.
|
|
||||||
|
|
||||||
Name format, Allowed titles
|
|
||||||
This controls how we'll ask your customers for their name, similar to the respective settings on event level.
|
|
||||||
|
|
||||||
Managing customer accounts
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
After customer accounts have been enabled, you will find a new menu option "Customer accounts" in the organizer-level
|
|
||||||
main menu. The first sub-item, "Customers", allows you to search and inspect the list of your customer accounts, as well
|
|
||||||
as to create a new customer account from the backend:
|
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customers.png
|
|
||||||
:align: center
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
If you click on a customer ID, you can see all details of this customer account, including registration information,
|
|
||||||
active memberships, past ticket orders, and account history:
|
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customer.png
|
|
||||||
:align: center
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
You can also perform various actions from this view, such as:
|
|
||||||
|
|
||||||
- Send a password reset link
|
|
||||||
- Change registration information
|
|
||||||
- Anonymize the customer account (does not anonymize connected orders)
|
|
||||||
|
|
||||||
When creating or changing a customer, you will be presented with the following form:
|
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customer_edit.png
|
|
||||||
:align: center
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
Most fields, such as name, e-mail address, phone number, and language should be self-explanatory. The following fields
|
|
||||||
might require some explanation:
|
|
||||||
|
|
||||||
Account active
|
|
||||||
If this checkbox is removed, the customer will not be able to log in.
|
|
||||||
|
|
||||||
External identifier
|
|
||||||
This field can be used to cross-reference your customer database with other sources. For example, if the customer
|
|
||||||
already has a number in another system, you can insert that number here. This can be especially powerful if you
|
|
||||||
use our API for synchronization with an external system.
|
|
||||||
|
|
||||||
Verified email address
|
|
||||||
This checkbox signifies whether you have verified that this customer in fact controls the given email address.
|
|
||||||
This will automatically be checked after a successful registration or after a successful password reset. Before it
|
|
||||||
is checked, the customer will not be able to log in. You should usually not modify this field manually.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
Entries in this field will only be visible to you and your team, not to the customer.
|
|
||||||
|
|
||||||
Single-Sign-On (SSO)
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
"Single-Sign-On" (SSO) is a technical term for a situation in which a person can log in to multiple systems using just
|
|
||||||
one login. This can be convenient if you have multiple applications that are exposed to your customers: They won't have
|
|
||||||
to remember multiple passwords or understand how your application landscape is structured, they can just always log in
|
|
||||||
with the same credentials whenever they see your brand.
|
|
||||||
|
|
||||||
In this scenario, pretix can be **either** the "SSO provider" **or** the "SSO client".
|
|
||||||
If pretix is the SSO provider, pretix will be the central source of truth for your customer accounts and your other
|
|
||||||
applications can connect to pretix to use pretix's login functionality.
|
|
||||||
If pretix is the SSO client, one of your existing systems will be the source of truth for the customer accounts and
|
|
||||||
pretix will use that system's login functionality.
|
|
||||||
|
|
||||||
All SSO support for customer accounts in pretix is currently built on the `OpenID Connect`_ standard, a modern and
|
|
||||||
widely accepted standard for SSO in all industries.
|
|
||||||
|
|
||||||
Connecting SSO clients (pretix as the SSO provider)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
To connect an external application as a SSO client, go to "Customer accounts" → "SSO clients" → "Create a new SSO client"
|
|
||||||
in your organizer account.
|
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customer_ssoclient_add.png
|
|
||||||
:align: center
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
You will need to fill out the following fields:
|
|
||||||
|
|
||||||
Active
|
|
||||||
If this checkbox is removed, the SSO client can not be used.
|
|
||||||
|
|
||||||
Application name
|
|
||||||
The name of your external application, e.g. "digital event marketplace".
|
|
||||||
|
|
||||||
Client type
|
|
||||||
For a server-side application which is able to store a secret that will be inaccessible to end users, chose
|
|
||||||
"confidential". For a client-side application, such as many mobile apps, choose "public".
|
|
||||||
|
|
||||||
Grant type
|
|
||||||
This value depends on the OpenID Connect implementation of your software.
|
|
||||||
|
|
||||||
Redirection URIs
|
|
||||||
One or multiple URIs that the user might be redirected to after the successful or failed login.
|
|
||||||
|
|
||||||
Allowed access scopes
|
|
||||||
The types of data the SSO client may access about the customer.
|
|
||||||
|
|
||||||
After you submitted all data, you will receive a client ID as well as a client secret. The client secret is shown
|
|
||||||
in the green success message and will only ever be shown once. If you need it again, use the option "Invalidate old
|
|
||||||
client secret and generate a new one".
|
|
||||||
|
|
||||||
You will need the client ID and client secret to configure your external application. The application will also likely
|
|
||||||
need some other information from you, such as your **issuer URI**. If you use pretix Hosted and your organizer account
|
|
||||||
does not have a custom domain, your issuer will be ``https://pretix.eu/myorgname``, where ``myorgname`` is the short
|
|
||||||
form of your organizer account. If you use a custom domain, such as ``tickets.mycompany.net``, then your issuer will be
|
|
||||||
``https://tickets.mycompany.net``.
|
|
||||||
|
|
||||||
Technical details
|
|
||||||
"""""""""""""""""
|
|
||||||
|
|
||||||
We implement `OpenID Connect Core 1.0`_, except for some optional parts that do not make sense for pretix or bring no
|
|
||||||
additional value. For example, we do not currently support encrypted tokens, offline access, refresh tokens, or passing
|
|
||||||
request parameters as JWTs.
|
|
||||||
|
|
||||||
We implement the provider metadata section from `OpenID Connect Discovery 1.0`_. You can find the endpoint relative
|
|
||||||
to the issuer URI as described above, for example ``http://pretix.eu/demo/.well-known/openid-configuration``.
|
|
||||||
|
|
||||||
We implement all three OpenID Connect Core flows:
|
|
||||||
|
|
||||||
- Authorization Code Flow (response type ``code``)
|
|
||||||
- Implicit Flow (response types ``id_token token`` and ``id_token``)
|
|
||||||
- Hybrid Flow (response types ``code id_token``, ``code id_token token``, and ``code token``)
|
|
||||||
|
|
||||||
We implement the response modes ``query`` and ``fragment``.
|
|
||||||
|
|
||||||
We currently offer the following scopes: ``openid``, ``profile``, ``email``, ``phone``
|
|
||||||
|
|
||||||
As well as the following standardized claims: ``iss``, ``aud``, ``exp``, ``iat``, ``auth_time``, ``nonce``, ``c_hash``,
|
|
||||||
``at_hash``, ``sub``, ``locale``, ``name``, ``given_name``, ``family_name``, ``middle_name``, ``nickname``, ``email``,
|
|
||||||
``email_verified``, ``phone_number``.
|
|
||||||
|
|
||||||
The various endpoints are located relative to the issuer URI as described above:
|
|
||||||
|
|
||||||
- Authorization: ``<issuer>/oauth2/v1/authorize``
|
|
||||||
- Token: ``<issuer>/oauth2/v1/token``
|
|
||||||
- User info: ``<issuer>/oauth2/v1/userinfo``
|
|
||||||
- Keys: ``<issuer>/oauth2/v1/keys``
|
|
||||||
|
|
||||||
We currently do not reproduce their documentation here as they follow the OpenID Connect and OAuth specifications
|
|
||||||
without any special behavior.
|
|
||||||
|
|
||||||
Connecting SSO providers (pretix as the SSO client)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
|
|
||||||
in your organizer account.
|
|
||||||
|
|
||||||
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
|
|
||||||
:align: center
|
|
||||||
:class: screenshot
|
|
||||||
|
|
||||||
The "Provider name" and "Login button label" is what we'll use to show the new login option to the user. For the actual
|
|
||||||
connection, we will require information such as the issuer URL, client ID, client secret, scope, and field (or claim)
|
|
||||||
names that you will receive from your SSO provider.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you want your customers to *only* use your SSO provider, it makes sense to turn off the "Allow customers to log in
|
|
||||||
with email address and password" settings option (see above).
|
|
||||||
|
|
||||||
Technical details
|
|
||||||
"""""""""""""""""
|
|
||||||
|
|
||||||
We assume that SSO providers fulfill the following requirements:
|
|
||||||
|
|
||||||
- Implementation according to `OpenID Connect Core 1.0`_.
|
|
||||||
|
|
||||||
- Published meta-data document at ``<issuer>/.well-known/openid-configuration`` as specified in `OpenID Connect Discovery 1.0`_.
|
|
||||||
|
|
||||||
- Support for Authorization code flow (``response_type=code``) with ``response_mode=query``.
|
|
||||||
|
|
||||||
- Support for client authentication using client ID and client secret and without public key cryptography.
|
|
||||||
|
|
||||||
|
|
||||||
.. _OpenID Connect: https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)
|
|
||||||
.. _OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
|
|
||||||
.. _OpenID Connect Discovery 1.0: https://openid.net/specs/openid-connect-discovery-1_0.html
|
|
||||||
@@ -411,7 +411,7 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
If you use ``analytics.js`` (Universal Analytics)::
|
If you use ```analytics.js` (Universal Analytics)::
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ wanting to use pretix to sell tickets.
|
|||||||
events/settings
|
events/settings
|
||||||
events/structureguide
|
events/structureguide
|
||||||
events/widget
|
events/widget
|
||||||
customers/index
|
|
||||||
events/giftcards
|
events/giftcards
|
||||||
faq
|
faq
|
||||||
markdown
|
markdown
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "4.13.0"
|
__version__ = "4.13.0.dev0"
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# Generated by Django 3.2.12 on 2022-09-13 14:48
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0218_checkinlist_addon_match'),
|
|
||||||
('pretixapi', '0007_alter_webhookcall_target_url'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='WebHookCallRetry',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
|
||||||
('retry_not_before', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('retry_count', models.PositiveIntegerField(default=0)),
|
|
||||||
('action_type', models.CharField(max_length=255)),
|
|
||||||
('logentry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhook_retries', to='pretixbase.logentry')),
|
|
||||||
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='retries', to='pretixapi.webhook')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('webhook', 'logentry')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -133,18 +133,6 @@ class WebHookCall(models.Model):
|
|||||||
ordering = ("-datetime",)
|
ordering = ("-datetime",)
|
||||||
|
|
||||||
|
|
||||||
class WebHookCallRetry(models.Model):
|
|
||||||
id = models.BigAutoField(primary_key=True)
|
|
||||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='retries')
|
|
||||||
logentry = models.ForeignKey('pretixbase.LogEntry', on_delete=models.CASCADE, related_name='webhook_retries')
|
|
||||||
retry_not_before = models.DateTimeField(auto_now_add=True)
|
|
||||||
retry_count = models.PositiveIntegerField(default=0)
|
|
||||||
action_type = models.CharField(max_length=255)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = (('webhook', 'logentry'),)
|
|
||||||
|
|
||||||
|
|
||||||
class ApiCall(models.Model):
|
class ApiCall(models.Model):
|
||||||
idempotency_key = models.CharField(max_length=190, db_index=True)
|
idempotency_key = models.CharField(max_length=190, db_index=True)
|
||||||
auth_hash = models.CharField(max_length=190, db_index=True)
|
auth_hash = models.CharField(max_length=190, db_index=True)
|
||||||
|
|||||||
@@ -74,19 +74,13 @@ class CustomerSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
||||||
'locale', 'last_modified', 'notes')
|
'locale', 'last_modified', 'notes')
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
if instance and instance.provider_id:
|
|
||||||
validated_data['external_identifier'] = instance.external_identifier
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerCreateSerializer(CustomerSerializer):
|
class CustomerCreateSerializer(CustomerSerializer):
|
||||||
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
|
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
|
||||||
password = serializers.CharField(write_only=True, required=False, allow_null=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Customer
|
model = Customer
|
||||||
fields = CustomerSerializer.Meta.fields + ('send_email', 'password')
|
fields = CustomerSerializer.Meta.fields + ('send_email',)
|
||||||
|
|
||||||
|
|
||||||
class MembershipTypeSerializer(I18nAwareModelSerializer):
|
class MembershipTypeSerializer(I18nAwareModelSerializer):
|
||||||
@@ -119,21 +113,20 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
if 'secret' in data:
|
s = data['secret']
|
||||||
s = data['secret']
|
qs = GiftCard.objects.filter(
|
||||||
qs = GiftCard.objects.filter(
|
secret=s
|
||||||
secret=s
|
).filter(
|
||||||
).filter(
|
Q(issuer=self.context["organizer"]) | Q(
|
||||||
Q(issuer=self.context["organizer"]) | Q(
|
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||||
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
)
|
||||||
|
if self.instance:
|
||||||
|
qs = qs.exclude(pk=self.instance.pk)
|
||||||
|
if qs.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
{'secret': _(
|
||||||
|
'A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
||||||
)
|
)
|
||||||
if self.instance:
|
|
||||||
qs = qs.exclude(pk=self.instance.pk)
|
|
||||||
if qs.exists():
|
|
||||||
raise ValidationError(
|
|
||||||
{'secret': _(
|
|
||||||
'A gift card with the same secret already exists in your or an affiliated organizer account.')}
|
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -289,7 +282,6 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
|||||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||||
default_fields = [
|
default_fields = [
|
||||||
'customer_accounts',
|
'customer_accounts',
|
||||||
'customer_accounts_native',
|
|
||||||
'customer_accounts_link_by_email',
|
'customer_accounts_link_by_email',
|
||||||
'invoice_regenerate_allowed',
|
'invoice_regenerate_allowed',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ urlpatterns = [
|
|||||||
re_path(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
re_path(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
||||||
re_path(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
re_path(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||||
re_path(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
re_path(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||||
re_path(r"^device/info$", device.InfoView.as_view(), name="device.info"),
|
|
||||||
re_path(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
re_path(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||||
re_path(r"^idempotency_query$", idempotency.IdempotencyQueryView.as_view(), name="idempotency.query"),
|
re_path(r"^idempotency_query$", idempotency.IdempotencyQueryView.as_view(), name="idempotency.query"),
|
||||||
re_path(r"^upload$", upload.UploadView.as_view(), name="upload"),
|
re_path(r"^upload$", upload.UploadView.as_view(), name="upload"),
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from pretix import __version__
|
|
||||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||||
from pretix.api.views.version import numeric_version
|
|
||||||
from pretix.base.models import CheckinList, Device, SubEvent
|
from pretix.base.models import CheckinList, Device, SubEvent
|
||||||
from pretix.base.models.devices import Gate, generate_api_token
|
from pretix.base.models.devices import Gate, generate_api_token
|
||||||
|
|
||||||
@@ -153,24 +151,6 @@ class RevokeKeyView(APIView):
|
|||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class InfoView(APIView):
|
|
||||||
authentication_classes = (DeviceTokenAuthentication,)
|
|
||||||
|
|
||||||
def get(self, request, format=None):
|
|
||||||
device = request.auth
|
|
||||||
serializer = DeviceSerializer(device)
|
|
||||||
return Response({
|
|
||||||
'device': serializer.data,
|
|
||||||
'server': {
|
|
||||||
'version': {
|
|
||||||
'pretix': __version__,
|
|
||||||
'pretix_numeric': numeric_version(__version__),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class EventSelectionView(APIView):
|
class EventSelectionView(APIView):
|
||||||
authentication_classes = (DeviceTokenAuthentication,)
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
|
|||||||
@@ -515,8 +515,8 @@ class CustomerViewSet(viewsets.ModelViewSet):
|
|||||||
raise MethodNotAllowed("Customers cannot be deleted.")
|
raise MethodNotAllowed("Customers cannot be deleted.")
|
||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
def perform_create(self, serializer, send_email=False, password=None):
|
def perform_create(self, serializer, send_email=False):
|
||||||
customer = serializer.save(organizer=self.request.organizer, password=make_password(password))
|
customer = serializer.save(organizer=self.request.organizer, password=make_password(None))
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.customer.created',
|
'pretix.customer.created',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -530,7 +530,7 @@ class CustomerViewSet(viewsets.ModelViewSet):
|
|||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = CustomerCreateSerializer(data=request.data, context=self.get_serializer_context())
|
serializer = CustomerCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
self.perform_create(serializer, send_email=serializer.validated_data.pop('send_email', False), password=serializer.validated_data.pop('password', None))
|
self.perform_create(serializer, send_email=serializer.validated_data.pop('send_email', False))
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
|||||||
@@ -23,24 +23,19 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.db import DatabaseError, connection, transaction
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from pretix.api.models import (
|
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
||||||
WebHook, WebHookCall, WebHookCallRetry, WebHookEventListener,
|
|
||||||
)
|
|
||||||
from pretix.api.signals import register_webhook_events
|
from pretix.api.signals import register_webhook_events
|
||||||
from pretix.base.models import LogEntry
|
from pretix.base.models import LogEntry
|
||||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
||||||
from pretix.base.signals import periodic_task
|
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -224,10 +219,6 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.order.expired',
|
'pretix.event.order.expired',
|
||||||
_('Order expired'),
|
_('Order expired'),
|
||||||
),
|
),
|
||||||
ParametrizedOrderWebhookEvent(
|
|
||||||
'pretix.event.order.expirychanged',
|
|
||||||
_('Order expiry date changed'),
|
|
||||||
),
|
|
||||||
ParametrizedOrderWebhookEvent(
|
ParametrizedOrderWebhookEvent(
|
||||||
'pretix.event.order.modified',
|
'pretix.event.order.modified',
|
||||||
_('Order information changed'),
|
_('Order information changed'),
|
||||||
@@ -240,30 +231,10 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.order.changed.*',
|
'pretix.event.order.changed.*',
|
||||||
_('Order changed'),
|
_('Order changed'),
|
||||||
),
|
),
|
||||||
ParametrizedOrderWebhookEvent(
|
|
||||||
'pretix.event.order.refund.created',
|
|
||||||
_('Refund of payment created'),
|
|
||||||
),
|
|
||||||
ParametrizedOrderWebhookEvent(
|
ParametrizedOrderWebhookEvent(
|
||||||
'pretix.event.order.refund.created.externally',
|
'pretix.event.order.refund.created.externally',
|
||||||
_('External refund of payment'),
|
_('External refund of payment'),
|
||||||
),
|
),
|
||||||
ParametrizedOrderWebhookEvent(
|
|
||||||
'pretix.event.order.refund.requested',
|
|
||||||
_('Refund of payment requested by customer'),
|
|
||||||
),
|
|
||||||
ParametrizedOrderWebhookEvent(
|
|
||||||
'pretix.event.order.refund.done',
|
|
||||||
_('Refund of payment completed'),
|
|
||||||
),
|
|
||||||
ParametrizedOrderWebhookEvent(
|
|
||||||
'pretix.event.order.refund.canceled',
|
|
||||||
_('Refund of payment canceled'),
|
|
||||||
),
|
|
||||||
ParametrizedOrderWebhookEvent(
|
|
||||||
'pretix.event.order.refund.failed',
|
|
||||||
_('Refund of payment failed'),
|
|
||||||
),
|
|
||||||
ParametrizedOrderWebhookEvent(
|
ParametrizedOrderWebhookEvent(
|
||||||
'pretix.event.order.approved',
|
'pretix.event.order.approved',
|
||||||
_('Order approved'),
|
_('Order approved'),
|
||||||
@@ -304,22 +275,6 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.subevent.deleted',
|
'pretix.subevent.deleted',
|
||||||
pgettext_lazy('subevent', 'Event series date deleted'),
|
pgettext_lazy('subevent', 'Event series date deleted'),
|
||||||
),
|
),
|
||||||
ParametrizedEventWebhookEvent(
|
|
||||||
'pretix.event.live.activated',
|
|
||||||
_('Shop taken live'),
|
|
||||||
),
|
|
||||||
ParametrizedEventWebhookEvent(
|
|
||||||
'pretix.event.live.deactivated',
|
|
||||||
_('Shop taken offline'),
|
|
||||||
),
|
|
||||||
ParametrizedEventWebhookEvent(
|
|
||||||
'pretix.event.testmode.activated',
|
|
||||||
_('Test-Mode of shop has been activated'),
|
|
||||||
),
|
|
||||||
ParametrizedEventWebhookEvent(
|
|
||||||
'pretix.event.testmode.deactivated',
|
|
||||||
_('Test-Mode of shop has been deactivated'),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -361,163 +316,59 @@ def notify_webhooks(logentry_ids: list):
|
|||||||
send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))
|
send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))
|
||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=60, acks_late=True, autoretry_for=(DatabaseError,),)
|
@app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True)
|
||||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int, retry_count: int = 0):
|
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||||
"""
|
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||||
Sends out a specific webhook using adequate retry and error handling logic.
|
|
||||||
|
|
||||||
Our retry logic is a little complex since we have different constraints here:
|
|
||||||
|
|
||||||
1. We historically documented that we retry for up to three days, so we want to keep that
|
|
||||||
promise. We want to use (approximately) exponentially increasing times to keep load
|
|
||||||
manageable.
|
|
||||||
|
|
||||||
2. We want to use Celery's ``acks_late=True`` options which prevents lost tasks if a worker
|
|
||||||
crashes.
|
|
||||||
|
|
||||||
3. A limitation of Celery's redis broker implementation is that it can not properly handle
|
|
||||||
tasks that *run or wait* longer than `visibility_timeout`, which defaults to 1h, when
|
|
||||||
``acks_late`` is enabled. So any task with a *retry interval* of >1h will be restarted
|
|
||||||
many times because celery believes the worker has crashed.
|
|
||||||
|
|
||||||
4. We do like that the first few retries happen within a few seconds to work around very
|
|
||||||
intermittent connectivity issues quickly. For the longer retries with multiple hours,
|
|
||||||
we don't care if they are emitted a few minutes too late.
|
|
||||||
|
|
||||||
We therefore have a two-phase retry process:
|
|
||||||
|
|
||||||
- For all retry intervals below 5 minutes, which is the first 3 retries currently, we
|
|
||||||
schedule a new celery task directly with an increased retry_count. We do *not* use
|
|
||||||
celery's retry() call currently to make the retry process in both phases more similar,
|
|
||||||
there should not be much of a difference though (except that the initial task will be in
|
|
||||||
SUCCESS state, but we don't check that status anywhere).
|
|
||||||
|
|
||||||
- For all retry intervals of at least 5 minutes, we create a database entry. Then, the
|
|
||||||
periodic task ``schedule_webhook_retries_on_celery`` will schedule celery tasks for them
|
|
||||||
once their time has come.
|
|
||||||
"""
|
|
||||||
retry_intervals = (
|
|
||||||
5, # + 5 seconds
|
|
||||||
30, # + 30 seconds
|
|
||||||
60, # + 1 minute
|
|
||||||
300, # + 5 minutes
|
|
||||||
1200, # + 20 minutes
|
|
||||||
3600, # + 60 minutes
|
|
||||||
1440, # + 4 hours
|
|
||||||
21600, # + 6 hours
|
|
||||||
43200, # + 12 hours
|
|
||||||
43200, # + 24 hours
|
|
||||||
86400, # + 24 hours
|
|
||||||
) # added up, these are approximately 3 days, as documented
|
|
||||||
retry_celery_cutoff = 300
|
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
webhook = WebHook.objects.get(id=webhook_id)
|
webhook = WebHook.objects.get(id=webhook_id)
|
||||||
|
with scope(organizer=webhook.organizer):
|
||||||
with scope(organizer=webhook.organizer), transaction.atomic():
|
|
||||||
logentry = LogEntry.all.get(id=logentry_id)
|
logentry = LogEntry.all.get(id=logentry_id)
|
||||||
types = get_all_webhook_events()
|
types = get_all_webhook_events()
|
||||||
event_type = types.get(action_type)
|
event_type = types.get(action_type)
|
||||||
if not event_type or not webhook.enabled:
|
if not event_type or not webhook.enabled:
|
||||||
return 'obsolete-webhook' # Ignore, e.g. plugin not installed
|
return # Ignore, e.g. plugin not installed
|
||||||
|
|
||||||
payload = event_type.build_payload(logentry)
|
payload = event_type.build_payload(logentry)
|
||||||
if payload is None:
|
if payload is None:
|
||||||
# Content object deleted?
|
# Content object deleted?
|
||||||
return 'obsolete-payload'
|
return
|
||||||
|
|
||||||
t = time.time()
|
t = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post(
|
try:
|
||||||
webhook.target_url,
|
resp = requests.post(
|
||||||
json=payload,
|
webhook.target_url,
|
||||||
allow_redirects=False,
|
json=payload,
|
||||||
timeout=30,
|
allow_redirects=False
|
||||||
)
|
|
||||||
WebHookCall.objects.create(
|
|
||||||
webhook=webhook,
|
|
||||||
action_type=logentry.action_type,
|
|
||||||
target_url=webhook.target_url,
|
|
||||||
is_retry=self.request.retries > 0,
|
|
||||||
execution_time=time.time() - t,
|
|
||||||
return_code=resp.status_code,
|
|
||||||
payload=json.dumps(payload),
|
|
||||||
response_body=resp.text[:1024 * 1024],
|
|
||||||
success=200 <= resp.status_code <= 299
|
|
||||||
)
|
|
||||||
if resp.status_code == 410:
|
|
||||||
webhook.enabled = False
|
|
||||||
webhook.save()
|
|
||||||
return 'gone'
|
|
||||||
elif resp.status_code > 299:
|
|
||||||
if retry_count >= len(retry_intervals):
|
|
||||||
return 'retry-given-up'
|
|
||||||
elif retry_intervals[retry_count] < retry_celery_cutoff:
|
|
||||||
send_webhook.apply_async(args=(logentry_id, action_type, webhook_id, retry_count + 1),
|
|
||||||
countdown=retry_intervals[retry_count])
|
|
||||||
return 'retry-via-celery'
|
|
||||||
else:
|
|
||||||
webhook.retries.update_or_create(
|
|
||||||
logentry=logentry,
|
|
||||||
defaults=dict(
|
|
||||||
retry_not_before=now() + timedelta(seconds=retry_intervals[retry_count]),
|
|
||||||
retry_count=retry_count + 1,
|
|
||||||
action_type=action_type,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return 'retry-via-db'
|
|
||||||
return 'ok'
|
|
||||||
except RequestException as e:
|
|
||||||
WebHookCall.objects.create(
|
|
||||||
webhook=webhook,
|
|
||||||
action_type=logentry.action_type,
|
|
||||||
target_url=webhook.target_url,
|
|
||||||
is_retry=self.request.retries > 0,
|
|
||||||
execution_time=time.time() - t,
|
|
||||||
return_code=0,
|
|
||||||
payload=json.dumps(payload),
|
|
||||||
response_body=str(e)[:1024 * 1024]
|
|
||||||
)
|
|
||||||
if retry_count >= len(retry_intervals):
|
|
||||||
return 'retry-given-up'
|
|
||||||
elif retry_intervals[retry_count] < retry_celery_cutoff:
|
|
||||||
send_webhook.apply_async(args=(logentry_id, action_type, webhook_id, retry_count + 1))
|
|
||||||
return 'retry-via-celery'
|
|
||||||
else:
|
|
||||||
webhook.retries.update_or_create(
|
|
||||||
logentry=logentry,
|
|
||||||
defaults=dict(
|
|
||||||
retry_not_before=now() + timedelta(seconds=retry_intervals[retry_count]),
|
|
||||||
retry_count=retry_count + 1,
|
|
||||||
action_type=action_type,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return 'retry-via-db'
|
WebHookCall.objects.create(
|
||||||
|
webhook=webhook,
|
||||||
|
action_type=logentry.action_type,
|
||||||
@app.task(base=TransactionAwareTask)
|
target_url=webhook.target_url,
|
||||||
def manually_retry_all_calls(webhook_id: int):
|
is_retry=self.request.retries > 0,
|
||||||
with scopes_disabled():
|
execution_time=time.time() - t,
|
||||||
webhook = WebHook.objects.get(id=webhook_id)
|
return_code=resp.status_code,
|
||||||
with scope(organizer=webhook.organizer), transaction.atomic():
|
payload=json.dumps(payload),
|
||||||
for whcr in webhook.retries.select_for_update(
|
response_body=resp.text[:1024 * 1024],
|
||||||
skip_locked=connection.features.has_select_for_update_skip_locked
|
success=200 <= resp.status_code <= 299
|
||||||
):
|
)
|
||||||
send_webhook.apply_async(
|
if resp.status_code == 410:
|
||||||
args=(whcr.logentry_id, whcr.action_type, whcr.webhook_id, whcr.retry_count),
|
webhook.enabled = False
|
||||||
)
|
webhook.save()
|
||||||
whcr.delete()
|
elif resp.status_code > 299:
|
||||||
|
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||||
|
except RequestException as e:
|
||||||
@receiver(signal=periodic_task, dispatch_uid='pretixapi_schedule_webhook_retries_on_celery')
|
WebHookCall.objects.create(
|
||||||
@scopes_disabled()
|
webhook=webhook,
|
||||||
def schedule_webhook_retries_on_celery(sender, **kwargs):
|
action_type=logentry.action_type,
|
||||||
with transaction.atomic():
|
target_url=webhook.target_url,
|
||||||
for whcr in WebHookCallRetry.objects.select_for_update(
|
is_retry=self.request.retries > 0,
|
||||||
skip_locked=connection.features.has_select_for_update_skip_locked
|
execution_time=time.time() - t,
|
||||||
).filter(retry_not_before__lt=now()):
|
return_code=0,
|
||||||
send_webhook.apply_async(
|
payload=json.dumps(payload),
|
||||||
args=(whcr.logentry_id, whcr.action_type, whcr.webhook_id, whcr.retry_count),
|
response_body=str(e)[:1024 * 1024]
|
||||||
)
|
)
|
||||||
whcr.delete()
|
raise self.retry(countdown=2 ** (self.request.retries * 2)) # max is 2 ** (8*2) = 65536 seconds = ~18 hours
|
||||||
|
except MaxRetriesExceededError:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
from urllib.parse import urlencode, urljoin
|
|
||||||
|
|
||||||
import jwt
|
|
||||||
import requests
|
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key
|
|
||||||
from cryptography.hazmat.primitives.serialization import (
|
|
||||||
Encoding, NoEncryption, PrivateFormat, PublicFormat,
|
|
||||||
)
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from requests import RequestException
|
|
||||||
|
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
This module contains utilities for implementing OpenID Connect for customer authentication both as a receiving party (RP)
|
|
||||||
as well as an OpenID Provider (OP).
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def _urljoin(base, path):
|
|
||||||
if not base.endswith("/"):
|
|
||||||
base += "/"
|
|
||||||
return urljoin(base, path)
|
|
||||||
|
|
||||||
|
|
||||||
def oidc_validate_and_complete_config(config):
|
|
||||||
for k in ("base_url", "client_id", "client_secret", "uid_field", "email_field", "scope"):
|
|
||||||
if not config.get(k):
|
|
||||||
raise ValidationError(_('Configuration option "{name}" is missing.').format(name=k))
|
|
||||||
|
|
||||||
conf_url = _urljoin(config["base_url"], ".well-known/openid-configuration")
|
|
||||||
try:
|
|
||||||
resp = requests.get(conf_url, timeout=10)
|
|
||||||
resp.raise_for_status()
|
|
||||||
provider_config = resp.json()
|
|
||||||
except RequestException as e:
|
|
||||||
raise ValidationError(_('Unable to retrieve configuration from "{url}". Error message: "{error}".').format(
|
|
||||||
url=conf_url,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
except ValueError as e:
|
|
||||||
raise ValidationError(_('Unable to retrieve configuration from "{url}". Error message: "{error}".').format(
|
|
||||||
url=conf_url,
|
|
||||||
error=str(e)
|
|
||||||
))
|
|
||||||
|
|
||||||
if not provider_config.get("authorization_endpoint"):
|
|
||||||
raise ValidationError(_('Incompatible SSO provider: "{error}".').format(
|
|
||||||
error="authorization_endpoint not set"
|
|
||||||
))
|
|
||||||
|
|
||||||
if not provider_config.get("userinfo_endpoint"):
|
|
||||||
raise ValidationError(_('Incompatible SSO provider: "{error}".').format(
|
|
||||||
error="userinfo_endpoint not set"
|
|
||||||
))
|
|
||||||
|
|
||||||
if not provider_config.get("token_endpoint"):
|
|
||||||
raise ValidationError(_('Incompatible SSO provider: "{error}".').format(
|
|
||||||
error="token_endpoint not set"
|
|
||||||
))
|
|
||||||
|
|
||||||
if "code" not in provider_config.get("response_types_supported", []):
|
|
||||||
raise ValidationError(_('Incompatible SSO provider: "{error}".').format(
|
|
||||||
error=f"provider supports response types {','.join(provider_config.get('response_types_supported', []))}, but we only support 'code'."
|
|
||||||
))
|
|
||||||
|
|
||||||
if "query" not in provider_config.get("response_modes_supported", ["query", "fragment"]):
|
|
||||||
raise ValidationError(_('Incompatible SSO provider: "{error}".').format(
|
|
||||||
error=f"provider supports response modes {','.join(provider_config.get('response_modes_supported', []))}, but we only support 'query'."
|
|
||||||
))
|
|
||||||
|
|
||||||
if "authorization_code" not in provider_config.get("grant_types_supported", ["authorization_code", "implicit"]):
|
|
||||||
raise ValidationError(_('Incompatible SSO provider: "{error}".').format(
|
|
||||||
error=f"provider supports grant types {','.join(provider_config.get('grant_types_supported', ''))}, but we only support 'authorization_code'."
|
|
||||||
))
|
|
||||||
|
|
||||||
if "openid" not in config["scope"].split(" "):
|
|
||||||
raise ValidationError(
|
|
||||||
_('You are not requesting "{scope}".').format(
|
|
||||||
scope="openid",
|
|
||||||
))
|
|
||||||
|
|
||||||
for scope in config["scope"].split(" "):
|
|
||||||
if scope not in provider_config.get("scopes_supported", []):
|
|
||||||
raise ValidationError(_('You are requesting scope "{scope}" but provider only supports these: {scopes}.').format(
|
|
||||||
scope=scope,
|
|
||||||
scopes=", ".join(provider_config.get("scopes_supported", []))
|
|
||||||
))
|
|
||||||
|
|
||||||
for k, v in config.items():
|
|
||||||
if k.endswith('_field') and v:
|
|
||||||
if v not in provider_config.get("claims_supported", []): # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
|
||||||
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
|
|
||||||
field=v,
|
|
||||||
fields=", ".join(provider_config.get("claims_supported", []))
|
|
||||||
))
|
|
||||||
|
|
||||||
config['provider_config'] = provider_config
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def oidc_authorize_url(provider, state, redirect_uri):
|
|
||||||
endpoint = provider.configuration['provider_config']['authorization_endpoint']
|
|
||||||
params = {
|
|
||||||
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
|
||||||
'response_type': 'code',
|
|
||||||
'client_id': provider.configuration['client_id'],
|
|
||||||
'scope': provider.configuration['scope'],
|
|
||||||
'state': state,
|
|
||||||
'redirect_uri': redirect_uri,
|
|
||||||
}
|
|
||||||
return endpoint + '?' + urlencode(params)
|
|
||||||
|
|
||||||
|
|
||||||
def oidc_validate_authorization(provider, code, redirect_uri):
|
|
||||||
endpoint = provider.configuration['provider_config']['token_endpoint']
|
|
||||||
params = {
|
|
||||||
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
||||||
'grant_type': 'authorization_code',
|
|
||||||
'code': code,
|
|
||||||
'redirect_uri': redirect_uri,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
resp = requests.post(
|
|
||||||
endpoint,
|
|
||||||
data=params,
|
|
||||||
headers={
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
auth=(provider.configuration['client_id'], provider.configuration['client_secret']),
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
except RequestException:
|
|
||||||
logger.exception('Could not retrieve authorization token')
|
|
||||||
raise ValidationError(
|
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
|
||||||
error='could not reach login provider',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'access_token' not in data:
|
|
||||||
raise ValidationError(
|
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
|
||||||
error='access token missing',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
endpoint = provider.configuration['provider_config']['userinfo_endpoint']
|
|
||||||
try:
|
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
|
||||||
resp = requests.get(
|
|
||||||
endpoint,
|
|
||||||
headers={
|
|
||||||
'Authorization': f'Bearer {data["access_token"]}'
|
|
||||||
},
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
userinfo = resp.json()
|
|
||||||
except RequestException:
|
|
||||||
logger.exception('Could not retrieve user info')
|
|
||||||
raise ValidationError(
|
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
|
||||||
error='could not fetch user info',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'email_verified' in userinfo and not userinfo['email_verified']:
|
|
||||||
# todo: how universal is this, do we need to make this configurable?
|
|
||||||
raise ValidationError(_('The email address on this account is not yet verified. Please first confirm the '
|
|
||||||
'email address in your customer account.'))
|
|
||||||
|
|
||||||
profile = {}
|
|
||||||
for k, v in provider.configuration.items():
|
|
||||||
if k.endswith('_field'):
|
|
||||||
profile[k[:-6]] = userinfo.get(v)
|
|
||||||
|
|
||||||
if not profile.get('uid'):
|
|
||||||
raise ValidationError(
|
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
|
||||||
error='could not fetch user id',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not profile.get('email'):
|
|
||||||
raise ValidationError(
|
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
|
||||||
error='could not fetch user email',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return profile
|
|
||||||
|
|
||||||
|
|
||||||
def _hash_scheme(value):
|
|
||||||
# As described in https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
|
|
||||||
digest = hashlib.sha256(value.encode()).digest()
|
|
||||||
digest_truncated = digest[:(len(digest) // 2)]
|
|
||||||
return base64.urlsafe_b64encode(digest_truncated).decode().rstrip("=")
|
|
||||||
|
|
||||||
|
|
||||||
def customer_claims(customer, scope):
|
|
||||||
scope = scope.split(' ')
|
|
||||||
claims = {
|
|
||||||
'sub': customer.identifier,
|
|
||||||
'locale': customer.locale,
|
|
||||||
}
|
|
||||||
if 'profile' in scope:
|
|
||||||
if customer.name:
|
|
||||||
claims['name'] = customer.name
|
|
||||||
if 'given_name' in customer.name_parts:
|
|
||||||
claims['given_name'] = customer.name_parts['given_name']
|
|
||||||
if 'family_name' in customer.name_parts:
|
|
||||||
claims['family_name'] = customer.name_parts['family_name']
|
|
||||||
if 'middle_name' in customer.name_parts:
|
|
||||||
claims['middle_name'] = customer.name_parts['middle_name']
|
|
||||||
if 'calling_name' in customer.name_parts:
|
|
||||||
claims['nickname'] = customer.name_parts['calling_name']
|
|
||||||
if 'email' in scope and customer.email:
|
|
||||||
claims['email'] = customer.email
|
|
||||||
claims['email_verified'] = customer.is_verified
|
|
||||||
if 'phone' in scope and customer.phone:
|
|
||||||
claims['phone_number'] = customer.phone.as_international
|
|
||||||
return claims
|
|
||||||
|
|
||||||
|
|
||||||
def _get_or_create_server_keypair(organizer):
|
|
||||||
if not organizer.settings.sso_server_signing_key_rsa256_private:
|
|
||||||
privkey = generate_private_key(key_size=4096, public_exponent=65537)
|
|
||||||
pubkey = privkey.public_key()
|
|
||||||
organizer.settings.sso_server_signing_key_rsa256_private = privkey.private_bytes(
|
|
||||||
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()
|
|
||||||
).decode()
|
|
||||||
organizer.settings.sso_server_signing_key_rsa256_public = pubkey.public_bytes(
|
|
||||||
Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
|
|
||||||
).decode()
|
|
||||||
return organizer.settings.sso_server_signing_key_rsa256_private, organizer.settings.sso_server_signing_key_rsa256_public
|
|
||||||
|
|
||||||
|
|
||||||
def generate_id_token(customer, client, auth_time, nonce, scope, expires: datetime, scope_claims=False, with_code=None, with_access_token=None):
|
|
||||||
payload = {
|
|
||||||
'iss': build_absolute_uri(client.organizer, 'presale:organizer.index').rstrip('/'),
|
|
||||||
'aud': client.client_id,
|
|
||||||
'exp': int(expires.timestamp()),
|
|
||||||
'iat': int(time.time()),
|
|
||||||
'auth_time': auth_time,
|
|
||||||
**customer_claims(customer, client.evaluated_scope(scope) if scope_claims else ''),
|
|
||||||
}
|
|
||||||
if nonce:
|
|
||||||
payload['nonce'] = nonce
|
|
||||||
if with_code:
|
|
||||||
payload['c_hash'] = _hash_scheme(with_code)
|
|
||||||
if with_access_token:
|
|
||||||
payload['at_hash'] = _hash_scheme(with_access_token)
|
|
||||||
privkey, pubkey = _get_or_create_server_keypair(client.organizer)
|
|
||||||
return jwt.encode(
|
|
||||||
payload,
|
|
||||||
privkey,
|
|
||||||
headers={
|
|
||||||
"kid": hashlib.sha256(pubkey.encode()).hexdigest()[:16]
|
|
||||||
},
|
|
||||||
algorithm="RS256",
|
|
||||||
)
|
|
||||||
@@ -300,8 +300,7 @@ def get_email_context(**kwargs):
|
|||||||
kwargs.setdefault("position_or_address", kwargs['position'])
|
kwargs.setdefault("position_or_address", kwargs['position'])
|
||||||
if 'order' in kwargs:
|
if 'order' in kwargs:
|
||||||
try:
|
try:
|
||||||
if not kwargs.get('invoice_address'):
|
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
|
||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
|
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ from django.core.validators import (
|
|||||||
)
|
)
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.forms import Select, widgets
|
from django.forms import Select, widgets
|
||||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@@ -430,7 +429,7 @@ class PortraitImageWidget(UploadedFileWidget):
|
|||||||
|
|
||||||
def value_from_datadict(self, data, files, name):
|
def value_from_datadict(self, data, files, name):
|
||||||
d = super().value_from_datadict(data, files, name)
|
d = super().value_from_datadict(data, files, name)
|
||||||
if d is not None and d is not False and d is not FILE_INPUT_CONTRADICTION:
|
if d is not None and d is not False:
|
||||||
d._cropdata = json.loads(data.get(name + '_cropdata', '{}') or '{}')
|
d._cropdata = json.loads(data.get(name + '_cropdata', '{}') or '{}')
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|||||||
@@ -533,7 +533,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
tstyledata = [
|
tstyledata = [
|
||||||
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
|
||||||
('FONTNAME', (0, 0), (-1, 0), self.font_bold),
|
('FONTNAME', (0, 0), (-1, 0), self.font_bold),
|
||||||
('FONTNAME', (0, -1), (-1, -1), self.font_bold),
|
('FONTNAME', (0, -1), (-1, -1), self.font_bold),
|
||||||
('LEFTPADDING', (0, 0), (0, -1), 0),
|
('LEFTPADDING', (0, 0), (0, -1), 0),
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# Generated by Django 3.2.12 on 2022-07-06 09:13
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import i18nfield.fields
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.base
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0218_checkinlist_addon_match'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CustomerSSOProvider',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
|
||||||
('name', i18nfield.fields.I18nCharField(max_length=200)),
|
|
||||||
('is_active', models.BooleanField(default=True)),
|
|
||||||
('button_label', i18nfield.fields.I18nCharField(max_length=200)),
|
|
||||||
('method', models.CharField(max_length=190)),
|
|
||||||
('configuration', models.JSONField()),
|
|
||||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sso_providers', to='pretixbase.organizer')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='customer',
|
|
||||||
name='provider',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='pretixbase.customerssoprovider'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Generated by Django 3.2.12 on 2022-08-11 10:02
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.base
|
|
||||||
import pretix.base.models.customers
|
|
||||||
import pretix.base.models.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0219_auto_20220706_0913'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CustomerSSOClient',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('is_active', models.BooleanField(default=True)),
|
|
||||||
('client_id', models.CharField(db_index=True, default=pretix.base.models.customers.generate_client_id, max_length=100, unique=True)),
|
|
||||||
('client_secret', models.CharField(max_length=255)),
|
|
||||||
('client_type', models.CharField(default='confidential', max_length=32)),
|
|
||||||
('authorization_grant_type', models.CharField(default='authorization-code', max_length=32)),
|
|
||||||
('redirect_uris', models.TextField()),
|
|
||||||
('allowed_scopes', pretix.base.models.fields.MultiStringField(default=['openid', 'profile', 'email', 'phone'])),
|
|
||||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sso_clients', to='pretixbase.organizer')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='customer',
|
|
||||||
name='provider',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='customers', to='pretixbase.customerssoprovider'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CustomerSSOGrant',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
|
||||||
('code', models.CharField(max_length=255, unique=True)),
|
|
||||||
('nonce', models.CharField(max_length=255, null=True)),
|
|
||||||
('auth_time', models.IntegerField()),
|
|
||||||
('expires', models.DateTimeField()),
|
|
||||||
('redirect_uri', models.TextField()),
|
|
||||||
('scope', models.TextField()),
|
|
||||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='grants', to='pretixbase.customerssoclient')),
|
|
||||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sso_grants', to='pretixbase.customer')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CustomerSSOAccessToken',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
|
||||||
('from_code', models.CharField(max_length=255, null=True)),
|
|
||||||
('token', models.CharField(max_length=255, unique=True)),
|
|
||||||
('expires', models.DateTimeField()),
|
|
||||||
('scope', models.TextField()),
|
|
||||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_tokens', to='pretixbase.customerssoclient')),
|
|
||||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sso_access_tokens', to='pretixbase.customer')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 3.2.4 on 2021-12-01 11:55
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db.models import Count
|
|
||||||
|
|
||||||
|
|
||||||
def change_unique_identifiers(apps, schema_editor):
|
|
||||||
# We cannot really know if a position was bundled or an add-on, but we can at least guess
|
|
||||||
Question = apps.get_model("pretixbase", "Question")
|
|
||||||
|
|
||||||
for r in Question.objects.values('event', 'identifier').order_by().annotate(c=Count('*')).filter(c__gt=1):
|
|
||||||
qs = Question.objects.filter(identifier=r['identifier'], event_id=r['event'])
|
|
||||||
for i, q in enumerate(qs[1:]):
|
|
||||||
q.identifier += f'_{i + 2}'
|
|
||||||
q.save(update_fields=['identifier'])
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0220_auto_20220811_1002'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(
|
|
||||||
change_unique_identifiers,
|
|
||||||
migrations.RunPython.noop,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 3.2.4 on 2021-12-01 12:04
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pretixbase', '0221_clean_nonunique_question_identifiers'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='question',
|
|
||||||
unique_together={('event', 'identifier')},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -24,59 +24,27 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.hashers import (
|
from django.contrib.auth.hashers import (
|
||||||
check_password, is_password_usable, make_password,
|
check_password, is_password_usable, make_password,
|
||||||
)
|
)
|
||||||
from django.core.validators import RegexValidator, URLValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.utils.crypto import get_random_string, salted_hmac
|
from django.utils.crypto import get_random_string, salted_hmac
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.fields import I18nCharField
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
|
||||||
from pretix.base.banlist import banned
|
from pretix.base.banlist import banned
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.fields import MultiStringField
|
|
||||||
from pretix.base.models.organizer import Organizer
|
from pretix.base.models.organizer import Organizer
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.helpers.countries import FastCountryField
|
from pretix.helpers.countries import FastCountryField
|
||||||
|
|
||||||
|
|
||||||
class CustomerSSOProvider(LoggedModel):
|
|
||||||
METHOD_OIDC = 'oidc'
|
|
||||||
METHODS = (
|
|
||||||
(METHOD_OIDC, 'OpenID Connect'),
|
|
||||||
)
|
|
||||||
|
|
||||||
id = models.BigAutoField(primary_key=True)
|
|
||||||
organizer = models.ForeignKey(Organizer, related_name='sso_providers', on_delete=models.CASCADE)
|
|
||||||
name = I18nCharField(
|
|
||||||
max_length=200,
|
|
||||||
verbose_name=_("Provider name"),
|
|
||||||
)
|
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
|
||||||
button_label = I18nCharField(
|
|
||||||
max_length=200,
|
|
||||||
verbose_name=_("Login button label"),
|
|
||||||
)
|
|
||||||
method = models.CharField(
|
|
||||||
max_length=190,
|
|
||||||
verbose_name=_("Single-sign-on method"),
|
|
||||||
null=False, blank=False,
|
|
||||||
choices=METHODS,
|
|
||||||
)
|
|
||||||
configuration = models.JSONField()
|
|
||||||
|
|
||||||
def allow_delete(self):
|
|
||||||
return not self.customers.exists()
|
|
||||||
|
|
||||||
|
|
||||||
class Customer(LoggedModel):
|
class Customer(LoggedModel):
|
||||||
"""
|
"""
|
||||||
Represents a registered customer of an organizer.
|
Represents a registered customer of an organizer.
|
||||||
"""
|
"""
|
||||||
id = models.BigAutoField(primary_key=True)
|
id = models.BigAutoField(primary_key=True)
|
||||||
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
|
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
|
||||||
provider = models.ForeignKey(CustomerSSOProvider, related_name='customers', on_delete=models.PROTECT, null=True, blank=True)
|
|
||||||
identifier = models.CharField(
|
identifier = models.CharField(
|
||||||
max_length=190,
|
max_length=190,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
@@ -349,134 +317,3 @@ class AttendeeProfile(models.Model):
|
|||||||
parts.append(f'{a["field_label"]}: {val}')
|
parts.append(f'{a["field_label"]}: {val}')
|
||||||
|
|
||||||
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
|
return '\n'.join([str(p).strip() for p in parts if p and str(p).strip()])
|
||||||
|
|
||||||
|
|
||||||
def generate_client_id():
|
|
||||||
return get_random_string(40)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_client_secret():
|
|
||||||
return get_random_string(40)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerSSOClient(LoggedModel):
|
|
||||||
CLIENT_CONFIDENTIAL = "confidential"
|
|
||||||
CLIENT_PUBLIC = "public"
|
|
||||||
CLIENT_TYPES = (
|
|
||||||
(CLIENT_CONFIDENTIAL, pgettext_lazy("openidconnect", "Confidential")),
|
|
||||||
(CLIENT_PUBLIC, pgettext_lazy("openidconnect", "Public")),
|
|
||||||
)
|
|
||||||
|
|
||||||
GRANT_AUTHORIZATION_CODE = "authorization-code"
|
|
||||||
GRANT_IMPLICIT = "implicit"
|
|
||||||
GRANT_TYPES = (
|
|
||||||
(GRANT_AUTHORIZATION_CODE, pgettext_lazy("openidconnect", "Authorization code")),
|
|
||||||
(GRANT_IMPLICIT, pgettext_lazy("openidconnect", "Implicit")),
|
|
||||||
)
|
|
||||||
|
|
||||||
SCOPE_CHOICES = (
|
|
||||||
('openid', _('OpenID Connect access (required)')),
|
|
||||||
('profile', _('Profile data (name, addresses)')),
|
|
||||||
('email', _('E-mail address')),
|
|
||||||
('phone', _('Phone number')),
|
|
||||||
)
|
|
||||||
|
|
||||||
id = models.BigAutoField(primary_key=True)
|
|
||||||
organizer = models.ForeignKey(Organizer, related_name='sso_clients', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
name = models.CharField(verbose_name=_("Application name"), max_length=255, blank=False)
|
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
|
||||||
|
|
||||||
client_id = models.CharField(
|
|
||||||
verbose_name=_("Client ID"),
|
|
||||||
max_length=100, unique=True, default=generate_client_id, db_index=True
|
|
||||||
)
|
|
||||||
client_secret = models.CharField(
|
|
||||||
max_length=255, blank=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
client_type = models.CharField(
|
|
||||||
max_length=32, choices=CLIENT_TYPES, verbose_name=_("Client type"), default=CLIENT_CONFIDENTIAL,
|
|
||||||
)
|
|
||||||
authorization_grant_type = models.CharField(
|
|
||||||
max_length=32, choices=GRANT_TYPES, verbose_name=_("Grant type"), default=GRANT_AUTHORIZATION_CODE,
|
|
||||||
)
|
|
||||||
redirect_uris = models.TextField(
|
|
||||||
blank=False,
|
|
||||||
verbose_name=_("Redirection URIs"),
|
|
||||||
help_text=_("Allowed URIs list, space separated")
|
|
||||||
)
|
|
||||||
allowed_scopes = MultiStringField(
|
|
||||||
default=['openid', 'profile', 'email', 'phone'],
|
|
||||||
delimiter=" ",
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_('Allowed access scopes'),
|
|
||||||
help_text=_('Separate multiple values with spaces'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_usable(self):
|
|
||||||
return self.is_active
|
|
||||||
|
|
||||||
def allow_redirect_uri(self, redirect_uri):
|
|
||||||
return self.redirect_uris and any(r.strip() == redirect_uri for r in self.redirect_uris.split(' '))
|
|
||||||
|
|
||||||
def allow_delete(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def evaluated_scope(self, scope):
|
|
||||||
scope = set(scope.split(' '))
|
|
||||||
allowed_scopes = set(self.allowed_scopes)
|
|
||||||
return ' '.join(scope & allowed_scopes)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
redirect_uris = self.redirect_uris.strip().split()
|
|
||||||
|
|
||||||
if redirect_uris:
|
|
||||||
validator = URLValidator()
|
|
||||||
for uri in redirect_uris:
|
|
||||||
validator(uri)
|
|
||||||
|
|
||||||
def set_client_secret(self):
|
|
||||||
secret = get_random_string(64)
|
|
||||||
self.client_secret = make_password(secret)
|
|
||||||
return secret
|
|
||||||
|
|
||||||
def check_client_secret(self, raw_secret):
|
|
||||||
"""
|
|
||||||
Return a boolean of whether the ra_secret was correct. Handles
|
|
||||||
hashing formats behind the scenes.
|
|
||||||
"""
|
|
||||||
def setter(raw_secret):
|
|
||||||
self.client_secret = make_password(raw_secret)
|
|
||||||
self.save(update_fields=["client_secret"])
|
|
||||||
return check_password(raw_secret, self.client_secret, setter)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerSSOGrant(models.Model):
|
|
||||||
id = models.BigAutoField(primary_key=True)
|
|
||||||
client = models.ForeignKey(
|
|
||||||
CustomerSSOClient, on_delete=models.CASCADE, related_name="grants"
|
|
||||||
)
|
|
||||||
customer = models.ForeignKey(
|
|
||||||
Customer, on_delete=models.CASCADE, related_name="sso_grants"
|
|
||||||
)
|
|
||||||
code = models.CharField(max_length=255, unique=True)
|
|
||||||
nonce = models.CharField(max_length=255, null=True, blank=True)
|
|
||||||
auth_time = models.IntegerField()
|
|
||||||
expires = models.DateTimeField()
|
|
||||||
redirect_uri = models.TextField()
|
|
||||||
scope = models.TextField(blank=True)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerSSOAccessToken(models.Model):
|
|
||||||
id = models.BigAutoField(primary_key=True)
|
|
||||||
client = models.ForeignKey(
|
|
||||||
CustomerSSOClient, on_delete=models.CASCADE, related_name="access_tokens"
|
|
||||||
)
|
|
||||||
customer = models.ForeignKey(
|
|
||||||
Customer, on_delete=models.CASCADE, related_name="sso_access_tokens"
|
|
||||||
)
|
|
||||||
from_code = models.CharField(max_length=255, null=True, blank=True)
|
|
||||||
token = models.CharField(max_length=255, unique=True)
|
|
||||||
expires = models.DateTimeField()
|
|
||||||
scope = models.TextField(blank=True)
|
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ class Discount(LoggedModel):
|
|||||||
candidates = []
|
candidates = []
|
||||||
cardinality = None
|
cardinality = None
|
||||||
for se, l in subevent_to_idx.items():
|
for se, l in subevent_to_idx.items():
|
||||||
l = [ll for ll in l if ll in initial_candidates and ll not in current_group]
|
l = [ll for ll in l if ll not in current_group]
|
||||||
if cardinality and len(l) != cardinality:
|
if cardinality and len(l) != cardinality:
|
||||||
continue
|
continue
|
||||||
if se not in {positions[idx][1] for idx in current_group}:
|
if se not in {positions[idx][1] for idx in current_group}:
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ from pretix.base.reldate import RelativeDateWrapper
|
|||||||
from pretix.base.validators import EventSlugBanlistValidator
|
from pretix.base.validators import EventSlugBanlistValidator
|
||||||
from pretix.helpers.database import GroupConcat
|
from pretix.helpers.database import GroupConcat
|
||||||
from pretix.helpers.daterange import daterange
|
from pretix.helpers.daterange import daterange
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
|
||||||
from pretix.helpers.json import safe_string
|
from pretix.helpers.json import safe_string
|
||||||
from pretix.helpers.thumb import get_thumbnail
|
from pretix.helpers.thumb import get_thumbnail
|
||||||
|
|
||||||
@@ -123,16 +122,6 @@ class EventMixin:
|
|||||||
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
|
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_weekday_from_display(self, tz=None, short=False) -> str:
|
|
||||||
"""
|
|
||||||
Returns a formatted string containing the weekday of the start date of the event with respect
|
|
||||||
to the current locale.
|
|
||||||
"""
|
|
||||||
tz = tz or self.timezone
|
|
||||||
return _date(
|
|
||||||
self.date_from.astimezone(tz), ("D" if short else "l")
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_time_from_display(self, tz=None) -> str:
|
def get_time_from_display(self, tz=None) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a formatted string containing the start time of the event, ignoring
|
Returns a formatted string containing the start time of the event, ignoring
|
||||||
@@ -157,18 +146,6 @@ class EventMixin:
|
|||||||
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
|
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_weekday_to_display(self, tz=None, short=False) -> str:
|
|
||||||
"""
|
|
||||||
Returns a formatted string containing the weekday of the end date of the event with respect
|
|
||||||
to the current locale.
|
|
||||||
"""
|
|
||||||
tz = tz or self.timezone
|
|
||||||
if not self.settings.show_date_to or not self.date_to:
|
|
||||||
return ""
|
|
||||||
return _date(
|
|
||||||
self.date_to.astimezone(tz), ("D" if short else "l")
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False) -> str:
|
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a formatted string containing the start date and the end date
|
Returns a formatted string containing the start date and the end date
|
||||||
@@ -940,13 +917,11 @@ class Event(EventMixin, LoggedModel):
|
|||||||
s.object = self
|
s.object = self
|
||||||
s.pk = None
|
s.pk = None
|
||||||
if s.value.startswith('file://'):
|
if s.value.startswith('file://'):
|
||||||
fi = default_storage.open(s.value[len('file://'):], 'rb')
|
fi = default_storage.open(s.value[7:], 'rb')
|
||||||
nonce = get_random_string(length=8)
|
nonce = get_random_string(length=8)
|
||||||
fname_base = clean_filename(os.path.basename(s.value))
|
|
||||||
|
|
||||||
# TODO: make sure pub is always correct
|
# TODO: make sure pub is always correct
|
||||||
fname = 'pub/%s/%s/%s.%s.%s' % (
|
fname = 'pub/%s/%s/%s.%s.%s' % (
|
||||||
self.organizer.slug, self.slug, fname_base, nonce, s.value.split('.')[-1]
|
self.organizer.slug, self.slug, s.key, nonce, s.value.split('.')[-1]
|
||||||
)
|
)
|
||||||
newname = default_storage.save(fname, fi)
|
newname = default_storage.save(fname, fi)
|
||||||
s.value = 'file://' + newname
|
s.value = 'file://' + newname
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ class MultiStringField(TextField):
|
|||||||
'delimiter_found': _('No value can contain the delimiter character.')
|
'delimiter_found': _('No value can contain the delimiter character.')
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, verbose_name=None, name=None, delimiter=DELIMITER, **kwargs):
|
def __init__(self, verbose_name=None, name=None, **kwargs):
|
||||||
self.delimiter = delimiter
|
|
||||||
super().__init__(verbose_name, name, **kwargs)
|
super().__init__(verbose_name, name, **kwargs)
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
@@ -45,13 +44,13 @@ class MultiStringField(TextField):
|
|||||||
if isinstance(value, (list, tuple)):
|
if isinstance(value, (list, tuple)):
|
||||||
return value
|
return value
|
||||||
elif value:
|
elif value:
|
||||||
return [v for v in value.split(self.delimiter) if v]
|
return [v for v in value.split(DELIMITER) if v]
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_prep_value(self, value):
|
def get_prep_value(self, value):
|
||||||
if isinstance(value, (list, tuple)):
|
if isinstance(value, (list, tuple)):
|
||||||
return self.delimiter + self.delimiter.join(value) + self.delimiter
|
return DELIMITER + DELIMITER.join(value) + DELIMITER
|
||||||
elif value is None:
|
elif value is None:
|
||||||
if self.null:
|
if self.null:
|
||||||
return None
|
return None
|
||||||
@@ -64,14 +63,14 @@ class MultiStringField(TextField):
|
|||||||
|
|
||||||
def from_db_value(self, value, expression, connection):
|
def from_db_value(self, value, expression, connection):
|
||||||
if value:
|
if value:
|
||||||
return [v for v in value.split(self.delimiter) if v]
|
return [v for v in value.split(DELIMITER) if v]
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def validate(self, value, model_instance):
|
def validate(self, value, model_instance):
|
||||||
super().validate(value, model_instance)
|
super().validate(value, model_instance)
|
||||||
for l in value:
|
for l in value:
|
||||||
if self.delimiter in l:
|
if DELIMITER in l:
|
||||||
raise exceptions.ValidationError(
|
raise exceptions.ValidationError(
|
||||||
self.error_messages['delimiter_found'],
|
self.error_messages['delimiter_found'],
|
||||||
code='delimiter_found',
|
code='delimiter_found',
|
||||||
@@ -79,9 +78,9 @@ class MultiStringField(TextField):
|
|||||||
|
|
||||||
def get_lookup(self, lookup_name):
|
def get_lookup(self, lookup_name):
|
||||||
if lookup_name == 'contains':
|
if lookup_name == 'contains':
|
||||||
return make_multistring_contains_lookup(self.delimiter)
|
return MultiStringContains
|
||||||
elif lookup_name == 'icontains':
|
elif lookup_name == 'icontains':
|
||||||
return make_multistring_icontains_lookup(self.delimiter)
|
return MultiStringIContains
|
||||||
elif lookup_name == 'isnull':
|
elif lookup_name == 'isnull':
|
||||||
return builtin_lookups.IsNull
|
return builtin_lookups.IsNull
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
@@ -89,22 +88,18 @@ class MultiStringField(TextField):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_multistring_contains_lookup(delimiter):
|
class MultiStringContains(builtin_lookups.Contains):
|
||||||
class Cls(builtin_lookups.Contains):
|
def process_rhs(self, qn, connection):
|
||||||
def process_rhs(self, qn, connection):
|
sql, params = super().process_rhs(qn, connection)
|
||||||
sql, params = super().process_rhs(qn, connection)
|
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
|
||||||
params[0] = "%" + delimiter + params[0][1:-1] + delimiter + "%"
|
return sql, params
|
||||||
return sql, params
|
|
||||||
return Cls
|
|
||||||
|
|
||||||
|
|
||||||
def make_multistring_icontains_lookup(delimiter):
|
class MultiStringIContains(builtin_lookups.IContains):
|
||||||
class Cls(builtin_lookups.IContains):
|
def process_rhs(self, qn, connection):
|
||||||
def process_rhs(self, qn, connection):
|
sql, params = super().process_rhs(qn, connection)
|
||||||
sql, params = super().process_rhs(qn, connection)
|
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
|
||||||
params[0] = "%" + delimiter + params[0][1:-1] + delimiter + "%"
|
return sql, params
|
||||||
return sql, params
|
|
||||||
return Cls
|
|
||||||
|
|
||||||
|
|
||||||
class MultiStringSerializer(serializers.Field):
|
class MultiStringSerializer(serializers.Field):
|
||||||
|
|||||||
@@ -600,14 +600,10 @@ class Item(LoggedModel):
|
|||||||
invoice_address=invoice_address,
|
invoice_address=invoice_address,
|
||||||
base_price_is='gross',
|
base_price_is='gross',
|
||||||
currency=currency)
|
currency=currency)
|
||||||
if not self.tax_rule:
|
compare_price = self.tax_rule.tax(b.designated_price * b.count,
|
||||||
compare_price = TaxedPrice(gross=b.designated_price * b.count, net=b.designated_price * b.count,
|
override_tax_rate=override_tax_rate,
|
||||||
tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
invoice_address=invoice_address,
|
||||||
else:
|
currency=currency)
|
||||||
compare_price = self.tax_rule.tax(b.designated_price * b.count,
|
|
||||||
override_tax_rate=override_tax_rate,
|
|
||||||
invoice_address=invoice_address,
|
|
||||||
currency=currency)
|
|
||||||
t.net += bprice.net - compare_price.net
|
t.net += bprice.net - compare_price.net
|
||||||
t.tax += bprice.tax - compare_price.tax
|
t.tax += bprice.tax - compare_price.tax
|
||||||
t.name = "MIXED!"
|
t.name = "MIXED!"
|
||||||
@@ -1329,7 +1325,6 @@ class Question(LoggedModel):
|
|||||||
verbose_name = _("Question")
|
verbose_name = _("Question")
|
||||||
verbose_name_plural = _("Questions")
|
verbose_name_plural = _("Questions")
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
unique_together = (('event', 'identifier'),)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.question)
|
return str(self.question)
|
||||||
@@ -1345,7 +1340,7 @@ class Question(LoggedModel):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _clean_identifier(event, code, instance=None):
|
def _clean_identifier(event, code, instance=None):
|
||||||
qs = Question.objects.filter(event=event, identifier__iexact=code)
|
qs = Question.objects.filter(event=event, identifier__iexact=code)
|
||||||
if instance and instance.pk:
|
if instance:
|
||||||
qs = qs.exclude(pk=instance.pk)
|
qs = qs.exclude(pk=instance.pk)
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
raise ValidationError(_('This identifier is already used for a different question.'))
|
raise ValidationError(_('This identifier is already used for a different question.'))
|
||||||
|
|||||||
@@ -268,10 +268,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if 'require_approval' not in self.get_deferred_fields() and 'status' not in self.get_deferred_fields():
|
if 'require_approval' not in self.get_deferred_fields() and 'status' not in self.get_deferred_fields():
|
||||||
self._transaction_key_reset()
|
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
|
||||||
|
|
||||||
def _transaction_key_reset(self):
|
|
||||||
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
|
|
||||||
|
|
||||||
def gracefully_delete(self, user=None, auth=None):
|
def gracefully_delete(self, user=None, auth=None):
|
||||||
from . import GiftCard, GiftCardTransaction, Membership, Voucher
|
from . import GiftCard, GiftCardTransaction, Membership, Voucher
|
||||||
@@ -1059,14 +1056,12 @@ class Order(LockModel, LoggedModel):
|
|||||||
if p.canceled and not _backfill_before_cancellation:
|
if p.canceled and not _backfill_before_cancellation:
|
||||||
continue
|
continue
|
||||||
target_transaction_count[Transaction.key(p)] += 1
|
target_transaction_count[Transaction.key(p)] += 1
|
||||||
p._transaction_key_reset()
|
|
||||||
|
|
||||||
fees = self.fees.all() if fees is None else fees
|
fees = self.fees.all() if fees is None else fees
|
||||||
for f in fees:
|
for f in fees:
|
||||||
if f.canceled and not _backfill_before_cancellation:
|
if f.canceled and not _backfill_before_cancellation:
|
||||||
continue
|
continue
|
||||||
target_transaction_count[Transaction.key(f)] += 1
|
target_transaction_count[Transaction.key(f)] += 1
|
||||||
f._transaction_key_reset()
|
|
||||||
|
|
||||||
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
|
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
|
||||||
create = []
|
create = []
|
||||||
@@ -1093,7 +1088,6 @@ class Order(LockModel, LoggedModel):
|
|||||||
create.sort(key=lambda t: (0 if t.count < 0 else 1, t.positionid or 0))
|
create.sort(key=lambda t: (0 if t.count < 0 else 1, t.positionid or 0))
|
||||||
if save:
|
if save:
|
||||||
Transaction.objects.bulk_create(create)
|
Transaction.objects.bulk_create(create)
|
||||||
self._transaction_key_reset()
|
|
||||||
_transactions_mark_order_clean(self.pk)
|
_transactions_mark_order_clean(self.pk)
|
||||||
return create
|
return create
|
||||||
|
|
||||||
@@ -2082,20 +2076,8 @@ class OrderFee(models.Model):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not self.get_deferred_fields():
|
if not self.get_deferred_fields():
|
||||||
self._transaction_key_reset()
|
self.__initial_transaction_key = Transaction.key(self)
|
||||||
|
self.__initial_canceled = self.canceled
|
||||||
def refresh_from_db(self, using=None, fields=None):
|
|
||||||
"""
|
|
||||||
Reload field values from the database. Similar to django's implementation
|
|
||||||
with adjustment for our method that forces us to create ``Transaction`` instances.
|
|
||||||
"""
|
|
||||||
if not self.get_deferred_fields():
|
|
||||||
self._transaction_key_reset()
|
|
||||||
return super().refresh_from_db(using, fields)
|
|
||||||
|
|
||||||
def _transaction_key_reset(self):
|
|
||||||
self.__initial_transaction_key = Transaction.key(self)
|
|
||||||
self.__initial_canceled = self.canceled
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.description:
|
if self.description:
|
||||||
@@ -2215,20 +2197,8 @@ class OrderPosition(AbstractPosition):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not self.get_deferred_fields():
|
if not self.get_deferred_fields():
|
||||||
self._transaction_key_reset()
|
self.__initial_transaction_key = Transaction.key(self)
|
||||||
|
self.__initial_canceled = self.canceled
|
||||||
def refresh_from_db(self, using=None, fields=None):
|
|
||||||
"""
|
|
||||||
Reload field values from the database. Similar to django's implementation
|
|
||||||
with adjustment for our method that forces us to create ``Transaction`` instances.
|
|
||||||
"""
|
|
||||||
if not self.get_deferred_fields():
|
|
||||||
self._transaction_key_reset()
|
|
||||||
return super().refresh_from_db(using, fields)
|
|
||||||
|
|
||||||
def _transaction_key_reset(self):
|
|
||||||
self.__initial_transaction_key = Transaction.key(self)
|
|
||||||
self.__initial_canceled = self.canceled
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Order position")
|
verbose_name = _("Order position")
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ EU_CURRENCIES = {
|
|||||||
'RO': 'RON',
|
'RO': 'RON',
|
||||||
'SE': 'SEK'
|
'SE': 'SEK'
|
||||||
}
|
}
|
||||||
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH', 'NO'}
|
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH'}
|
||||||
|
|
||||||
|
|
||||||
def is_eu_country(cc):
|
def is_eu_country(cc):
|
||||||
|
|||||||
@@ -251,11 +251,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"editor_sample": _("20:00"),
|
"editor_sample": _("20:00"),
|
||||||
"evaluate": lambda op, order, ev: ev.get_time_from_display()
|
"evaluate": lambda op, order, ev: ev.get_time_from_display()
|
||||||
}),
|
}),
|
||||||
("event_begin_weekday", {
|
|
||||||
"label": _("Event begin weekday"),
|
|
||||||
"editor_sample": _("Friday"),
|
|
||||||
"evaluate": lambda op, order, ev: ev.get_weekday_from_display()
|
|
||||||
}),
|
|
||||||
("event_end", {
|
("event_end", {
|
||||||
"label": _("Event end date and time"),
|
"label": _("Event end date and time"),
|
||||||
"editor_sample": _("2017-05-31 22:00"),
|
"editor_sample": _("2017-05-31 22:00"),
|
||||||
@@ -280,11 +275,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"TIME_FORMAT"
|
"TIME_FORMAT"
|
||||||
) if ev.date_to else ""
|
) if ev.date_to else ""
|
||||||
}),
|
}),
|
||||||
("event_end_weekday", {
|
|
||||||
"label": _("Event end weekday"),
|
|
||||||
"editor_sample": _("Friday"),
|
|
||||||
"evaluate": lambda op, order, ev: ev.get_weekday_to_display()
|
|
||||||
}),
|
|
||||||
("event_admission", {
|
("event_admission", {
|
||||||
"label": _("Event admission date and time"),
|
"label": _("Event admission date and time"),
|
||||||
"editor_sample": _("2017-05-31 19:00"),
|
"editor_sample": _("2017-05-31 19:00"),
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from django.utils.timezone import now
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import CachedCombinedTicket, CachedTicket
|
from pretix.base.models import CachedCombinedTicket, CachedTicket
|
||||||
from pretix.base.models.customers import CustomerSSOGrant
|
|
||||||
|
|
||||||
from ..models import CachedFile, CartPosition, InvoiceAddress
|
from ..models import CachedFile, CartPosition, InvoiceAddress
|
||||||
from ..signals import periodic_task
|
from ..signals import periodic_task
|
||||||
@@ -69,9 +68,3 @@ def clean_cached_tickets(sender, **kwargs):
|
|||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def clearsessions(sender, **kwargs):
|
def clearsessions(sender, **kwargs):
|
||||||
call_command('clearsessions')
|
call_command('clearsessions')
|
||||||
|
|
||||||
|
|
||||||
@receiver(signal=periodic_task)
|
|
||||||
@scopes_disabled()
|
|
||||||
def clear_oidc_data(sender, **kwargs):
|
|
||||||
CustomerSSOGrant.objects.filter(expires__lt=now() - timedelta(days=14)).delete()
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
:param order: The order this email is related to (optional). If set, this will be used to include a link to the
|
:param order: The order this email is related to (optional). If set, this will be used to include a link to the
|
||||||
order below the email.
|
order below the email.
|
||||||
|
|
||||||
:param position: The order position this email is related to (optional). If set, this will be used to include a link
|
:param order: The order position this email is related to (optional). If set, this will be used to include a link
|
||||||
to the order position instead of the order below the email.
|
to the order position instead of the order below the email.
|
||||||
|
|
||||||
:param headers: A dict of custom mail headers to add to the mail
|
:param headers: A dict of custom mail headers to add to the mail
|
||||||
@@ -141,7 +141,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
|
|
||||||
:param user: The user this email is sent to
|
:param user: The user this email is sent to
|
||||||
|
|
||||||
:param customer: The customer this email is sent to
|
:param customer: The user this email is sent to
|
||||||
|
|
||||||
:param attach_cached_files: A list of cached file to attach to this email.
|
:param attach_cached_files: A list of cached file to attach to this email.
|
||||||
|
|
||||||
@@ -502,11 +502,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
rc.expire(redis_key, 300)
|
rc.expire(redis_key, 300)
|
||||||
|
|
||||||
max_retries = 10
|
max_retries = 10
|
||||||
retry_after = min(30 + cnt * 10, 1800)
|
retry_after = 30 + cnt * 10
|
||||||
else:
|
else:
|
||||||
# Most likely some other kind of temporary failure, retry again (but pretty soon)
|
# Most likely some other kind of temporary failure, retry again (but pretty soon)
|
||||||
max_retries = 5
|
max_retries = 5
|
||||||
retry_after = [10, 30, 60, 300, 900, 900][self.request.retries]
|
retry_after = 2 ** (self.request.retries * 3) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.retry(max_retries=max_retries, countdown=retry_after)
|
self.retry(max_retries=max_retries, countdown=retry_after)
|
||||||
@@ -542,7 +542,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
if not any(c >= 500 for c in smtp_codes):
|
if not any(c >= 500 for c in smtp_codes):
|
||||||
# Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals
|
# Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals
|
||||||
try:
|
try:
|
||||||
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800][self.request.retries])
|
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3) * 4) # max is 2 ** (4*3) * 4 = 16384 seconds = approx 4.5 hours
|
||||||
except MaxRetriesExceededError:
|
except MaxRetriesExceededError:
|
||||||
# ignore and go on with logging the error
|
# ignore and go on with logging the error
|
||||||
pass
|
pass
|
||||||
@@ -567,7 +567,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)):
|
if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)):
|
||||||
try:
|
try:
|
||||||
self.retry(max_retries=5, countdown=[10, 30, 60, 300, 900, 900][self.request.retries])
|
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||||
except MaxRetriesExceededError:
|
except MaxRetriesExceededError:
|
||||||
if log_target:
|
if log_target:
|
||||||
log_target.log_action(
|
log_target.log_action(
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ from pretix.base.models import (
|
|||||||
from pretix.base.models.orders import Transaction
|
from pretix.base.models.orders import Transaction
|
||||||
from pretix.base.orderimport import get_all_columns
|
from pretix.base.orderimport import get_all_columns
|
||||||
from pretix.base.services.invoices import generate_invoice, invoice_qualified
|
from pretix.base.services.invoices import generate_invoice, invoice_qualified
|
||||||
from pretix.base.services.locking import NoLockManager
|
|
||||||
from pretix.base.services.tasks import ProfiledEventTask
|
from pretix.base.services.tasks import ProfiledEventTask
|
||||||
from pretix.base.signals import order_paid, order_placed
|
from pretix.base.signals import order_paid, order_placed
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
@@ -86,9 +85,9 @@ def setif(record, obj, attr, setting):
|
|||||||
|
|
||||||
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
|
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
|
||||||
def import_orders(event: Event, fileid: str, settings: dict, locale: str, user) -> None:
|
def import_orders(event: Event, fileid: str, settings: dict, locale: str, user) -> None:
|
||||||
|
# TODO: quotacheck?
|
||||||
cf = CachedFile.objects.get(id=fileid)
|
cf = CachedFile.objects.get(id=fileid)
|
||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
seats_used = False
|
|
||||||
with language(locale, event.settings.region):
|
with language(locale, event.settings.region):
|
||||||
cols = get_all_columns(event)
|
cols = get_all_columns(event)
|
||||||
parsed = parse_csv(cf.file)
|
parsed = parse_csv(cf.file)
|
||||||
@@ -134,8 +133,6 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
|
|||||||
position = OrderPosition(positionid=len(order._positions) + 1)
|
position = OrderPosition(positionid=len(order._positions) + 1)
|
||||||
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
||||||
position.meta_info = {}
|
position.meta_info = {}
|
||||||
if position.seat is not None:
|
|
||||||
seats_used = True
|
|
||||||
order._positions.append(position)
|
order._positions.append(position)
|
||||||
position.assign_pseudonymization_id()
|
position.assign_pseudonymization_id()
|
||||||
|
|
||||||
@@ -147,12 +144,9 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
|
|||||||
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||||
)
|
)
|
||||||
|
|
||||||
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats
|
# quota check?
|
||||||
# are in use
|
with event.lock():
|
||||||
lockfn = event.lock if seats_used else NoLockManager
|
with transaction.atomic():
|
||||||
|
|
||||||
try:
|
|
||||||
with lockfn(), transaction.atomic():
|
|
||||||
save_transactions = []
|
save_transactions = []
|
||||||
for o in orders:
|
for o in orders:
|
||||||
o.total = sum([c.price for c in o._positions]) # currently no support for fees
|
o.total = sum([c.price for c in o._positions]) # currently no support for fees
|
||||||
@@ -210,7 +204,4 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
|
|||||||
) and not o.invoices.last()
|
) and not o.invoices.last()
|
||||||
if gen_invoice:
|
if gen_invoice:
|
||||||
generate_invoice(o, trigger_pdf=True)
|
generate_invoice(o, trigger_pdf=True)
|
||||||
except DataImportError:
|
|
||||||
raise ValidationError(_('We were not able to process your request completely as the server was too busy. '
|
|
||||||
'Please try again.'))
|
|
||||||
cf.delete()
|
cf.delete()
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from xml.etree import ElementTree
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
import requests
|
import vat_moss.errors
|
||||||
import vat_moss.id
|
import vat_moss.id
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -35,16 +35,6 @@ from zeep.exceptions import Fault
|
|||||||
from pretix.base.models.tax import cc_to_vat_prefix, is_eu_country
|
from pretix.base.models.tax import cc_to_vat_prefix, is_eu_country
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
error_messages = {
|
|
||||||
'unavailable': _(
|
|
||||||
'Your VAT ID could not be checked, as the VAT checking service of '
|
|
||||||
'your country is currently not available. We will therefore '
|
|
||||||
'need to charge VAT on your invoice. You can get the tax amount '
|
|
||||||
'back via the VAT reimbursement process.'
|
|
||||||
),
|
|
||||||
'invalid': _('This VAT ID is not valid. Please re-check your input.'),
|
|
||||||
'country_mismatch': _('Your VAT ID does not match the selected country.'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class VATIDError(Exception):
|
class VATIDError(Exception):
|
||||||
@@ -60,107 +50,33 @@ class VATIDTemporaryError(VATIDError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _validate_vat_id_NO(vat_id, country_code):
|
|
||||||
# Inspired by vat_moss library
|
|
||||||
vat_id = vat_moss.id.normalize(vat_id)
|
|
||||||
|
|
||||||
if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]):
|
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
|
||||||
|
|
||||||
organization_number = vat_id[2:].replace('MVA', '')
|
|
||||||
validation_url = 'https://data.brreg.no/enhetsregisteret/api/enheter/%s' % organization_number
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(validation_url, timeout=10)
|
|
||||||
if response.status_code in (404, 400):
|
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
|
||||||
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
info = response.json()
|
|
||||||
# This should never happen, but keeping it incase the API is changed
|
|
||||||
if 'organisasjonsnummer' not in info or info['organisasjonsnummer'] != organization_number:
|
|
||||||
logger.warning(
|
|
||||||
'VAT ID checking failed for Norway due to missing or mismatching organisasjonsnummer in repsonse'
|
|
||||||
)
|
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
|
||||||
except requests.RequestException:
|
|
||||||
logger.exception('VAT ID checking failed for country {}'.format(country_code))
|
|
||||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
|
||||||
else:
|
|
||||||
return vat_id
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_vat_id_EU(vat_id, country_code):
|
def _validate_vat_id_EU(vat_id, country_code):
|
||||||
# Inspired by vat_moss library
|
|
||||||
try:
|
|
||||||
vat_id = vat_moss.id.normalize(vat_id)
|
|
||||||
except ValueError:
|
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
|
||||||
|
|
||||||
if not vat_id or len(vat_id) < 3:
|
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
|
||||||
|
|
||||||
number = vat_id[2:]
|
|
||||||
|
|
||||||
if vat_id[:2] != cc_to_vat_prefix(country_code):
|
if vat_id[:2] != cc_to_vat_prefix(country_code):
|
||||||
raise VATIDFinalError(error_messages['country_mismatch'])
|
raise VATIDFinalError(_('Your VAT ID does not match the selected country.'))
|
||||||
|
|
||||||
if not re.match(vat_moss.id.ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], number):
|
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
|
||||||
|
|
||||||
# We are relying on the country code of the normalized VAT-ID and not the user/InvoiceAddress-provided
|
|
||||||
# VAT-ID, since Django and the EU have different ideas of which country is using which country code.
|
|
||||||
# For example: For django and most people, Greece is GR. However, the VAT-service expects EL.
|
|
||||||
payload = """
|
|
||||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
|
|
||||||
<soapenv:Header/>
|
|
||||||
<soapenv:Body>
|
|
||||||
<urn:checkVat>
|
|
||||||
<urn:countryCode>%s</urn:countryCode>
|
|
||||||
<urn:vatNumber>%s</urn:vatNumber>
|
|
||||||
</urn:checkVat>
|
|
||||||
</soapenv:Body>
|
|
||||||
</soapenv:Envelope>
|
|
||||||
""".strip() % (vat_id[:2], number)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
result = vat_moss.id.validate(vat_id)
|
||||||
'https://ec.europa.eu/taxation_customs/vies/services/checkVatService',
|
if result:
|
||||||
data=payload,
|
country_code, normalized_id, company_name = result
|
||||||
timeout=10,
|
return normalized_id
|
||||||
)
|
except (vat_moss.errors.InvalidError, ValueError):
|
||||||
response.raise_for_status()
|
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
|
||||||
|
except vat_moss.errors.WebServiceUnavailableError:
|
||||||
return_xml = response.text
|
|
||||||
|
|
||||||
try:
|
|
||||||
envelope = ElementTree.fromstring(return_xml)
|
|
||||||
except ElementTree.ParseError:
|
|
||||||
logger.error(
|
|
||||||
f'VAT ID checking failed for {country_code} due to XML parse error'
|
|
||||||
)
|
|
||||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
|
||||||
|
|
||||||
namespaces = {
|
|
||||||
'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
|
|
||||||
'vat': 'urn:ec.europa.eu:taxud:vies:services:checkVat:types'
|
|
||||||
}
|
|
||||||
valid_elements = envelope.findall('./soap:Body/vat:checkVatResponse/vat:valid', namespaces)
|
|
||||||
if not valid_elements:
|
|
||||||
logger.error(
|
|
||||||
f'VAT ID checking failed for {country_code} due to missing <valid> tag'
|
|
||||||
)
|
|
||||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
|
||||||
|
|
||||||
if valid_elements[0].text.lower() != 'true':
|
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
|
||||||
|
|
||||||
except requests.RequestException:
|
|
||||||
logger.exception('VAT ID checking failed for country {}'.format(country_code))
|
logger.exception('VAT ID checking failed for country {}'.format(country_code))
|
||||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
raise VATIDTemporaryError(_(
|
||||||
else:
|
'Your VAT ID could not be checked, as the VAT checking service of '
|
||||||
return vat_id
|
'your country is currently not available. We will therefore '
|
||||||
|
'need to charge VAT on your invoice. You can get the tax amount '
|
||||||
|
'back via the VAT reimbursement process.'
|
||||||
|
))
|
||||||
|
except (vat_moss.errors.WebServiceError, HTTPError):
|
||||||
|
logger.exception('VAT ID checking failed for country {}'.format(country_code))
|
||||||
|
raise VATIDTemporaryError(_(
|
||||||
|
'Your VAT ID could not be checked, as the VAT checking service of '
|
||||||
|
'your country returned an incorrect result. We will therefore '
|
||||||
|
'need to charge VAT on your invoice. Please contact support to '
|
||||||
|
'resolve this manually.'
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def _validate_vat_id_CH(vat_id, country_code):
|
def _validate_vat_id_CH(vat_id, country_code):
|
||||||
@@ -169,13 +85,10 @@ def _validate_vat_id_CH(vat_id, country_code):
|
|||||||
|
|
||||||
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
|
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
|
||||||
try:
|
try:
|
||||||
transport = Transport(
|
transport = Transport(cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")))
|
||||||
cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")),
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
client = Client(
|
client = Client(
|
||||||
'https://www.uid-wse.admin.ch/V5.0/PublicServices.svc?wsdl',
|
'https://www.uid-wse.admin.ch/V5.0/PublicServices.svc?wsdl',
|
||||||
transport=transport,
|
transport=transport
|
||||||
)
|
)
|
||||||
result = client.service.ValidateUID(uid=vat_id)
|
result = client.service.ValidateUID(uid=vat_id)
|
||||||
except Fault as e:
|
except Fault as e:
|
||||||
@@ -212,14 +125,10 @@ def _validate_vat_id_CH(vat_id, country_code):
|
|||||||
|
|
||||||
|
|
||||||
def validate_vat_id(vat_id, country_code):
|
def validate_vat_id(vat_id, country_code):
|
||||||
if not vat_id:
|
|
||||||
return vat_id
|
|
||||||
country_code = str(country_code)
|
country_code = str(country_code)
|
||||||
if is_eu_country(country_code):
|
if is_eu_country(country_code):
|
||||||
return _validate_vat_id_EU(vat_id, country_code)
|
return _validate_vat_id_EU(vat_id, country_code)
|
||||||
elif country_code == 'CH':
|
elif country_code == 'CH':
|
||||||
return _validate_vat_id_CH(vat_id, country_code)
|
return _validate_vat_id_CH(vat_id, country_code)
|
||||||
elif country_code == 'NO':
|
|
||||||
return _validate_vat_id_NO(vat_id, country_code)
|
|
||||||
|
|
||||||
raise VATIDTemporaryError(f'VAT ID should not be entered for country {country_code}')
|
raise VATIDTemporaryError(f'VAT ID should not be entered for country {country_code}')
|
||||||
|
|||||||
@@ -146,17 +146,6 @@ DEFAULTS = {
|
|||||||
"advanced features like memberships.")
|
"advanced features like memberships.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'customer_accounts_native': {
|
|
||||||
'default': 'True',
|
|
||||||
'type': bool,
|
|
||||||
'form_class': forms.BooleanField,
|
|
||||||
'serializer_class': serializers.BooleanField,
|
|
||||||
'form_kwargs': dict(
|
|
||||||
label=_("Allow customers to log in with email address and password"),
|
|
||||||
help_text=_("If disabled, you will need to connect one or more single-sign-on providers."),
|
|
||||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'customer_accounts_link_by_email': {
|
'customer_accounts_link_by_email': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="line-height: 0">
|
<td style="line-height: 0">
|
||||||
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAAAAACf95tlAAAAAXNCSVQI5gpbmQAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAG/SURBVHja7dvRboMwDIXhvf/DLiQQAwkku9+qDgq2hPyfN6j1qTlx06/uMunbLMnnhL98fuzRDtYILEeZ7GBNwAIWsIB1LdkOVgaWo4gdLAGWo6x2sFZgOUq1g1WB5SjNDlYDlqcEK1dDB5anmK3eE7C4FnIpBNbVFLo7sB7d3huwKFlULGA9pWQJsJxls4G1ActbooWr2IHlLbMFrBlY7rJbwNqBxb2QZ8nAuiUGO9ICLOo71R1YN0X9td8KLJ8ZeDEDrAd+Za3A4mLIz4TAujGqv+tUYPmN4v8LcweW3zS1t++hActzCrtRYD3pMJQOLOeJ7NyBpZFdoWaFDVjuJ6BRswpTBZbCAn5hpsDq/fbHpDMTBZbC1TAzT2ApyMIVsDROQ2GWwFJo8PR2YP3eOtywzwrsGYD1J9vlHXzcmSKw7q/wU2OEwHpdtALHILA00jJfV8DSaVofvYOPlckB658sp/8VNrBkANahqnXqfhhXJgasgymHD8REZwfWmezzga+tQdhcAet0qry1FYV3osD6dP1QJL3YbYUkhfUCsK6einWRPI0pxjROWZbK+QcsAiwCLEKARYBFgEXIu/wAYbjtwujw8KwAAAAASUVORK5CYII="
|
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAAAAACf95tlAAAAAXNCSVQI5gpbmQAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAG/SURBVHja7dvRboMwDIXhvf/DLiQQAwkku9+qDgq2hPyfN6j1qTlx06/uMunbLMnnhL98fuzRDtYILEeZ7GBNwAIWsIB1LdkOVgaWo4gdLAGWo6x2sFZgOUq1g1WB5SjNDlYDlqcEK1dDB5anmK3eE7C4FnIpBNbVFLo7sB7d3huwKFlULGA9pWQJsJxls4G1ActbooWr2IHlLbMFrBlY7rJbwNqBxb2QZ8nAuiUGO9ICLOo71R1YN0X9td8KLJ8ZeDEDrAd+Za3A4mLIz4TAujGqv+tUYPmN4v8LcweW3zS1t++hActzCrtRYD3pMJQOLOeJ7NyBpZFdoWaFDVjuJ6BRswpTBZbCAn5hpsDq/fbHpDMTBZbC1TAzT2ApyMIVsDROQ2GWwFJo8PR2YP3eOtywzwrsGYD1J9vlHXzcmSKw7q/wU2OEwHpdtALHILA00jJfV8DSaVofvYOPlckB658sp/8VNrBkANahqnXqfhhXJgasgymHD8REZwfWmezzga+tQdhcAet0qry1FYV3osD6dP1QJL3YbYUkhfUCsK6einWRPI0pxjROWZbK+QcsAiwCLEKARYBFgEXIu/wAYbjtwujw8KwAAAAASUVORK5CYII="
|
||||||
style="max-height: 60px;" alt="">
|
style="max-height: 60px;">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
<td style="line-height: 0">
|
<td style="line-height: 0">
|
||||||
<br>
|
<br>
|
||||||
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAYAAAC6nMS5AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAPnSURBVHic7d3dbuJIEAbQsg2Ecd7/TQeDf3svVuFmdjJLxsGm+xwJKXcpqS76U3VTVCmlFAAArKbeugAAgNwIWAAAKxOwAABWJmABAKxMwAIAWNlh6wIAIiKWZYllWWKe5/vfEREppfsnIqKqqvsnIqKu66jrOpqmuf8NsDUBC3i6lFJM0xTjOMY0TbEsS6y1MaaqqqjrOg6HQxyPxzgcDvcwBvAslT1YwDN8BKpxHGOe56f+76Zp4ng83gMXwHcTsIBvsyxLDMMQfd/fr/y2Vtd1nE6nOJ1O0TTN1uUAmRKwgNV9hKppmrYu5VOHwyHO53Mcj8etSwEyI2ABqxmGIW6329OvAP9WXddxPp/j7e1t61KATAhYwF/r+z5ut9turgG/StAC1iJgAV82z3N0Xbf7q8BHNU0Tbdt6EA98mYAFPCylFNfrNfq+37qUb3U6naJtW2segIcJWMBDhmGIrutW21u1d1VVRdu2cTqdti4FeCECFvC/lDK1+h3TLOARAhbwR/M8x+VyeblvB66taZp4f3+3Pwv4IwEL+NQ4jnG5XIq5EvyTqqri/f3d7izgUwIW8FvDMMTlctm6jF1q29Y6B+C3BCzgP91ut7her1uXsWvn8zl+/PixdRnADglYwC+6riv2Mfuj3t7eom3brcsAdqbeugBgX4Srx/R9H13XbV0GsDMCFnB3u92Eqy/4+KkggA8CFhAR/z5o9+bq60reEQb8SsAC7qsY+Dtd18U4jluXAeyAgAWFW5ZFuFqRhaxAhIAFxfv586cloitKKVnMCghYULKu60xbvsE8z96zQeEELCjUOI4eZX+jvu+9x4KCCVhQoI9rLL6Xq0Iol4AFBbperw7+J0gpuSqEQglYUJh5nl0NPlHf9zFN09ZlAE8mYEFh/KzL85liQXkELCiIaco2pmmKYRi2LgN4IgELCuL38rZjigVlEbCgEMMwxLIsW5dRrGVZTLGgIAIWFML0ant6AOUQsKAA4zja2L4D8zxbPgqFELCgACYn+6EXUAYBCzK3LItvDu7INE3ewkEBBCzInIfV+6MnkD8BCzLnMN8fPYH8CViQsXmePW7fIX2B/AlYkDGTkv3SG8ibgAUZ87h9v/QG8iZgQaZSSg7xHZumKVJKW5cBfBMBCzIlXO2fHkG+BCzIlMN7//QI8iVgQaYc3vunR5AvAQsyZQ3A/tnoDvkSsCBDKSUPqF/Asiz6BJkSsCBDplevQ68gTwIWZMjV0+vQK8iTgAUZMhV5HXoFeRKwIEPe9bwOvYI8CViQIYf269AryJOABQCwMgELMmQq8jr0CvIkYEGGHNqvQ68gT/8AETAn3pyLgvsAAAAASUVORK5CYII="
|
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAYAAAC6nMS5AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAPnSURBVHic7d3dbuJIEAbQsg2Ecd7/TQeDf3svVuFmdjJLxsGm+xwJKXcpqS76U3VTVCmlFAAArKbeugAAgNwIWAAAKxOwAABWJmABAKxMwAIAWNlh6wIAIiKWZYllWWKe5/vfEREppfsnIqKqqvsnIqKu66jrOpqmuf8NsDUBC3i6lFJM0xTjOMY0TbEsS6y1MaaqqqjrOg6HQxyPxzgcDvcwBvAslT1YwDN8BKpxHGOe56f+76Zp4ng83gMXwHcTsIBvsyxLDMMQfd/fr/y2Vtd1nE6nOJ1O0TTN1uUAmRKwgNV9hKppmrYu5VOHwyHO53Mcj8etSwEyI2ABqxmGIW6329OvAP9WXddxPp/j7e1t61KATAhYwF/r+z5ut9turgG/StAC1iJgAV82z3N0Xbf7q8BHNU0Tbdt6EA98mYAFPCylFNfrNfq+37qUb3U6naJtW2segIcJWMBDhmGIrutW21u1d1VVRdu2cTqdti4FeCECFvC/lDK1+h3TLOARAhbwR/M8x+VyeblvB66taZp4f3+3Pwv4IwEL+NQ4jnG5XIq5EvyTqqri/f3d7izgUwIW8FvDMMTlctm6jF1q29Y6B+C3BCzgP91ut7her1uXsWvn8zl+/PixdRnADglYwC+6riv2Mfuj3t7eom3brcsAdqbeugBgX4Srx/R9H13XbV0GsDMCFnB3u92Eqy/4+KkggA8CFhAR/z5o9+bq60reEQb8SsAC7qsY+Dtd18U4jluXAeyAgAWFW5ZFuFqRhaxAhIAFxfv586cloitKKVnMCghYULKu60xbvsE8z96zQeEELCjUOI4eZX+jvu+9x4KCCVhQoI9rLL6Xq0Iol4AFBbperw7+J0gpuSqEQglYUJh5nl0NPlHf9zFN09ZlAE8mYEFh/KzL85liQXkELCiIaco2pmmKYRi2LgN4IgELCuL38rZjigVlEbCgEMMwxLIsW5dRrGVZTLGgIAIWFML0ant6AOUQsKAA4zja2L4D8zxbPgqFELCgACYn+6EXUAYBCzK3LItvDu7INE3ewkEBBCzInIfV+6MnkD8BCzLnMN8fPYH8CViQsXmePW7fIX2B/AlYkDGTkv3SG8ibgAUZ87h9v/QG8iZgQaZSSg7xHZumKVJKW5cBfBMBCzIlXO2fHkG+BCzIlMN7//QI8iVgQaYc3vunR5AvAQsyZQ3A/tnoDvkSsCBDKSUPqF/Asiz6BJkSsCBDplevQ68gTwIWZMjV0+vQK8iTgAUZMhV5HXoFeRKwIEPe9bwOvYI8CViQIYf269AryJOABQCwMgELMmQq8jr0CvIkYEGGHNqvQ68gT/8AETAn3pyLgvsAAAAASUVORK5CYII="
|
||||||
style="max-height: 60px;" alt="">
|
style="max-height: 60px;">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div style="line-height: 18px;height: 18px;"> </div>
|
<div style="line-height: 18px;height: 18px;"> </div>
|
||||||
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAAEBAMAAACgm1xKAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV/TSkUrDu0g4pChOlkQleKoVShChVArtOpgcukXNGlIUlwcBdeCgx+LVQcXZ10dXAVB8APExdVJ0UVK/F9aaBHjwXE/3t173L0DhEaFaVZgAtB020wnE2I2tyoGXxFCAP0IIy4zy5iTpBQ8x9c9fHy9i/Es73N/jgE1bzHAJxLPMsO0iTeI45u2wXmfOMJKskp8Tjxu0gWJH7mutPiNc9FlgWdGzEx6njhCLBa7WOliVjI14mniqKrplC9kW6xy3uKsVWqsfU/+wlBeX1nmOs0RJLGIJUgQoaCGMiqwEaNVJ8VCmvYTHv5h1y+RSyFXGYwcC6hCg+z6wf/gd7dWYWqylRRKAD0vjvMxCgR3gWbdcb6PHad5AvifgSu94682gJlP0usdLXoEDG4DF9cdTdkDLneAoSdDNmVX8tMUCgXg/Yy+KQeEb4G+tVZv7X2cPgAZ6ip1AxwcAmNFyl73eHdvd2//nmn39wNhNnKgJpT5BQAAAC1QTFRF7u7u7+/v8PDw8fHx8vLy9PT09fX19/f3+Pj4+fn5+vr6/Pz8/f39/v7+////BLnnfgAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB+MMBAsUDD3bzUUAAABhSURBVDjLY2BAgLp3CNCAJO6HJH4ASZwXSfwxkjgnkvhzJHFWJPHXSOKMSOLvFJAk1iGJJyCJ5yGJL0AS10MSf4Akzo0k/hRJnB1J/CWSOAuS+FsG7GA0sEYDazSwBiSwAPzzGpfLqBMlAAAAAElFTkSuQmCC"
|
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAAEBAMAAACgm1xKAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV/TSkUrDu0g4pChOlkQleKoVShChVArtOpgcukXNGlIUlwcBdeCgx+LVQcXZ10dXAVB8APExdVJ0UVK/F9aaBHjwXE/3t173L0DhEaFaVZgAtB020wnE2I2tyoGXxFCAP0IIy4zy5iTpBQ8x9c9fHy9i/Es73N/jgE1bzHAJxLPMsO0iTeI45u2wXmfOMJKskp8Tjxu0gWJH7mutPiNc9FlgWdGzEx6njhCLBa7WOliVjI14mniqKrplC9kW6xy3uKsVWqsfU/+wlBeX1nmOs0RJLGIJUgQoaCGMiqwEaNVJ8VCmvYTHv5h1y+RSyFXGYwcC6hCg+z6wf/gd7dWYWqylRRKAD0vjvMxCgR3gWbdcb6PHad5AvifgSu94682gJlP0usdLXoEDG4DF9cdTdkDLneAoSdDNmVX8tMUCgXg/Yy+KQeEb4G+tVZv7X2cPgAZ6ip1AxwcAmNFyl73eHdvd2//nmn39wNhNnKgJpT5BQAAAC1QTFRF7u7u7+/v8PDw8fHx8vLy9PT09fX19/f3+Pj4+fn5+vr6/Pz8/f39/v7+////BLnnfgAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB+MMBAsUDD3bzUUAAABhSURBVDjLY2BAgLp3CNCAJO6HJH4ASZwXSfwxkjgnkvhzJHFWJPHXSOKMSOLvFJAk1iGJJyCJ5yGJL0AS10MSf4Akzo0k/hRJnB1J/CWSOAuS+FsG7GA0sEYDazSwBiSwAPzzGpfLqBMlAAAAAElFTkSuQmCC"
|
||||||
style="max-height: 4px;" alt="">
|
style="max-height: 4px;">
|
||||||
<div style="line-height: 18px;height: 18px;"> </div>
|
<div style="line-height: 18px;height: 18px;"> </div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -216,8 +216,7 @@ class AsyncFormView(AsyncMixin, FormView):
|
|||||||
task_base = ProfiledEventTask
|
task_base = ProfiledEventTask
|
||||||
|
|
||||||
def __init_subclass__(cls):
|
def __init_subclass__(cls):
|
||||||
def async_execute(self, *, request_path, query_string, form_kwargs, locale, tz, url_kwargs=None, url_args=None,
|
def async_execute(self, *, request_path, query_string, form_kwargs, locale, tz, organizer=None, event=None, user=None, session_key=None):
|
||||||
organizer=None, event=None, user=None, session_key=None):
|
|
||||||
view_instance = cls()
|
view_instance = cls()
|
||||||
form_kwargs['data'] = QueryDict(form_kwargs['data'])
|
form_kwargs['data'] = QueryDict(form_kwargs['data'])
|
||||||
req = RequestFactory().post(
|
req = RequestFactory().post(
|
||||||
@@ -226,8 +225,6 @@ class AsyncFormView(AsyncMixin, FormView):
|
|||||||
content_type='application/x-www-form-urlencoded'
|
content_type='application/x-www-form-urlencoded'
|
||||||
)
|
)
|
||||||
view_instance.request = req
|
view_instance.request = req
|
||||||
view_instance.kwargs = url_kwargs
|
|
||||||
view_instance.args = url_args
|
|
||||||
if event:
|
if event:
|
||||||
view_instance.request.event = event
|
view_instance.request.event = event
|
||||||
view_instance.request.organizer = event.organizer
|
view_instance.request.organizer = event.organizer
|
||||||
@@ -287,8 +284,6 @@ class AsyncFormView(AsyncMixin, FormView):
|
|||||||
'request_path': self.request.path,
|
'request_path': self.request.path,
|
||||||
'query_string': self.request.GET.urlencode(),
|
'query_string': self.request.GET.urlencode(),
|
||||||
'form_kwargs': form_kwargs,
|
'form_kwargs': form_kwargs,
|
||||||
'url_args': self.args,
|
|
||||||
'url_kwargs': self.kwargs,
|
|
||||||
'locale': get_language(),
|
'locale': get_language(),
|
||||||
'tz': get_current_timezone().zone,
|
'tz': get_current_timezone().zone,
|
||||||
}
|
}
|
||||||
@@ -341,8 +336,6 @@ class AsyncPostView(AsyncMixin, View):
|
|||||||
content_type='application/x-www-form-urlencoded'
|
content_type='application/x-www-form-urlencoded'
|
||||||
)
|
)
|
||||||
view_instance.request = req
|
view_instance.request = req
|
||||||
view_instance.kwargs = url_kwargs
|
|
||||||
view_instance.args = url_args
|
|
||||||
if event:
|
if event:
|
||||||
view_instance.request.event = event
|
view_instance.request.event = event
|
||||||
view_instance.request.organizer = event.organizer
|
view_instance.request.organizer = event.organizer
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
|
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
|
||||||
|
|
||||||
from ...base.forms import I18nModelForm
|
from ...base.forms import I18nModelForm
|
||||||
|
|
||||||
# Import for backwards compatibility with okd import paths
|
# Import for backwards compatibility with okd import paths
|
||||||
@@ -129,7 +127,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
if hasattr(self.file, 'display_name'):
|
if hasattr(self.file, 'display_name'):
|
||||||
return self.file.display_name
|
return self.file.display_name
|
||||||
return clean_filename(os.path.basename(self.file.name))
|
return os.path.basename(self.file.name).split('.', 1)[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
|
|||||||
@@ -129,11 +129,6 @@ class QuestionForm(I18nModelForm):
|
|||||||
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def clean_identifier(self):
|
|
||||||
val = self.cleaned_data.get('identifier')
|
|
||||||
Question._clean_identifier(self.instance.event, val, self.instance)
|
|
||||||
return val
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
if d.get('dependency_question') and not d.get('dependency_values'):
|
if d.get('dependency_question') and not d.get('dependency_values'):
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ from pytz import common_timezones
|
|||||||
|
|
||||||
from pretix.api.models import WebHook
|
from pretix.api.models import WebHook
|
||||||
from pretix.api.webhooks import get_all_webhook_events
|
from pretix.api.webhooks import get_all_webhook_events
|
||||||
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
|
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||||
from pretix.base.forms.questions import (
|
from pretix.base.forms.questions import (
|
||||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
|
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
|
||||||
@@ -62,7 +61,6 @@ from pretix.base.models import (
|
|||||||
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
|
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
|
||||||
MembershipType, Organizer, Team,
|
MembershipType, Organizer, Team,
|
||||||
)
|
)
|
||||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
|
||||||
from pretix.base.models.organizer import OrganizerFooterLink
|
from pretix.base.models.organizer import OrganizerFooterLink
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
|
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
|
||||||
from pretix.control.forms import ExtFileField, SplitDateTimeField
|
from pretix.control.forms import ExtFileField, SplitDateTimeField
|
||||||
@@ -161,7 +159,7 @@ class OrganizerUpdateForm(OrganizerForm):
|
|||||||
instance = super().save(commit)
|
instance = super().save(commit)
|
||||||
|
|
||||||
if self.domain:
|
if self.domain:
|
||||||
current_domain = instance.domains.filter(event__isnull=True).first()
|
current_domain = instance.domains.first()
|
||||||
if self.cleaned_data['domain']:
|
if self.cleaned_data['domain']:
|
||||||
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
|
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
|
||||||
current_domain.delete()
|
current_domain.delete()
|
||||||
@@ -356,7 +354,6 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
auto_fields = [
|
auto_fields = [
|
||||||
'allowed_restricted_plugins',
|
'allowed_restricted_plugins',
|
||||||
'customer_accounts',
|
'customer_accounts',
|
||||||
'customer_accounts_native',
|
|
||||||
'customer_accounts_link_by_email',
|
'customer_accounts_link_by_email',
|
||||||
'invoice_regenerate_allowed',
|
'invoice_regenerate_allowed',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
@@ -634,10 +631,6 @@ class CustomerUpdateForm(forms.ModelForm):
|
|||||||
titles=self.instance.organizer.settings.name_scheme_titles,
|
titles=self.instance.organizer.settings.name_scheme_titles,
|
||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
)
|
)
|
||||||
if self.instance.provider_id:
|
|
||||||
self.fields['email'].disabled = True
|
|
||||||
self.fields['is_verified'].disabled = True
|
|
||||||
self.fields['external_identifier'].disabled = True
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
email = self.cleaned_data.get('email')
|
email = self.cleaned_data.get('email')
|
||||||
@@ -713,120 +706,3 @@ OrganizerFooterLinkFormset = inlineformset_factory(
|
|||||||
formset=BaseOrganizerFooterLinkFormSet,
|
formset=BaseOrganizerFooterLinkFormSet,
|
||||||
can_order=False, can_delete=True, extra=0
|
can_order=False, can_delete=True, extra=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SSOProviderForm(I18nModelForm):
|
|
||||||
|
|
||||||
config_oidc_base_url = forms.URLField(
|
|
||||||
label=pgettext_lazy('sso_oidc', 'Base URL'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
config_oidc_client_id = forms.CharField(
|
|
||||||
label=pgettext_lazy('sso_oidc', 'Client ID'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
config_oidc_client_secret = forms.CharField(
|
|
||||||
label=pgettext_lazy('sso_oidc', 'Client secret'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
config_oidc_scope = forms.CharField(
|
|
||||||
label=pgettext_lazy('sso_oidc', 'Scope'),
|
|
||||||
help_text=pgettext_lazy('sso_oidc', 'Multiple scopes separated with spaces.'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
config_oidc_uid_field = forms.CharField(
|
|
||||||
label=pgettext_lazy('sso_oidc', 'User ID field'),
|
|
||||||
help_text=pgettext_lazy('sso_oidc', 'We will assume that the contents of the user ID fields are unique and '
|
|
||||||
'can never change for a user.'),
|
|
||||||
required=True,
|
|
||||||
initial='sub',
|
|
||||||
)
|
|
||||||
config_oidc_email_field = forms.CharField(
|
|
||||||
label=pgettext_lazy('sso_oidc', 'Email field'),
|
|
||||||
help_text=pgettext_lazy('sso_oidc', 'We will assume that all email addresses received from the SSO provider '
|
|
||||||
'are verified to really belong the the user. If this can\'t be '
|
|
||||||
'guaranteed, security issues might arise.'),
|
|
||||||
required=True,
|
|
||||||
initial='email',
|
|
||||||
)
|
|
||||||
config_oidc_phone_field = forms.CharField(
|
|
||||||
label=pgettext_lazy('sso_oidc', 'Phone field'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CustomerSSOProvider
|
|
||||||
fields = ['is_active', 'name', 'button_label', 'method']
|
|
||||||
widgets = {
|
|
||||||
'method': forms.RadioSelect,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
name_scheme = self.event.settings.name_scheme
|
|
||||||
scheme = PERSON_NAME_SCHEMES.get(name_scheme)
|
|
||||||
for fname, label, size in scheme['fields']:
|
|
||||||
self.fields[f'config_oidc_{fname}_field'] = forms.CharField(
|
|
||||||
label=pgettext_lazy('sso_oidc', f'{label} field').format(label=label),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.fields['method'].choices = [c for c in self.fields['method'].choices if c[0]]
|
|
||||||
|
|
||||||
for fname, f in self.fields.items():
|
|
||||||
if fname.startswith('config_'):
|
|
||||||
prefix, method, suffix = fname.split('_', 2)
|
|
||||||
f.widget.attrs['data-display-dependency'] = f'input[name=method][value={method}]'
|
|
||||||
|
|
||||||
if self.instance and self.instance.method == method:
|
|
||||||
f.initial = self.instance.configuration.get(suffix)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
data = self.cleaned_data
|
|
||||||
if not data.get("method"):
|
|
||||||
return data
|
|
||||||
|
|
||||||
config = {}
|
|
||||||
for fname, f in self.fields.items():
|
|
||||||
if fname.startswith(f'config_{data["method"]}_'):
|
|
||||||
prefix, method, suffix = fname.split('_', 2)
|
|
||||||
config[suffix] = data.get(fname)
|
|
||||||
|
|
||||||
if data["method"] == "oidc":
|
|
||||||
oidc_validate_and_complete_config(config)
|
|
||||||
|
|
||||||
self.instance.configuration = config
|
|
||||||
|
|
||||||
|
|
||||||
class SSOClientForm(I18nModelForm):
|
|
||||||
regenerate_client_secret = forms.BooleanField(
|
|
||||||
label=_('Invalidate old client secret and generate a new one'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CustomerSSOClient
|
|
||||||
fields = ['is_active', 'name', 'client_id', 'client_type', 'authorization_grant_type', 'redirect_uris',
|
|
||||||
'allowed_scopes']
|
|
||||||
widgets = {
|
|
||||||
'authorization_grant_type': forms.RadioSelect,
|
|
||||||
'client_type': forms.RadioSelect,
|
|
||||||
'allowed_scopes': forms.CheckboxSelectMultiple,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['allowed_scopes'] = forms.MultipleChoiceField(
|
|
||||||
label=self.fields['allowed_scopes'].label,
|
|
||||||
help_text=self.fields['allowed_scopes'].help_text,
|
|
||||||
required=self.fields['allowed_scopes'].required,
|
|
||||||
initial=self.fields['allowed_scopes'].initial,
|
|
||||||
choices=CustomerSSOClient.SCOPE_CHOICES,
|
|
||||||
widget=forms.CheckboxSelectMultiple
|
|
||||||
)
|
|
||||||
if self.instance and self.instance.pk:
|
|
||||||
self.fields['client_id'].disabled = True
|
|
||||||
else:
|
|
||||||
del self.fields['client_id']
|
|
||||||
del self.fields['regenerate_client_secret']
|
|
||||||
|
|||||||
@@ -319,14 +319,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||||
'pretix.webhook.created': _('The webhook has been created.'),
|
'pretix.webhook.created': _('The webhook has been created.'),
|
||||||
'pretix.webhook.changed': _('The webhook has been changed.'),
|
'pretix.webhook.changed': _('The webhook has been changed.'),
|
||||||
'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'),
|
|
||||||
'pretix.webhook.retries.dropped': _('The webhook call retry jobs have been dropped.'),
|
|
||||||
'pretix.ssoprovider.created': _('The SSO provider has been created.'),
|
|
||||||
'pretix.ssoprovider.changed': _('The SSO provider has been changed.'),
|
|
||||||
'pretix.ssoprovider.deleted': _('The SSO provider has been deleted.'),
|
|
||||||
'pretix.ssoclient.created': _('The SSO client has been created.'),
|
|
||||||
'pretix.ssoclient.changed': _('The SSO client has been changed.'),
|
|
||||||
'pretix.ssoclient.deleted': _('The SSO client has been deleted.'),
|
|
||||||
'pretix.membershiptype.created': _('The membership type has been created.'),
|
'pretix.membershiptype.created': _('The membership type has been created.'),
|
||||||
'pretix.membershiptype.changed': _('The membership type has been changed.'),
|
'pretix.membershiptype.changed': _('The membership type has been changed.'),
|
||||||
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
|
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
|
||||||
|
|||||||
@@ -550,24 +550,6 @@ def get_organizer_navigation(request):
|
|||||||
'active': 'organizer.membershiptype' in url.url_name,
|
'active': 'organizer.membershiptype' in url.url_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
children.append(
|
|
||||||
{
|
|
||||||
'label': _('SSO clients'),
|
|
||||||
'url': reverse('control:organizer.ssoclients', kwargs={
|
|
||||||
'organizer': request.organizer.slug
|
|
||||||
}),
|
|
||||||
'active': 'organizer.ssoclient' in url.url_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
children.append(
|
|
||||||
{
|
|
||||||
'label': _('SSO providers'),
|
|
||||||
'url': reverse('control:organizer.ssoproviders', kwargs={
|
|
||||||
'organizer': request.organizer.slug
|
|
||||||
}),
|
|
||||||
'active': 'organizer.ssoprovider' in url.url_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if children:
|
if children:
|
||||||
nav.append({
|
nav.append({
|
||||||
'label': _('Customer accounts'),
|
'label': _('Customer accounts'),
|
||||||
|
|||||||
@@ -72,10 +72,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="post" action="{% url "control:event.orders.checkinlists.bulk_action" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}" data-asynctask>
|
<form method="post" action="{% url "control:event.orders.checkinlists.bulk_action" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}" data-asynctask>
|
||||||
{% for field in filter_form %}
|
<div class="hidden">
|
||||||
{{ field.as_hidden }}
|
{{ filter_form.as_p }}
|
||||||
{% endfor %}
|
<input name="returnquery" type="hidden" value="{{ request.META.QUERY_STRING }}">
|
||||||
<input name="returnquery" type="hidden" value="{{ request.META.QUERY_STRING }}">
|
</div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-condensed table-hover">
|
<table class="table table-condensed table-hover">
|
||||||
|
|||||||
@@ -207,16 +207,14 @@
|
|||||||
{% bootstrap_field sform.logo_show_title layout="control" %}
|
{% bootstrap_field sform.logo_show_title layout="control" %}
|
||||||
{% bootstrap_field sform.og_image layout="control" %}
|
{% bootstrap_field sform.og_image layout="control" %}
|
||||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||||
{% with org_url|add:"#tab-0-4-open" as org_url_tab %}
|
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" "theme_color_background" "theme_round_borders" %}
|
||||||
{% propagated request.event org_url_tab "primary_color" "primary_font" "theme_color_success" "theme_color_danger" "theme_color_background" "theme_round_borders" %}
|
{% bootstrap_field sform.primary_color layout="control" %}
|
||||||
{% bootstrap_field sform.primary_color layout="control" %}
|
{% bootstrap_field sform.theme_color_success layout="control" %}
|
||||||
{% bootstrap_field sform.theme_color_success layout="control" %}
|
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
||||||
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
{% bootstrap_field sform.theme_color_background layout="control" %}
|
||||||
{% bootstrap_field sform.theme_color_background layout="control" %}
|
{% bootstrap_field sform.theme_round_borders layout="control" %}
|
||||||
{% bootstrap_field sform.theme_round_borders layout="control" %}
|
{% bootstrap_field sform.primary_font layout="control" %}
|
||||||
{% bootstrap_field sform.primary_font layout="control" %}
|
{% endpropagated %}
|
||||||
{% endpropagated %}
|
|
||||||
{% endwith %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Timeline" %}</legend>
|
<legend>{% trans "Timeline" %}</legend>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@
|
|||||||
<dt>{% trans "Invoices" %}</dt>
|
<dt>{% trans "Invoices" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% for i in invoices %}
|
{% for i in invoices %}
|
||||||
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}" target="_blank">
|
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||||
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
|
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
|
||||||
{{ i.number }}</a>
|
{{ i.number }}</a>
|
||||||
({{ i.date|date:"SHORT_DATE_FORMAT" }})
|
({{ i.date|date:"SHORT_DATE_FORMAT" }})
|
||||||
|
|||||||
@@ -27,14 +27,10 @@
|
|||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
<dt>{% trans "Customer ID" %}</dt>
|
<dt>{% trans "Customer ID" %}</dt>
|
||||||
<dd>#{{ customer.identifier }}</dd>
|
<dd>#{{ customer.identifier }}</dd>
|
||||||
{% if customer.provider %}
|
{% if customer.external_identifier %}
|
||||||
<dt>{% trans "SSO provider" %}</dt>
|
<dt>{% trans "External identifier" %}</dt>
|
||||||
<dd>{{ customer.provider.name }}</dd>
|
<dd>{{ customer.external_identifier }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if customer.external_identifier %}
|
|
||||||
<dt>{% trans "External identifier" %}</dt>
|
|
||||||
<dd>{{ customer.external_identifier }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
<dt>{% trans "Status" %}</dt>
|
<dt>{% trans "Status" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if not customer.is_active %}
|
{% if not customer.is_active %}
|
||||||
@@ -48,7 +44,7 @@
|
|||||||
<dt>{% trans "E-mail" %}</dt>
|
<dt>{% trans "E-mail" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ customer.email|default_if_none:"" }}
|
{{ customer.email|default_if_none:"" }}
|
||||||
{% if customer.email and not customer.provider %}
|
{% if customer.email %}
|
||||||
<button type="submit" name="action" value="pwreset" class="btn btn-xs btn-default">
|
<button type="submit" name="action" value="pwreset" class="btn btn-xs btn-default">
|
||||||
{% trans "Send password reset link" %}
|
{% trans "Send password reset link" %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -57,9 +57,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in filter_form %}
|
<div class="hidden">
|
||||||
{{ field.as_hidden }}
|
{{ filter_form.as_p }}
|
||||||
{% endfor %}
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-condensed table-hover table-quotas">
|
<table class="table table-condensed table-hover table-quotas">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -131,7 +131,6 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Customer accounts" %}</legend>
|
<legend>{% trans "Customer accounts" %}</legend>
|
||||||
{% bootstrap_field sform.customer_accounts layout="control" %}
|
{% bootstrap_field sform.customer_accounts layout="control" %}
|
||||||
{% bootstrap_field sform.customer_accounts_native layout="control" %}
|
|
||||||
{% bootstrap_field sform.customer_accounts_link_by_email layout="control" %}
|
{% bootstrap_field sform.customer_accounts_link_by_email layout="control" %}
|
||||||
{% bootstrap_field sform.name_scheme layout="control" %}
|
{% bootstrap_field sform.name_scheme layout="control" %}
|
||||||
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends "pretixcontrol/organizers/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% block inner %}
|
|
||||||
<h1>{% trans "Delete SSO client:" %} {{ client.name }}</h1>
|
|
||||||
<form action="" method="post" class="form-horizontal">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% if is_allowed %}
|
|
||||||
<p>{% blocktrans %}Are you sure you want to delete this SSO client?{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
<p>{% blocktrans %}This SSO client cannot be deleted since it has already been used.{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
<div class="form-group submit-group">
|
|
||||||
<a href="{% url "control:organizer.ssoclients" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
|
||||||
{% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
{% if is_allowed %}
|
|
||||||
<button type="submit" class="btn btn-danger btn-save">
|
|
||||||
{% trans "Delete" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{% extends "pretixcontrol/organizers/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% block inner %}
|
|
||||||
{% if client %}
|
|
||||||
<h1>{% trans "SSO client:" %} {{ client.name }}</h1>
|
|
||||||
{% else %}
|
|
||||||
<h1>{% trans "Create a new SSO client" %}</h1>
|
|
||||||
{% endif %}
|
|
||||||
<form class="form-horizontal" action="" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% bootstrap_form form layout="control" %}
|
|
||||||
<div class="form-group submit-group">
|
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
|
||||||
{% trans "Save" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{% extends "pretixcontrol/organizers/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% block title %}{% trans "SSO clients" %}{% endblock %}
|
|
||||||
{% block inner %}
|
|
||||||
<h1>{% trans "SSO clients" %}</h1>
|
|
||||||
<p>
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
You can allow your customers to log into other systems using their customer account credentials by setting up
|
|
||||||
your other systems as a Single-Sign-On (SSO) client based on OpenID Connect.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
<a href="{% url "control:organizer.ssoclient.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
|
||||||
<span class="fa fa-plus"></span>
|
|
||||||
{% trans "Create a new SSO client" %}
|
|
||||||
</a>
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Name" %}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for c in clients %}
|
|
||||||
<tr>
|
|
||||||
<td><strong>
|
|
||||||
<a href="{% url "control:organizer.ssoclient.edit" organizer=request.organizer.slug client=c.id %}">
|
|
||||||
{% if not c.is_active %}<del>{% endif %}
|
|
||||||
{{ c.name }}
|
|
||||||
{% if not c.is_active %}</del>{% endif %}
|
|
||||||
</a>
|
|
||||||
</strong></td>
|
|
||||||
<td class="text-right flip">
|
|
||||||
<a href="{% url "control:organizer.ssoclient.edit" organizer=request.organizer.slug client=c.id %}"
|
|
||||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
|
||||||
<a href="{% url "control:organizer.ssoclient.delete" organizer=request.organizer.slug client=c.id %}"
|
|
||||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends "pretixcontrol/organizers/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% block inner %}
|
|
||||||
<h1>{% trans "Delete SSO provider:" %} {{ provider.name }}</h1>
|
|
||||||
<form action="" method="post" class="form-horizontal">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% if is_allowed %}
|
|
||||||
<p>{% blocktrans %}Are you sure you want to delete this SSO provider?{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
<p>{% blocktrans %}This SSO provider cannot be deleted since it has already been used.{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
<div class="form-group submit-group">
|
|
||||||
<a href="{% url "control:organizer.ssoproviders" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
|
|
||||||
{% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
{% if is_allowed %}
|
|
||||||
<button type="submit" class="btn btn-danger btn-save">
|
|
||||||
{% trans "Delete" %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{% extends "pretixcontrol/organizers/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% block inner %}
|
|
||||||
{% if provider %}
|
|
||||||
<h1>{% trans "SSO provider:" %} {{ provider.name }}</h1>
|
|
||||||
{% else %}
|
|
||||||
<h1>{% trans "Create a new SSO provider" %}</h1>
|
|
||||||
{% endif %}
|
|
||||||
<form class="form-horizontal" action="" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% bootstrap_form form layout="control" %}
|
|
||||||
{% if redirect_uri %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-md-3 control-label" for="redirect_uri">
|
|
||||||
{% trans "Redirection URL" context "sso" %}
|
|
||||||
|
|
||||||
</label>
|
|
||||||
<div class="col-md-9">
|
|
||||||
<input type="text" value="{{ redirect_uri }}"
|
|
||||||
class="form-control"
|
|
||||||
disabled id="redirect_uri">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="form-group submit-group">
|
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
|
||||||
{% trans "Save" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{% extends "pretixcontrol/organizers/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bootstrap3 %}
|
|
||||||
{% block title %}{% trans "SSO providers" %}{% endblock %}
|
|
||||||
{% block inner %}
|
|
||||||
<h1>{% trans "SSO providers" %}</h1>
|
|
||||||
<p>
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
You can connect existing Single-Sign-On (SSO) providers to allow your customers to log in using your own
|
|
||||||
account system.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
<a href="{% url "control:organizer.ssoprovider.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
|
||||||
<span class="fa fa-plus"></span>
|
|
||||||
{% trans "Create a new SSO provider" %}
|
|
||||||
</a>
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Name" %}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for p in providers %}
|
|
||||||
<tr>
|
|
||||||
<td><strong>
|
|
||||||
<a href="{% url "control:organizer.ssoprovider.edit" organizer=request.organizer.slug provider=p.id %}">
|
|
||||||
{% if not p.is_active %}<del>{% endif %}
|
|
||||||
{{ p.name }}
|
|
||||||
{% if not p.is_active %}</del>{% endif %}
|
|
||||||
</a>
|
|
||||||
</strong></td>
|
|
||||||
<td class="text-right flip">
|
|
||||||
<a href="{% url "control:organizer.ssoprovider.edit" organizer=request.organizer.slug provider=p.id %}"
|
|
||||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
|
||||||
<a href="{% url "control:organizer.ssoprovider.delete" organizer=request.organizer.slug provider=p.id %}"
|
|
||||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -6,42 +6,13 @@
|
|||||||
<p>
|
<p>
|
||||||
{% trans "This page shows all calls to your webhook in the past 30 days." %}
|
{% trans "This page shows all calls to your webhook in the past 30 days." %}
|
||||||
</p>
|
</p>
|
||||||
{% if retry_count %}
|
|
||||||
<form action="" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<p>
|
|
||||||
{% blocktranslate trimmed count count=retry_count %}
|
|
||||||
One webhook is scheduled to be retried.
|
|
||||||
{% plural %}
|
|
||||||
{{ count }} webhooks are scheduled to be retried.
|
|
||||||
{% endblocktranslate %}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<button type="submit" name="action" value="expedite" class="btn btn-success">
|
|
||||||
{% trans "Retry now" %}
|
|
||||||
</button>
|
|
||||||
<button type="submit" name="action" value="drop" class="btn btn-danger">
|
|
||||||
{% trans "Stop retrying" %}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<p class="help-block">
|
|
||||||
{% blocktranslate trimmed with minutes=5 %}
|
|
||||||
Webhooks scheduled to be retried in less than {{ minutes }} minutes may not be listed here and can
|
|
||||||
no longer be stopped or expedited.
|
|
||||||
{% endblocktranslate %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
{% for c in calls %}
|
{% for c in calls %}
|
||||||
<details class="panel panel-default">
|
<details class="panel panel-default">
|
||||||
<summary class="panel-heading">
|
<summary class="panel-heading">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||||
{% if c.is_retry %}
|
{% if c.is_retry %}
|
||||||
<span class="fa fa-repeat fa-fw" data-toggle="tooltip"
|
<span class="fa fa-repeat fa-fw" data-toggle="tooltip" title="{% trans "This webhook was retried since it previously failed." %}"></span>
|
||||||
title="{% trans "This webhook was retried since it previously failed." %}"></span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="fa fa-clock-o fa-fw"></span>
|
<span class="fa fa-clock-o fa-fw"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -72,9 +72,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
<form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in filter_form %}
|
<div class="hidden">
|
||||||
{{ field.as_hidden }}
|
{{ filter_form.as_p }}
|
||||||
{% endfor %}
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-quotas">
|
<table class="table table-hover table-quotas">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class PropagatedNode(Node):
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body help-text">
|
<div class="panel-body help-text">
|
||||||
{text_expl}<br>
|
{text_expl}<br>
|
||||||
<a href="{url}" target="_blank" class="btn btn-default">
|
<a href="{url}" target="_blank">
|
||||||
{text_orga}
|
{text_orga}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -133,20 +133,6 @@ urlpatterns = [
|
|||||||
name='organizer.membershiptype.edit'),
|
name='organizer.membershiptype.edit'),
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/membershiptype/(?P<type>[^/]+)/delete$', organizer.MembershipTypeDeleteView.as_view(),
|
re_path(r'^organizer/(?P<organizer>[^/]+)/membershiptype/(?P<type>[^/]+)/delete$', organizer.MembershipTypeDeleteView.as_view(),
|
||||||
name='organizer.membershiptype.delete'),
|
name='organizer.membershiptype.delete'),
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoproviders$', organizer.SSOProviderListView.as_view(), name='organizer.ssoproviders'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoprovider/add$', organizer.SSOProviderCreateView.as_view(),
|
|
||||||
name='organizer.ssoprovider.add'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoprovider/(?P<provider>[^/]+)/edit$', organizer.SSOProviderUpdateView.as_view(),
|
|
||||||
name='organizer.ssoprovider.edit'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoprovider/(?P<provider>[^/]+)/delete$', organizer.SSOProviderDeleteView.as_view(),
|
|
||||||
name='organizer.ssoprovider.delete'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclients$', organizer.SSOClientListView.as_view(), name='organizer.ssoclients'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclient/add$', organizer.SSOClientCreateView.as_view(),
|
|
||||||
name='organizer.ssoclient.add'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclient/(?P<client>[^/]+)/edit$', organizer.SSOClientUpdateView.as_view(),
|
|
||||||
name='organizer.ssoclient.edit'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclient/(?P<client>[^/]+)/delete$', organizer.SSOClientDeleteView.as_view(),
|
|
||||||
name='organizer.ssoclient.delete'),
|
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/customers$', organizer.CustomerListView.as_view(), name='organizer.customers'),
|
re_path(r'^organizer/(?P<organizer>[^/]+)/customers$', organizer.CustomerListView.as_view(), name='organizer.customers'),
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/customers/select2$', typeahead.customer_select2, name='organizer.customers.select2'),
|
re_path(r'^organizer/(?P<organizer>[^/]+)/customers/select2$', typeahead.customer_select2, name='organizer.customers.select2'),
|
||||||
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/add$',
|
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/add$',
|
||||||
|
|||||||
@@ -1210,8 +1210,7 @@ class OrderTransition(OrderView):
|
|||||||
OrderPayment.PAYMENT_STATE_CREATED)):
|
OrderPayment.PAYMENT_STATE_CREATED)):
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if p.payment_provider:
|
p.payment_provider.cancel_payment(p)
|
||||||
p.payment_provider.cancel_payment(p)
|
|
||||||
self.order.log_action('pretix.event.order.payment.canceled', {
|
self.order.log_action('pretix.event.order.payment.canceled', {
|
||||||
'local_id': p.local_id,
|
'local_id': p.local_id,
|
||||||
'provider': p.provider,
|
'provider': p.provider,
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ from django.views.generic import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from pretix.api.models import WebHook
|
from pretix.api.models import WebHook
|
||||||
from pretix.api.webhooks import manually_retry_all_calls
|
|
||||||
from pretix.base.auth import get_auth_backends
|
from pretix.base.auth import get_auth_backends
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
@@ -71,7 +70,6 @@ from pretix.base.models import (
|
|||||||
Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer,
|
Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer,
|
||||||
Team, TeamInvite, User,
|
Team, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
|
||||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
||||||
from pretix.base.models.giftcards import (
|
from pretix.base.models.giftcards import (
|
||||||
GiftCardTransaction, gen_giftcard_secret,
|
GiftCardTransaction, gen_giftcard_secret,
|
||||||
@@ -95,8 +93,7 @@ from pretix.control.forms.organizer import (
|
|||||||
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
|
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||||
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||||
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
|
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
|
||||||
OrganizerSettingsForm, OrganizerUpdateForm, SSOClientForm, SSOProviderForm,
|
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||||
TeamForm, WebHookForm,
|
|
||||||
)
|
)
|
||||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
@@ -1255,7 +1252,6 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['webhook'] = self.webhook
|
ctx['webhook'] = self.webhook
|
||||||
ctx['retry_count'] = self.webhook.retries.count()
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -1267,25 +1263,6 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.webhook.calls.order_by('-datetime')
|
return self.webhook.calls.order_by('-datetime')
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
if request.POST.get("action") == "expedite":
|
|
||||||
self.request.organizer.log_action('pretix.webhook.retries.expedited', user=self.request.user, data={
|
|
||||||
'webhook': self.webhook.pk,
|
|
||||||
})
|
|
||||||
manually_retry_all_calls.apply_async(args=(self.webhook.pk,))
|
|
||||||
messages.success(request, _('All requests will now be scheduled for an immediate attempt. Please '
|
|
||||||
'allow for a few minutes before they are processed.'))
|
|
||||||
elif request.POST.get("action") == "drop":
|
|
||||||
self.request.organizer.log_action('pretix.webhook.retries.dropped', user=self.request.user, data={
|
|
||||||
'webhook': self.webhook.pk,
|
|
||||||
})
|
|
||||||
self.webhook.retries.all().delete()
|
|
||||||
messages.success(request, _('All unprocessed webhooks have been stopped from retrying.'))
|
|
||||||
return redirect(reverse('control:organizer.webhook.logs', kwargs={
|
|
||||||
'organizer': self.request.organizer.slug,
|
|
||||||
'webhook': self.webhook.pk,
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
@@ -1926,247 +1903,6 @@ class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequ
|
|||||||
return redirect(success_url)
|
return redirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
class SSOProviderListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
||||||
model = CustomerSSOProvider
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoproviders.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
context_object_name = 'providers'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.request.organizer.sso_providers.all()
|
|
||||||
|
|
||||||
|
|
||||||
class SSOProviderCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
||||||
model = CustomerSSOProvider
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
form_class = SSOProviderForm
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return get_object_or_404(CustomerSSOProvider, organizer=self.request.organizer, pk=self.kwargs.get('provider'))
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('control:organizer.ssoproviders', kwargs={
|
|
||||||
'organizer': self.request.organizer.slug,
|
|
||||||
})
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
kwargs['event'] = self.request.organizer
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
messages.success(self.request, _('The provider has been created.'))
|
|
||||||
form.instance.organizer = self.request.organizer
|
|
||||||
ret = super().form_valid(form)
|
|
||||||
form.instance.log_action('pretix.ssoprovider.created', user=self.request.user, data={
|
|
||||||
k: getattr(self.object, k, self.object.configuration.get(k)) for k in form.changed_data
|
|
||||||
})
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
messages.error(self.request, _('Your changes could not be saved.'))
|
|
||||||
return super().form_invalid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
||||||
model = CustomerSSOProvider
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
context_object_name = 'provider'
|
|
||||||
form_class = SSOProviderForm
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return get_object_or_404(CustomerSSOProvider, organizer=self.request.organizer, pk=self.kwargs.get('provider'))
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('control:organizer.ssoproviders', kwargs={
|
|
||||||
'organizer': self.request.organizer.slug,
|
|
||||||
})
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
|
|
||||||
'provider': self.object.pk
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
kwargs['event'] = self.request.organizer
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
if form.has_changed():
|
|
||||||
self.object.log_action('pretix.ssoprovider.changed', user=self.request.user, data={
|
|
||||||
k: getattr(self.object, k, self.object.configuration.get(k)) for k in form.changed_data
|
|
||||||
})
|
|
||||||
messages.success(self.request, _('Your changes have been saved.'))
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
messages.error(self.request, _('Your changes could not be saved.'))
|
|
||||||
return super().form_invalid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
|
||||||
model = CustomerSSOProvider
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoprovider_delete.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
context_object_name = 'provider'
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return get_object_or_404(CustomerSSOProvider, organizer=self.request.organizer, pk=self.kwargs.get('provider'))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
ctx['is_allowed'] = self.object.allow_delete()
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('control:organizer.ssoproviders', kwargs={
|
|
||||||
'organizer': self.request.organizer.slug,
|
|
||||||
})
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
success_url = self.get_success_url()
|
|
||||||
self.object = self.get_object()
|
|
||||||
if self.object.allow_delete():
|
|
||||||
self.object.log_action('pretix.ssoprovider.deleted', user=self.request.user)
|
|
||||||
self.object.delete()
|
|
||||||
messages.success(request, _('The selected object has been deleted.'))
|
|
||||||
return redirect(success_url)
|
|
||||||
|
|
||||||
|
|
||||||
class SSOClientListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
|
||||||
model = CustomerSSOClient
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoclients.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
context_object_name = 'clients'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.request.organizer.sso_clients.all()
|
|
||||||
|
|
||||||
|
|
||||||
class SSOClientCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
|
||||||
model = CustomerSSOClient
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
form_class = SSOClientForm
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('control:organizer.ssoclient.edit', kwargs={
|
|
||||||
'organizer': self.request.organizer.slug,
|
|
||||||
'client': self.object.pk
|
|
||||||
})
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
kwargs['event'] = self.request.organizer
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
secret = form.instance.set_client_secret()
|
|
||||||
messages.success(
|
|
||||||
self.request,
|
|
||||||
_('The SSO client has been created. Please note down the following client secret, it will never be shown '
|
|
||||||
'again: {secret}').format(secret=secret)
|
|
||||||
)
|
|
||||||
form.instance.organizer = self.request.organizer
|
|
||||||
ret = super().form_valid(form)
|
|
||||||
form.instance.log_action('pretix.ssoclient.created', user=self.request.user, data={
|
|
||||||
k: getattr(self.object, k, form.cleaned_data.get(k)) for k in form.changed_data
|
|
||||||
})
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
messages.error(self.request, _('Your changes could not be saved.'))
|
|
||||||
return super().form_invalid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
|
||||||
model = CustomerSSOClient
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
context_object_name = 'client'
|
|
||||||
form_class = SSOClientForm
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('control:organizer.ssoclient.edit', kwargs={
|
|
||||||
'organizer': self.request.organizer.slug,
|
|
||||||
'client': self.object.pk
|
|
||||||
})
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
kwargs['event'] = self.request.organizer
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
if form.has_changed():
|
|
||||||
self.object.log_action('pretix.ssoclient.changed', user=self.request.user, data={
|
|
||||||
k: getattr(self.object, k, form.cleaned_data.get(k)) for k in form.changed_data
|
|
||||||
})
|
|
||||||
if form.cleaned_data.get('regenerate_client_secret'):
|
|
||||||
secret = form.instance.set_client_secret()
|
|
||||||
messages.success(
|
|
||||||
self.request,
|
|
||||||
_('Your changes have been saved. Please note down the following client secret, it will never be shown '
|
|
||||||
'again: {secret}').format(secret=secret)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
messages.success(
|
|
||||||
self.request,
|
|
||||||
_('Your changes have been saved.')
|
|
||||||
)
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
messages.error(self.request, _('Your changes could not be saved.'))
|
|
||||||
return super().form_invalid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
|
||||||
model = CustomerSSOClient
|
|
||||||
template_name = 'pretixcontrol/organizers/ssoclient_delete.html'
|
|
||||||
permission = 'can_change_organizer_settings'
|
|
||||||
context_object_name = 'client'
|
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
|
||||||
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
ctx['is_allowed'] = self.object.allow_delete()
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse('control:organizer.ssoclients', kwargs={
|
|
||||||
'organizer': self.request.organizer.slug,
|
|
||||||
})
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
success_url = self.get_success_url()
|
|
||||||
self.object = self.get_object()
|
|
||||||
if self.object.allow_delete():
|
|
||||||
self.object.log_action('pretix.ssoclient.deleted', user=self.request.user)
|
|
||||||
self.object.delete()
|
|
||||||
messages.success(request, _('The selected object has been deleted.'))
|
|
||||||
return redirect(success_url)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
model = Customer
|
model = Customer
|
||||||
template_name = 'pretixcontrol/organizers/customers.html'
|
template_name = 'pretixcontrol/organizers/customers.html'
|
||||||
@@ -2212,7 +1948,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
)
|
)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if request.POST.get('action') == 'pwreset' and self.customer.provider_id is None:
|
if request.POST.get('action') == 'pwreset':
|
||||||
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
|
self.customer.log_action('pretix.customer.password.resetrequested', {}, user=self.request.user)
|
||||||
ctx = self.customer.get_email_context()
|
ctx = self.customer.get_email_context()
|
||||||
token = TokenGenerator().make_token(self.customer)
|
token = TokenGenerator().make_token(self.customer)
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ import pyuca
|
|||||||
from babel.core import Locale
|
from babel.core import Locale
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.encoding import force_str
|
from django_countries import Countries
|
||||||
from django_countries import Countries, CountryTuple
|
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||||
|
|
||||||
@@ -61,22 +60,6 @@ class CachedCountries(Countries):
|
|||||||
cache.set(cache_key, val, 3600 * 24 * 30)
|
cache.set(cache_key, val, 3600 * 24 * 30)
|
||||||
yield from val
|
yield from val
|
||||||
|
|
||||||
def translate_pair(self, code: str, name=None):
|
|
||||||
# We need to temporarily override this function until
|
|
||||||
# https://github.com/SmileyChris/django-countries/issues/364
|
|
||||||
# is fixed
|
|
||||||
if name is None:
|
|
||||||
name = self.countries[code]
|
|
||||||
if isinstance(name, dict):
|
|
||||||
if "names" in name:
|
|
||||||
country_name: str = name["names"][0]
|
|
||||||
else:
|
|
||||||
country_name = name["name"]
|
|
||||||
else:
|
|
||||||
country_name = name
|
|
||||||
country_name = force_str(country_name)
|
|
||||||
return CountryTuple(code, country_name)
|
|
||||||
|
|
||||||
|
|
||||||
class FastCountryField(CountryField):
|
class FastCountryField(CountryField):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -486,48 +486,48 @@ msgstr "الدقائق"
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "QR الدخول"
|
msgstr "QR الدخول"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
|
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "مجموعة من العناصر"
|
msgstr "مجموعة من العناصر"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "عنصر نص"
|
msgstr "عنصر نص"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "منطقة باركود"
|
msgstr "منطقة باركود"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "منطقة صورة"
|
msgstr "منطقة صورة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "مدعوم من pretix"
|
msgstr "مدعوم من pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "عنصر"
|
msgstr "عنصر"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "تصميم التذكرة"
|
msgstr "تصميم التذكرة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "فشلت عملية الحفظ."
|
msgstr "فشلت عملية الحفظ."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr "حصل خطأ أثناء رفع ملف PDF الخاص بك، يرجى المحاولة مرة أخرى."
|
msgstr "حصل خطأ أثناء رفع ملف PDF الخاص بك، يرجى المحاولة مرة أخرى."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟"
|
msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟"
|
||||||
|
|
||||||
@@ -573,15 +573,15 @@ msgstr "البحث في الاستفسارات"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "المختارة فقط"
|
msgstr "المختارة فقط"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "اضغط لاغلاق الصفحة"
|
msgstr "اضغط لاغلاق الصفحة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "لم تقم بحفظ التعديلات!"
|
msgstr "لم تقم بحفظ التعديلات!"
|
||||||
|
|
||||||
@@ -655,20 +655,20 @@ msgstr "ستسترد %(currency)%(amount)"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
|
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "مطلوب"
|
msgstr "مطلوب"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "المنطقة الزمنية:"
|
msgstr "المنطقة الزمنية:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "التوقيت المحلي:"
|
msgstr "التوقيت المحلي:"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -469,48 +469,48 @@ msgstr ""
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Disseny del tiquet"
|
msgstr "Disseny del tiquet"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -556,15 +556,15 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -628,22 +628,22 @@ msgstr ""
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "Cistella expirada"
|
msgstr "Cistella expirada"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2021-12-06 23:00+0000\n"
|
"PO-Revision-Date: 2021-12-06 23:00+0000\n"
|
||||||
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
||||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -483,48 +483,48 @@ msgstr "minuty"
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in QR kód"
|
msgstr "Check-in QR kód"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Pozadí PDF nemohl být načten:"
|
msgstr "Pozadí PDF nemohl být načten:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Skupina objektů"
|
msgstr "Skupina objektů"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Textový objekt"
|
msgstr "Textový objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "Oblast s QR kódem"
|
msgstr "Oblast s QR kódem"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Oblast obrazu"
|
msgstr "Oblast obrazu"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Poháněno společností pretix"
|
msgstr "Poháněno společností pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Design vstupenky"
|
msgstr "Design vstupenky"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Uložení se nepodařilo."
|
msgstr "Uložení se nepodařilo."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr "Při nahrávání souboru PDF došlo k problému, zkuste to prosím znovu."
|
msgstr "Při nahrávání souboru PDF došlo k problému, zkuste to prosím znovu."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr "Opravdu chcete opustit editor bez uložení změn?"
|
msgstr "Opravdu chcete opustit editor bez uložení změn?"
|
||||||
|
|
||||||
@@ -573,15 +573,15 @@ msgstr "Hledaný výraz"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Pouze vybrané"
|
msgstr "Pouze vybrané"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Interně používat jiný název"
|
msgstr "Interně používat jiný název"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Kliknutím zavřete"
|
msgstr "Kliknutím zavřete"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Máte neuložené změny!"
|
msgstr "Máte neuložené změny!"
|
||||||
|
|
||||||
@@ -648,20 +648,20 @@ msgstr "Dostanete %(currency)s %(amount)s zpět"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "Zadejte částku, kterou si organizátor může ponechat."
|
msgstr "Zadejte částku, kterou si organizátor může ponechat."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
|
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "povinný"
|
msgstr "povinný"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Časové pásmo:"
|
msgstr "Časové pásmo:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Místní čas:"
|
msgstr "Místní čas:"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2022-04-01 13:36+0000\n"
|
"PO-Revision-Date: 2022-04-01 13:36+0000\n"
|
||||||
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
|
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
|
||||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -511,50 +511,50 @@ msgstr ""
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in QR"
|
msgstr "Check-in QR"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Baggrunds-pdf'en kunne ikke hentes af følgende grund:"
|
msgstr "Baggrunds-pdf'en kunne ikke hentes af følgende grund:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Gruppe af objekter"
|
msgstr "Gruppe af objekter"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Tekstobjekt"
|
msgstr "Tekstobjekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "QR-kode-område"
|
msgstr "QR-kode-område"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Barcode area"
|
#| msgid "Barcode area"
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "QR-kode-område"
|
msgstr "QR-kode-område"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Drevet af pretix"
|
msgstr "Drevet af pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Billetdesign"
|
msgstr "Billetdesign"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Gem fejlede."
|
msgstr "Gem fejlede."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr "Fejl under upload af pdf. Prøv venligt igen."
|
msgstr "Fejl under upload af pdf. Prøv venligt igen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Er du sikker på at du vil forlade editoren uden at gemme dine ændringer?"
|
"Er du sikker på at du vil forlade editoren uden at gemme dine ændringer?"
|
||||||
@@ -601,15 +601,15 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Klik for at lukke"
|
msgstr "Klik for at lukke"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Du har ændringer, der ikke er gemt!"
|
msgstr "Du har ændringer, der ikke er gemt!"
|
||||||
|
|
||||||
@@ -683,22 +683,22 @@ msgstr "fra %(currency)s %(price)s"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "Kurv udløbet"
|
msgstr "Kurv udløbet"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Tidszone:"
|
msgstr "Tidszone:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Din lokaltid:"
|
msgstr "Din lokaltid:"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
|
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -484,49 +484,49 @@ msgstr "Minuten"
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in-QR-Code"
|
msgstr "Check-in-QR-Code"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Gruppe von Objekten"
|
msgstr "Gruppe von Objekten"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Text-Objekt"
|
msgstr "Text-Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "QR-Code-Bereich"
|
msgstr "QR-Code-Bereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Bildbereich"
|
msgstr "Bildbereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Event-Ticketshop von pretix"
|
msgstr "Event-Ticketshop von pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Ticket-Design"
|
msgstr "Ticket-Design"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Speichern fehlgeschlagen."
|
msgstr "Speichern fehlgeschlagen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||||
@@ -577,15 +577,15 @@ msgstr "Suchbegriff"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Nur ausgewählte"
|
msgstr "Nur ausgewählte"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Intern einen anderen Namen verwenden"
|
msgstr "Intern einen anderen Namen verwenden"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Klicken zum Schließen"
|
msgstr "Klicken zum Schließen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Sie haben ungespeicherte Änderungen!"
|
msgstr "Sie haben ungespeicherte Änderungen!"
|
||||||
|
|
||||||
@@ -650,20 +650,20 @@ msgstr "Sie erhalten %(currency)s %(amount)s zurück"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
|
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
|
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "verpflichtend"
|
msgstr "verpflichtend"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Zeitzone:"
|
msgstr "Zeitzone:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Deine lokale Zeit:"
|
msgstr "Deine lokale Zeit:"
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ ggf
|
|||||||
GiroCode
|
GiroCode
|
||||||
giropay
|
giropay
|
||||||
GPL
|
GPL
|
||||||
Grants
|
|
||||||
Guide
|
Guide
|
||||||
Gutscheineinlöser
|
Gutscheineinlöser
|
||||||
herunterscrollen
|
herunterscrollen
|
||||||
@@ -139,7 +138,6 @@ innennamen
|
|||||||
Installations
|
Installations
|
||||||
integrationen
|
integrationen
|
||||||
INV
|
INV
|
||||||
invalidieren
|
|
||||||
invalidiert
|
invalidiert
|
||||||
ISU
|
ISU
|
||||||
iOS
|
iOS
|
||||||
@@ -178,10 +176,8 @@ name
|
|||||||
Nr
|
Nr
|
||||||
number
|
number
|
||||||
OK
|
OK
|
||||||
On
|
|
||||||
Open
|
Open
|
||||||
OpenCage
|
OpenCage
|
||||||
OpenID
|
|
||||||
OpenStreetMap
|
OpenStreetMap
|
||||||
Opera
|
Opera
|
||||||
Output
|
Output
|
||||||
@@ -240,15 +236,12 @@ Scanergebnis
|
|||||||
Scanning
|
Scanning
|
||||||
schiefgeht
|
schiefgeht
|
||||||
schiefgelaufen
|
schiefgelaufen
|
||||||
Scope
|
|
||||||
Scopes
|
|
||||||
sechsstelligen
|
sechsstelligen
|
||||||
Secret
|
Secret
|
||||||
Security
|
Security
|
||||||
SEPA
|
SEPA
|
||||||
Shirts
|
Shirts
|
||||||
Signaturverfahren
|
Signaturverfahren
|
||||||
Sign
|
|
||||||
Sitzplanmoduls
|
Sitzplanmoduls
|
||||||
Social
|
Social
|
||||||
Sofort
|
Sofort
|
||||||
@@ -257,7 +250,6 @@ Sorry
|
|||||||
Source
|
Source
|
||||||
SPF
|
SPF
|
||||||
SSL
|
SSL
|
||||||
SSO
|
|
||||||
STARTTLS
|
STARTTLS
|
||||||
Steuerschuldnerschaft
|
Steuerschuldnerschaft
|
||||||
Store
|
Store
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
|
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||||
@@ -483,49 +483,49 @@ msgstr "Minuten"
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Check-in-QR-Code"
|
msgstr "Check-in-QR-Code"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Gruppe von Objekten"
|
msgstr "Gruppe von Objekten"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Text-Objekt"
|
msgstr "Text-Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "QR-Code-Bereich"
|
msgstr "QR-Code-Bereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Bildbereich"
|
msgstr "Bildbereich"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Event-Ticketshop von pretix"
|
msgstr "Event-Ticketshop von pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objekt"
|
msgstr "Objekt"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Ticket-Design"
|
msgstr "Ticket-Design"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Speichern fehlgeschlagen."
|
msgstr "Speichern fehlgeschlagen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||||
@@ -576,15 +576,15 @@ msgstr "Suchbegriff"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Nur ausgewählte"
|
msgstr "Nur ausgewählte"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Intern einen anderen Namen verwenden"
|
msgstr "Intern einen anderen Namen verwenden"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Klicken zum Schließen"
|
msgstr "Klicken zum Schließen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Du hast ungespeicherte Änderungen!"
|
msgstr "Du hast ungespeicherte Änderungen!"
|
||||||
|
|
||||||
@@ -649,20 +649,20 @@ msgstr "Du erhältst %(currency)s %(amount)s zurück"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
|
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Bitte trage eine Menge für eines der Produkte ein."
|
msgstr "Bitte trage eine Menge für eines der Produkte ein."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "verpflichtend"
|
msgstr "verpflichtend"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Zeitzone:"
|
msgstr "Zeitzone:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Deine lokale Zeit:"
|
msgstr "Deine lokale Zeit:"
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ ggf
|
|||||||
GiroCode
|
GiroCode
|
||||||
giropay
|
giropay
|
||||||
GPL
|
GPL
|
||||||
Grants
|
|
||||||
Guide
|
Guide
|
||||||
Gutscheineinlöser
|
Gutscheineinlöser
|
||||||
herunterscrollen
|
herunterscrollen
|
||||||
@@ -139,7 +138,6 @@ innennamen
|
|||||||
Installations
|
Installations
|
||||||
integrationen
|
integrationen
|
||||||
INV
|
INV
|
||||||
invalidieren
|
|
||||||
invalidiert
|
invalidiert
|
||||||
ISU
|
ISU
|
||||||
iOS
|
iOS
|
||||||
@@ -178,10 +176,8 @@ name
|
|||||||
Nr
|
Nr
|
||||||
number
|
number
|
||||||
OK
|
OK
|
||||||
On
|
|
||||||
Open
|
Open
|
||||||
OpenCage
|
OpenCage
|
||||||
OpenID
|
|
||||||
OpenStreetMap
|
OpenStreetMap
|
||||||
Opera
|
Opera
|
||||||
Output
|
Output
|
||||||
@@ -240,15 +236,12 @@ Scanergebnis
|
|||||||
Scanning
|
Scanning
|
||||||
schiefgeht
|
schiefgeht
|
||||||
schiefgelaufen
|
schiefgelaufen
|
||||||
Scope
|
|
||||||
Scopes
|
|
||||||
sechsstelligen
|
sechsstelligen
|
||||||
Secret
|
Secret
|
||||||
Security
|
Security
|
||||||
SEPA
|
SEPA
|
||||||
Shirts
|
Shirts
|
||||||
Signaturverfahren
|
Signaturverfahren
|
||||||
Sign
|
|
||||||
Sitzplanmoduls
|
Sitzplanmoduls
|
||||||
Social
|
Social
|
||||||
Sofort
|
Sofort
|
||||||
@@ -257,7 +250,6 @@ Sorry
|
|||||||
Source
|
Source
|
||||||
SPF
|
SPF
|
||||||
SSL
|
SSL
|
||||||
SSO
|
|
||||||
STARTTLS
|
STARTTLS
|
||||||
Steuerschuldnerschaft
|
Steuerschuldnerschaft
|
||||||
Store
|
Store
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -468,48 +468,48 @@ msgstr ""
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -555,15 +555,15 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -623,20 +623,20 @@ msgstr ""
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -518,51 +518,51 @@ msgstr ""
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Έλεγχος QR"
|
msgstr "Έλεγχος QR"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Το αρχείο φόντου PDF δεν ήταν δυνατό να φορτωθεί για τον ακόλουθο λόγο:"
|
"Το αρχείο φόντου PDF δεν ήταν δυνατό να φορτωθεί για τον ακόλουθο λόγο:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Ομάδα αντικειμένων"
|
msgstr "Ομάδα αντικειμένων"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Αντικείμενο κειμένου"
|
msgstr "Αντικείμενο κειμένου"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "Περιοχή Barcode"
|
msgstr "Περιοχή Barcode"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Barcode area"
|
#| msgid "Barcode area"
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Περιοχή Barcode"
|
msgstr "Περιοχή Barcode"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Υποστηρίζεται από το Pretix"
|
msgstr "Υποστηρίζεται από το Pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Αντικείμενο"
|
msgstr "Αντικείμενο"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Σχεδιασμός εισιτηρίων"
|
msgstr "Σχεδιασμός εισιτηρίων"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Η αποθήκευση απέτυχε."
|
msgstr "Η αποθήκευση απέτυχε."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr "Σφάλμα κατά τη μεταφόρτωση του αρχείου PDF, δοκιμάστε ξανά."
|
msgstr "Σφάλμα κατά τη μεταφόρτωση του αρχείου PDF, δοκιμάστε ξανά."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Θέλετε πραγματικά να αφήσετε τον επεξεργαστή χωρίς να αποθηκεύσετε τις "
|
"Θέλετε πραγματικά να αφήσετε τον επεξεργαστή χωρίς να αποθηκεύσετε τις "
|
||||||
@@ -615,15 +615,15 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
|
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Κάντε κλικ για να κλείσετε"
|
msgstr "Κάντε κλικ για να κλείσετε"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -695,22 +695,22 @@ msgstr "απο %(currency)s %(price)s"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
|
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "Το καλάθι έληξε"
|
msgstr "Το καλάθι έληξε"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2021-11-25 21:00+0000\n"
|
"PO-Revision-Date: 2021-11-25 21:00+0000\n"
|
||||||
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
||||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -483,51 +483,51 @@ msgstr "minutos"
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "QR de Chequeo"
|
msgstr "QR de Chequeo"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"El archivo PDF de fondo no ha podido ser cargado debido al siguiente motivo:"
|
"El archivo PDF de fondo no ha podido ser cargado debido al siguiente motivo:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Grupo de objetos"
|
msgstr "Grupo de objetos"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Objeto de texto"
|
msgstr "Objeto de texto"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "Área para código de barras"
|
msgstr "Área para código de barras"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Área de imagen"
|
msgstr "Área de imagen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Proveído por pretix"
|
msgstr "Proveído por pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objeto"
|
msgstr "Objeto"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Diseño del ticket"
|
msgstr "Diseño del ticket"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "El guardado falló."
|
msgstr "El guardado falló."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ha habido un error mientras se cargaba el archivo PDF, por favor, intente de "
|
"Ha habido un error mientras se cargaba el archivo PDF, por favor, intente de "
|
||||||
"nuevo."
|
"nuevo."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr "¿Realmente desea salir del editor sin haber guardado sus cambios?"
|
msgstr "¿Realmente desea salir del editor sin haber guardado sus cambios?"
|
||||||
|
|
||||||
@@ -577,15 +577,15 @@ msgstr "Consultar búsqueda"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Solamente seleccionados"
|
msgstr "Solamente seleccionados"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Usar un nombre diferente internamente"
|
msgstr "Usar un nombre diferente internamente"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Click para cerrar"
|
msgstr "Click para cerrar"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "¡Tienes cambios sin guardar!"
|
msgstr "¡Tienes cambios sin guardar!"
|
||||||
|
|
||||||
@@ -649,20 +649,20 @@ msgstr "Obtienes %(currency)s %(price)s de vuelta"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr "Por favor, ingrese el monto que el organizador puede quedarse."
|
msgstr "Por favor, ingrese el monto que el organizador puede quedarse."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "Por favor, introduzca un valor para cada tipo de entrada."
|
msgstr "Por favor, introduzca un valor para cada tipo de entrada."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "campo requerido"
|
msgstr "campo requerido"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Zona horaria:"
|
msgstr "Zona horaria:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr "Su hora local:"
|
msgstr "Su hora local:"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -487,50 +487,50 @@ msgstr ""
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Tekstiobjekti"
|
msgstr "Tekstiobjekti"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "Viivakoodialue"
|
msgstr "Viivakoodialue"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Barcode area"
|
#| msgid "Barcode area"
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Viivakoodialue"
|
msgstr "Viivakoodialue"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "Tallennus epäonnistui."
|
msgstr "Tallennus epäonnistui."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -576,15 +576,15 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Käytä toista nimeä sisäisesti"
|
msgstr "Käytä toista nimeä sisäisesti"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Sulje klikkaamalla"
|
msgstr "Sulje klikkaamalla"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Sinulla on tallentamattomia muutoksia!"
|
msgstr "Sinulla on tallentamattomia muutoksia!"
|
||||||
|
|
||||||
@@ -648,22 +648,22 @@ msgstr ""
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "Ostoskori on vanhentunut"
|
msgstr "Ostoskori on vanhentunut"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr "Aikavyöhyke:"
|
msgstr "Aikavyöhyke:"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: French\n"
|
"Project-Id-Version: French\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-09-29 09:24+0000\n"
|
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||||
"PO-Revision-Date: 2022-04-07 10:40+0000\n"
|
"PO-Revision-Date: 2022-04-07 10:40+0000\n"
|
||||||
"Last-Translator: Eva-Maria Obermann <obermann@rami.io>\n"
|
"Last-Translator: Eva-Maria Obermann <obermann@rami.io>\n"
|
||||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -511,53 +511,53 @@ msgstr ""
|
|||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
msgstr "Enregistrement QR code"
|
msgstr "Enregistrement QR code"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:378
|
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||||
msgid "The PDF background file could not be loaded for the following reason:"
|
msgid "The PDF background file could not be loaded for the following reason:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Le fichier PDF généré en arrière-plan n'a pas pu être chargé pour la raison "
|
"Le fichier PDF généré en arrière-plan n'a pas pu être chargé pour la raison "
|
||||||
"suivante :"
|
"suivante :"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||||
msgid "Group of objects"
|
msgid "Group of objects"
|
||||||
msgstr "Groupe d'objets"
|
msgstr "Groupe d'objets"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||||
msgid "Text object"
|
msgid "Text object"
|
||||||
msgstr "Objet texte"
|
msgstr "Objet texte"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
msgstr "Zone de code-barres"
|
msgstr "Zone de code-barres"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Barcode area"
|
#| msgid "Barcode area"
|
||||||
msgid "Image area"
|
msgid "Image area"
|
||||||
msgstr "Zone de code-barres"
|
msgstr "Zone de code-barres"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||||
msgid "Powered by pretix"
|
msgid "Powered by pretix"
|
||||||
msgstr "Propulsé par pretix"
|
msgstr "Propulsé par pretix"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||||
msgid "Object"
|
msgid "Object"
|
||||||
msgstr "Objet"
|
msgstr "Objet"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||||
msgid "Ticket design"
|
msgid "Ticket design"
|
||||||
msgstr "Conception des billets"
|
msgstr "Conception des billets"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||||
msgid "Saving failed."
|
msgid "Saving failed."
|
||||||
msgstr "L'enregistrement a échoué."
|
msgstr "L'enregistrement a échoué."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||||
msgid "Error while uploading your PDF file, please try again."
|
msgid "Error while uploading your PDF file, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Erreur lors du téléchargement de votre fichier PDF, veuillez réessayer."
|
"Erreur lors du téléchargement de votre fichier PDF, veuillez réessayer."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1030
|
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||||
msgid "Do you really want to leave the editor without saving your changes?"
|
msgid "Do you really want to leave the editor without saving your changes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Voulez-vous vraiment quitter l'éditeur sans sauvegarder vos modifications ?"
|
"Voulez-vous vraiment quitter l'éditeur sans sauvegarder vos modifications ?"
|
||||||
@@ -607,15 +607,15 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Utiliser un nom différent en interne"
|
msgstr "Utiliser un nom différent en interne"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Cliquez pour fermer"
|
msgstr "Cliquez pour fermer"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Vous avez des modifications non sauvegardées !"
|
msgstr "Vous avez des modifications non sauvegardées !"
|
||||||
|
|
||||||
@@ -683,22 +683,22 @@ msgstr "de %(currency)s %(price)s"
|
|||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:380
|
#: pretix/static/pretixpresale/js/ui/main.js:378
|
||||||
msgid "Please enter a quantity for one of the ticket types."
|
msgid "Please enter a quantity for one of the ticket types."
|
||||||
msgstr "SVP entrez une quantité pour un de vos types de billets."
|
msgstr "SVP entrez une quantité pour un de vos types de billets."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "Panier expiré"
|
msgstr "Panier expiré"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||||
msgid "Time zone:"
|
msgid "Time zone:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||||
msgid "Your local time:"
|
msgid "Your local time:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user