mirror of
https://github.com/pretix/pretix.git
synced 2025-12-09 00:42:28 +00:00
Compare commits
5 Commits
common-cou
...
fix-pdf-ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbca2f6c40 | ||
|
|
6c0f19a1f6 | ||
|
|
37454dd84e | ||
|
|
9bba61a495 | ||
|
|
4cd4b94a4b |
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).
|
||||
@@ -2,7 +2,6 @@
|
||||
file=/tmp/supervisor.sock
|
||||
|
||||
[supervisord]
|
||||
environment = AUTOMIGRATE="skip"
|
||||
logfile=/dev/stdout
|
||||
logfile_maxbytes=0
|
||||
loglevel=info
|
||||
|
||||
@@ -6067,10 +6067,6 @@ url('../opensans_regular_macroman/OpenSans-Regular-webfont.svg#open_sansregular'
|
||||
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);
|
||||
}
|
||||
section > a.screenshot {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Changes */
|
||||
.versionchanged {
|
||||
|
||||
@@ -105,37 +105,6 @@ following endpoint:
|
||||
|
||||
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
|
||||
----------------------
|
||||
|
||||
|
||||
@@ -14,10 +14,7 @@ The customer resource contains the following public fields:
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
identifier string Internal ID of the customer
|
||||
external_identifier string External ID of the customer (or ``null``). This field can
|
||||
be changed for customers created manually or through
|
||||
the API, but is read-only for customers created through a
|
||||
SSO integration.
|
||||
external_identifier string External ID of the customer (or ``null``)
|
||||
email string Customer email address
|
||||
name string Name of this customer (or ``null``)
|
||||
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
|
||||
last_modified datetime Date and time of modification of the record
|
||||
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
|
||||
|
||||
.. versionchanged:: 4.3
|
||||
|
||||
Passwords can now be set through the API during customer creation.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -155,7 +146,6 @@ Endpoints
|
||||
|
||||
{
|
||||
"email": "test@example.org",
|
||||
"password": "verysecret",
|
||||
"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.reactivated``
|
||||
* ``pretix.event.order.expired``
|
||||
* ``pretix.event.order.expirychanged``
|
||||
* ``pretix.event.order.modified``
|
||||
* ``pretix.event.order.contact.changed``
|
||||
* ``pretix.event.order.changed.*``
|
||||
* ``pretix.event.order.refund.created``
|
||||
* ``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.denied``
|
||||
* ``pretix.event.checkin``
|
||||
@@ -56,10 +50,6 @@ The following values for ``action_types`` are valid with pretix core:
|
||||
* ``pretix.subevent.added``
|
||||
* ``pretix.subevent.changed``
|
||||
* ``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.
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
time frame, pretix will retry to deliver for up to three days with an exponential back off. Therefore, we recommend that
|
||||
you implement your endpoint in a way where calling it multiple times for the same event due to a perceived error does
|
||||
not do any harm.
|
||||
If we do not receive a status code in the range of ``200`` and ``299``, pretix will retry to deliver for up to three
|
||||
days with an exponential back off. Therefore, we recommend that you implement your endpoint in a way where calling it
|
||||
multiple times for the same event due to a perceived error does not do any harm.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
jwt
|
||||
JWT
|
||||
JWTs
|
||||
libpretixprint
|
||||
libsass
|
||||
linters
|
||||
@@ -89,9 +88,7 @@ nginx
|
||||
nodejs
|
||||
NotificationType
|
||||
npm
|
||||
OIDC
|
||||
ons
|
||||
OpenID
|
||||
optimizations
|
||||
overpayment
|
||||
param
|
||||
@@ -136,7 +133,6 @@ serializer
|
||||
serializers
|
||||
sexualized
|
||||
SQL
|
||||
SSO
|
||||
startup
|
||||
stdout
|
||||
stylesheet
|
||||
@@ -163,8 +159,6 @@ untrusted
|
||||
uptime
|
||||
username
|
||||
url
|
||||
URI
|
||||
URIs
|
||||
validator
|
||||
versa
|
||||
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>
|
||||
|
||||
If you use ``analytics.js`` (Universal Analytics)::
|
||||
If you use ```analytics.js` (Universal Analytics)::
|
||||
|
||||
<script>
|
||||
(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/structureguide
|
||||
events/widget
|
||||
customers/index
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
|
||||
@@ -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",)
|
||||
|
||||
|
||||
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):
|
||||
idempotency_key = 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',
|
||||
'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):
|
||||
send_email = serializers.BooleanField(default=False, required=False, allow_null=True)
|
||||
password = serializers.CharField(write_only=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = CustomerSerializer.Meta.fields + ('send_email', 'password')
|
||||
fields = CustomerSerializer.Meta.fields + ('send_email',)
|
||||
|
||||
|
||||
class MembershipTypeSerializer(I18nAwareModelSerializer):
|
||||
@@ -119,21 +113,20 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if 'secret' in data:
|
||||
s = data['secret']
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(
|
||||
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||
s = data['secret']
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(
|
||||
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
|
||||
|
||||
class Meta:
|
||||
@@ -289,7 +282,6 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
default_fields = [
|
||||
'customer_accounts',
|
||||
'customer_accounts_native',
|
||||
'customer_accounts_link_by_email',
|
||||
'invoice_regenerate_allowed',
|
||||
'contact_mail',
|
||||
|
||||
@@ -138,7 +138,6 @@ urlpatterns = [
|
||||
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/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"^idempotency_query$", idempotency.IdempotencyQueryView.as_view(), name="idempotency.query"),
|
||||
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.views import APIView
|
||||
|
||||
from pretix import __version__
|
||||
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.devices import Gate, generate_api_token
|
||||
|
||||
@@ -153,24 +151,6 @@ class RevokeKeyView(APIView):
|
||||
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):
|
||||
authentication_classes = (DeviceTokenAuthentication,)
|
||||
|
||||
|
||||
@@ -515,8 +515,8 @@ class CustomerViewSet(viewsets.ModelViewSet):
|
||||
raise MethodNotAllowed("Customers cannot be deleted.")
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer, send_email=False, password=None):
|
||||
customer = serializer.save(organizer=self.request.organizer, password=make_password(password))
|
||||
def perform_create(self, serializer, send_email=False):
|
||||
customer = serializer.save(organizer=self.request.organizer, password=make_password(None))
|
||||
serializer.instance.log_action(
|
||||
'pretix.customer.created',
|
||||
user=self.request.user,
|
||||
@@ -530,7 +530,7 @@ class CustomerViewSet(viewsets.ModelViewSet):
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = CustomerCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||
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)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
|
||||
@@ -23,24 +23,19 @@ import json
|
||||
import logging
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
from django.db import DatabaseError, connection, transaction
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from requests import RequestException
|
||||
|
||||
from pretix.api.models import (
|
||||
WebHook, WebHookCall, WebHookCallRetry, WebHookEventListener,
|
||||
)
|
||||
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
||||
from pretix.api.signals import register_webhook_events
|
||||
from pretix.base.models import LogEntry
|
||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
||||
from pretix.base.signals import periodic_task
|
||||
from pretix.celery_app import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -224,10 +219,6 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.order.expired',
|
||||
_('Order expired'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.expirychanged',
|
||||
_('Order expiry date changed'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.modified',
|
||||
_('Order information changed'),
|
||||
@@ -240,30 +231,10 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.order.changed.*',
|
||||
_('Order changed'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.refund.created',
|
||||
_('Refund of payment created'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.refund.created.externally',
|
||||
_('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(
|
||||
'pretix.event.order.approved',
|
||||
_('Order approved'),
|
||||
@@ -304,22 +275,6 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.subevent.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))
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=60, acks_late=True, autoretry_for=(DatabaseError,),)
|
||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int, retry_count: int = 0):
|
||||
"""
|
||||
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
|
||||
|
||||
@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):
|
||||
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||
with scopes_disabled():
|
||||
webhook = WebHook.objects.get(id=webhook_id)
|
||||
|
||||
with scope(organizer=webhook.organizer), transaction.atomic():
|
||||
with scope(organizer=webhook.organizer):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
types = get_all_webhook_events()
|
||||
event_type = types.get(action_type)
|
||||
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)
|
||||
if payload is None:
|
||||
# Content object deleted?
|
||||
return 'obsolete-payload'
|
||||
return
|
||||
|
||||
t = time.time()
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
webhook.target_url,
|
||||
json=payload,
|
||||
allow_redirects=False,
|
||||
timeout=30,
|
||||
)
|
||||
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,
|
||||
),
|
||||
try:
|
||||
resp = requests.post(
|
||||
webhook.target_url,
|
||||
json=payload,
|
||||
allow_redirects=False
|
||||
)
|
||||
return 'retry-via-db'
|
||||
|
||||
|
||||
@app.task(base=TransactionAwareTask)
|
||||
def manually_retry_all_calls(webhook_id: int):
|
||||
with scopes_disabled():
|
||||
webhook = WebHook.objects.get(id=webhook_id)
|
||||
with scope(organizer=webhook.organizer), transaction.atomic():
|
||||
for whcr in webhook.retries.select_for_update(
|
||||
skip_locked=connection.features.has_select_for_update_skip_locked
|
||||
):
|
||||
send_webhook.apply_async(
|
||||
args=(whcr.logentry_id, whcr.action_type, whcr.webhook_id, whcr.retry_count),
|
||||
)
|
||||
whcr.delete()
|
||||
|
||||
|
||||
@receiver(signal=periodic_task, dispatch_uid='pretixapi_schedule_webhook_retries_on_celery')
|
||||
@scopes_disabled()
|
||||
def schedule_webhook_retries_on_celery(sender, **kwargs):
|
||||
with transaction.atomic():
|
||||
for whcr in WebHookCallRetry.objects.select_for_update(
|
||||
skip_locked=connection.features.has_select_for_update_skip_locked
|
||||
).filter(retry_not_before__lt=now()):
|
||||
send_webhook.apply_async(
|
||||
args=(whcr.logentry_id, whcr.action_type, whcr.webhook_id, whcr.retry_count),
|
||||
)
|
||||
whcr.delete()
|
||||
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()
|
||||
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:
|
||||
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]
|
||||
)
|
||||
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",
|
||||
)
|
||||
@@ -43,7 +43,6 @@ from pretix.base.i18n import (
|
||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||
)
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import (
|
||||
register_html_mail_renderers, register_mail_placeholders,
|
||||
@@ -300,8 +299,7 @@ def get_email_context(**kwargs):
|
||||
kwargs.setdefault("position_or_address", kwargs['position'])
|
||||
if 'order' in kwargs:
|
||||
try:
|
||||
if not kwargs.get('invoice_address'):
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
|
||||
finally:
|
||||
@@ -471,19 +469,6 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||
lambda order, event:
|
||||
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
if order.modify_deadline
|
||||
else '',
|
||||
lambda event: date_format(
|
||||
event.settings.get(
|
||||
'last_order_modification_date', as_type=RelativeDateWrapper
|
||||
).datetime(event).astimezone(event.timezone),
|
||||
'SHORT_DATETIME_FORMAT'
|
||||
) if event.settings.get('last_order_modification_date') else '',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||
lambda event: str(event.location or ''),
|
||||
|
||||
@@ -610,10 +610,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(_('Invoice address name') + ': ' + str(label))
|
||||
headers += [
|
||||
_('Invoice address street'), _('Invoice address ZIP code'), _('Invoice address city'),
|
||||
_('Invoice address country'),
|
||||
pgettext('address', 'Invoice address state'),
|
||||
_('VAT ID'),
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
||||
]
|
||||
headers += [
|
||||
_('Sales channel'), _('Order locale'),
|
||||
|
||||
@@ -51,7 +51,6 @@ from django.core.validators import (
|
||||
)
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Select, widgets
|
||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -87,7 +86,7 @@ from pretix.control.forms import (
|
||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
||||
)
|
||||
from pretix.helpers.countries import (
|
||||
get_cached_countries_class, get_phone_prefixes_sorted_and_localized,
|
||||
CachedCountries, get_phone_prefixes_sorted_and_localized,
|
||||
)
|
||||
from pretix.helpers.escapejson import escapejson_attr
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
@@ -430,7 +429,7 @@ class PortraitImageWidget(UploadedFileWidget):
|
||||
|
||||
def value_from_datadict(self, 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 '{}')
|
||||
return d
|
||||
|
||||
@@ -630,7 +629,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
)
|
||||
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
|
||||
add_fields['country'] = CountryField(
|
||||
countries=get_cached_countries_class(common_names=event.settings.country_names_common),
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||
label=_('Country'),
|
||||
@@ -726,7 +725,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
)
|
||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||
field = CountryField(
|
||||
countries=get_cached_countries_class(common_names=event.settings.country_names_common),
|
||||
countries=CachedCountries,
|
||||
blank=True, null=True, blank_label=' ',
|
||||
).formfield(
|
||||
label=label, required=required,
|
||||
@@ -974,7 +973,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
|
||||
])
|
||||
|
||||
self.fields['country'].choices = get_cached_countries_class(event.settings.country_names_common)()
|
||||
self.fields['country'].choices = CachedCountries()
|
||||
|
||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||
fprefix = self.prefix + '-' if self.prefix else ''
|
||||
|
||||
@@ -533,7 +533,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
tstyledata = [
|
||||
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||
('FONTNAME', (0, 0), (-1, 0), self.font_bold),
|
||||
('FONTNAME', (0, -1), (-1, -1), self.font_bold),
|
||||
('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 (
|
||||
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.models import F, Q
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from pretix.base.banlist import banned
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
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):
|
||||
"""
|
||||
Represents a registered customer of an organizer.
|
||||
"""
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
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(
|
||||
max_length=190,
|
||||
db_index=True,
|
||||
@@ -349,134 +317,3 @@ class AttendeeProfile(models.Model):
|
||||
parts.append(f'{a["field_label"]}: {val}')
|
||||
|
||||
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 = []
|
||||
cardinality = None
|
||||
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:
|
||||
continue
|
||||
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.helpers.database import GroupConcat
|
||||
from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
from pretix.helpers.json import safe_string
|
||||
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")
|
||||
)
|
||||
|
||||
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:
|
||||
"""
|
||||
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")
|
||||
)
|
||||
|
||||
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:
|
||||
"""
|
||||
Returns a formatted string containing the start date and the end date
|
||||
@@ -940,13 +917,11 @@ class Event(EventMixin, LoggedModel):
|
||||
s.object = self
|
||||
s.pk = None
|
||||
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)
|
||||
fname_base = clean_filename(os.path.basename(s.value))
|
||||
|
||||
# TODO: make sure pub is always correct
|
||||
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)
|
||||
s.value = 'file://' + newname
|
||||
|
||||
@@ -33,8 +33,7 @@ class MultiStringField(TextField):
|
||||
'delimiter_found': _('No value can contain the delimiter character.')
|
||||
}
|
||||
|
||||
def __init__(self, verbose_name=None, name=None, delimiter=DELIMITER, **kwargs):
|
||||
self.delimiter = delimiter
|
||||
def __init__(self, verbose_name=None, name=None, **kwargs):
|
||||
super().__init__(verbose_name, name, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
@@ -45,13 +44,13 @@ class MultiStringField(TextField):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return 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:
|
||||
return []
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return self.delimiter + self.delimiter.join(value) + self.delimiter
|
||||
return DELIMITER + DELIMITER.join(value) + DELIMITER
|
||||
elif value is None:
|
||||
if self.null:
|
||||
return None
|
||||
@@ -64,14 +63,14 @@ class MultiStringField(TextField):
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value:
|
||||
return [v for v in value.split(self.delimiter) if v]
|
||||
return [v for v in value.split(DELIMITER) if v]
|
||||
else:
|
||||
return []
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
super().validate(value, model_instance)
|
||||
for l in value:
|
||||
if self.delimiter in l:
|
||||
if DELIMITER in l:
|
||||
raise exceptions.ValidationError(
|
||||
self.error_messages['delimiter_found'],
|
||||
code='delimiter_found',
|
||||
@@ -79,9 +78,9 @@ class MultiStringField(TextField):
|
||||
|
||||
def get_lookup(self, lookup_name):
|
||||
if lookup_name == 'contains':
|
||||
return make_multistring_contains_lookup(self.delimiter)
|
||||
return MultiStringContains
|
||||
elif lookup_name == 'icontains':
|
||||
return make_multistring_icontains_lookup(self.delimiter)
|
||||
return MultiStringIContains
|
||||
elif lookup_name == 'isnull':
|
||||
return builtin_lookups.IsNull
|
||||
raise NotImplementedError(
|
||||
@@ -89,22 +88,18 @@ class MultiStringField(TextField):
|
||||
)
|
||||
|
||||
|
||||
def make_multistring_contains_lookup(delimiter):
|
||||
class Cls(builtin_lookups.Contains):
|
||||
def process_rhs(self, qn, connection):
|
||||
sql, params = super().process_rhs(qn, connection)
|
||||
params[0] = "%" + delimiter + params[0][1:-1] + delimiter + "%"
|
||||
return sql, params
|
||||
return Cls
|
||||
class MultiStringContains(builtin_lookups.Contains):
|
||||
def process_rhs(self, qn, connection):
|
||||
sql, params = super().process_rhs(qn, connection)
|
||||
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
|
||||
return sql, params
|
||||
|
||||
|
||||
def make_multistring_icontains_lookup(delimiter):
|
||||
class Cls(builtin_lookups.IContains):
|
||||
def process_rhs(self, qn, connection):
|
||||
sql, params = super().process_rhs(qn, connection)
|
||||
params[0] = "%" + delimiter + params[0][1:-1] + delimiter + "%"
|
||||
return sql, params
|
||||
return Cls
|
||||
class MultiStringIContains(builtin_lookups.IContains):
|
||||
def process_rhs(self, qn, connection):
|
||||
sql, params = super().process_rhs(qn, connection)
|
||||
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
|
||||
return sql, params
|
||||
|
||||
|
||||
class MultiStringSerializer(serializers.Field):
|
||||
|
||||
@@ -46,7 +46,6 @@ from django.utils.translation import pgettext
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
from pretix.base.templatetags.country import country_name
|
||||
from pretix.helpers.countries import FastCountryField
|
||||
|
||||
|
||||
@@ -182,7 +181,7 @@ class Invoice(models.Model):
|
||||
self.invoice_from_name,
|
||||
self.invoice_from,
|
||||
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
||||
country_name(self.invoice_from_country, self.event.settings.country_names_common),
|
||||
self.invoice_from_country.name if self.invoice_from_country else "",
|
||||
pgettext("invoice", "VAT-ID: %s") % self.invoice_from_vat_id if self.invoice_from_vat_id else "",
|
||||
taxidrow,
|
||||
]
|
||||
@@ -194,7 +193,7 @@ class Invoice(models.Model):
|
||||
self.invoice_from_name,
|
||||
self.invoice_from,
|
||||
(self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""),
|
||||
country_name(self.invoice_from_country, self.event.settings.country_names_common),
|
||||
self.invoice_from_country.name if self.invoice_from_country else "",
|
||||
]
|
||||
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
||||
|
||||
@@ -220,7 +219,7 @@ class Invoice(models.Model):
|
||||
self.invoice_to_name,
|
||||
self.invoice_to_street,
|
||||
((self.invoice_to_zipcode or "") + " " + (self.invoice_to_city or "") + " " + (state_name or "")).strip(),
|
||||
country_name(self.invoice_to_country, self.event.settings.country_names_common),
|
||||
self.invoice_to_country.name if self.invoice_to_country else "",
|
||||
]
|
||||
return '\n'.join([p.strip() for p in parts if p and p.strip()])
|
||||
|
||||
|
||||
@@ -600,14 +600,10 @@ class Item(LoggedModel):
|
||||
invoice_address=invoice_address,
|
||||
base_price_is='gross',
|
||||
currency=currency)
|
||||
if not self.tax_rule:
|
||||
compare_price = TaxedPrice(gross=b.designated_price * b.count, net=b.designated_price * b.count,
|
||||
tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
else:
|
||||
compare_price = self.tax_rule.tax(b.designated_price * b.count,
|
||||
override_tax_rate=override_tax_rate,
|
||||
invoice_address=invoice_address,
|
||||
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.tax += bprice.tax - compare_price.tax
|
||||
t.name = "MIXED!"
|
||||
@@ -1329,7 +1325,6 @@ class Question(LoggedModel):
|
||||
verbose_name = _("Question")
|
||||
verbose_name_plural = _("Questions")
|
||||
ordering = ('position', 'id')
|
||||
unique_together = (('event', 'identifier'),)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.question)
|
||||
@@ -1345,7 +1340,7 @@ class Question(LoggedModel):
|
||||
@staticmethod
|
||||
def _clean_identifier(event, code, instance=None):
|
||||
qs = Question.objects.filter(event=event, identifier__iexact=code)
|
||||
if instance and instance.pk:
|
||||
if instance:
|
||||
qs = qs.exclude(pk=instance.pk)
|
||||
if qs.exists():
|
||||
raise ValidationError(_('This identifier is already used for a different question.'))
|
||||
|
||||
@@ -268,10 +268,7 @@ class Order(LockModel, LoggedModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'require_approval' not in self.get_deferred_fields() and 'status' not in self.get_deferred_fields():
|
||||
self._transaction_key_reset()
|
||||
|
||||
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
|
||||
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):
|
||||
from . import GiftCard, GiftCardTransaction, Membership, Voucher
|
||||
@@ -749,19 +746,6 @@ class Order(LockModel, LoggedModel):
|
||||
length += 1
|
||||
iteration = 0
|
||||
|
||||
@property
|
||||
def modify_deadline(self):
|
||||
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
if self.event.has_subevents and modify_deadline:
|
||||
dates = [
|
||||
modify_deadline.datetime(se)
|
||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||
]
|
||||
return min(dates) if dates else None
|
||||
elif modify_deadline:
|
||||
return modify_deadline.datetime(self.event)
|
||||
return None
|
||||
|
||||
@property
|
||||
def can_modify_answers(self) -> bool:
|
||||
"""
|
||||
@@ -774,7 +758,16 @@ class Order(LockModel, LoggedModel):
|
||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
|
||||
modify_deadline = self.modify_deadline
|
||||
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
if self.event.has_subevents and modify_deadline:
|
||||
dates = [
|
||||
modify_deadline.datetime(se)
|
||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||
]
|
||||
modify_deadline = min(dates) if dates else None
|
||||
elif modify_deadline:
|
||||
modify_deadline = modify_deadline.datetime(self.event)
|
||||
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
return False
|
||||
|
||||
@@ -1059,14 +1052,12 @@ class Order(LockModel, LoggedModel):
|
||||
if p.canceled and not _backfill_before_cancellation:
|
||||
continue
|
||||
target_transaction_count[Transaction.key(p)] += 1
|
||||
p._transaction_key_reset()
|
||||
|
||||
fees = self.fees.all() if fees is None else fees
|
||||
for f in fees:
|
||||
if f.canceled and not _backfill_before_cancellation:
|
||||
continue
|
||||
target_transaction_count[Transaction.key(f)] += 1
|
||||
f._transaction_key_reset()
|
||||
|
||||
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
|
||||
create = []
|
||||
@@ -1093,7 +1084,6 @@ class Order(LockModel, LoggedModel):
|
||||
create.sort(key=lambda t: (0 if t.count < 0 else 1, t.positionid or 0))
|
||||
if save:
|
||||
Transaction.objects.bulk_create(create)
|
||||
self._transaction_key_reset()
|
||||
_transactions_mark_order_clean(self.pk)
|
||||
return create
|
||||
|
||||
@@ -2082,20 +2072,8 @@ class OrderFee(models.Model):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.get_deferred_fields():
|
||||
self._transaction_key_reset()
|
||||
|
||||
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
|
||||
self.__initial_transaction_key = Transaction.key(self)
|
||||
self.__initial_canceled = self.canceled
|
||||
|
||||
def __str__(self):
|
||||
if self.description:
|
||||
@@ -2215,20 +2193,8 @@ class OrderPosition(AbstractPosition):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.get_deferred_fields():
|
||||
self._transaction_key_reset()
|
||||
|
||||
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
|
||||
self.__initial_transaction_key = Transaction.key(self)
|
||||
self.__initial_canceled = self.canceled
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order position")
|
||||
|
||||
@@ -114,7 +114,7 @@ EU_CURRENCIES = {
|
||||
'RO': 'RON',
|
||||
'SE': 'SEK'
|
||||
}
|
||||
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH', 'NO'}
|
||||
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH'}
|
||||
|
||||
|
||||
def is_eu_country(cc):
|
||||
|
||||
@@ -392,7 +392,7 @@ class InvoiceAddressCountry(ImportColumn):
|
||||
return list(countries)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and not (Country(value).numeric or value in settings.COUNTRIES_OVERRIDE):
|
||||
if value and not Country(value).numeric:
|
||||
raise ValidationError(_("Please enter a valid country code."))
|
||||
return value
|
||||
|
||||
@@ -538,7 +538,7 @@ class AttendeeCountry(ImportColumn):
|
||||
return list(countries)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and not (Country(value).numeric or value in settings.COUNTRIES_OVERRIDE):
|
||||
if value and not Country(value).numeric:
|
||||
raise ValidationError(_("Please enter a valid country code."))
|
||||
return value
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from PyPDF2 import PdfReader
|
||||
from PyPDF2 import PdfFileReader
|
||||
from pytz import timezone
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
@@ -251,11 +251,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"editor_sample": _("20:00"),
|
||||
"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", {
|
||||
"label": _("Event end date and time"),
|
||||
"editor_sample": _("2017-05-31 22:00"),
|
||||
@@ -280,11 +275,6 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"TIME_FORMAT"
|
||||
) 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", {
|
||||
"label": _("Event admission date and time"),
|
||||
"editor_sample": _("2017-05-31 19:00"),
|
||||
@@ -656,7 +646,7 @@ class Renderer:
|
||||
self.event = event
|
||||
if self.background_file:
|
||||
self.bg_bytes = self.background_file.read()
|
||||
self.bg_pdf = PdfReader(BytesIO(self.bg_bytes), strict=False)
|
||||
self.bg_pdf = PdfFileReader(BytesIO(self.bg_bytes), strict=False)
|
||||
else:
|
||||
self.bg_bytes = None
|
||||
self.bg_pdf = None
|
||||
@@ -871,7 +861,7 @@ class Renderer:
|
||||
canvas.restoreState()
|
||||
|
||||
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition, show_page=True, only_page=None):
|
||||
page_count = len(self.bg_pdf.pages)
|
||||
page_count = self.bg_pdf.getNumPages()
|
||||
|
||||
if not only_page and not show_page:
|
||||
raise ValueError("only_page=None and show_page=False cannot be combined")
|
||||
@@ -891,7 +881,7 @@ class Renderer:
|
||||
elif o['type'] == "poweredby":
|
||||
self._draw_poweredby(canvas, op, o)
|
||||
if self.bg_pdf:
|
||||
canvas.setPageSize((self.bg_pdf.pages[0].mediabox[2], self.bg_pdf.pages[0].mediabox[3]))
|
||||
canvas.setPageSize((self.bg_pdf.getPage(page).mediaBox[2], self.bg_pdf.getPage(page).mediaBox[3]))
|
||||
if show_page:
|
||||
canvas.showPage()
|
||||
|
||||
@@ -915,17 +905,17 @@ class Renderer:
|
||||
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
|
||||
return BytesIO(f.read())
|
||||
else:
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfReader(buffer)
|
||||
output = PdfWriter()
|
||||
new_pdf = PdfFileReader(buffer)
|
||||
output = PdfFileWriter()
|
||||
|
||||
for i, page in enumerate(new_pdf.pages):
|
||||
bg_page = copy.copy(self.bg_pdf.pages[i])
|
||||
bg_page.merge_page(page)
|
||||
output.add_page(bg_page)
|
||||
bg_page = copy.copy(self.bg_pdf.getPage(i))
|
||||
bg_page.mergePage(page)
|
||||
output.addPage(bg_page)
|
||||
|
||||
output.add_metadata({
|
||||
output.addMetadata({
|
||||
'/Title': str(title),
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
|
||||
@@ -28,7 +28,6 @@ from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import CachedCombinedTicket, CachedTicket
|
||||
from pretix.base.models.customers import CustomerSSOGrant
|
||||
|
||||
from ..models import CachedFile, CartPosition, InvoiceAddress
|
||||
from ..signals import periodic_task
|
||||
@@ -69,9 +68,3 @@ def clean_cached_tickets(sender, **kwargs):
|
||||
@scopes_disabled()
|
||||
def clearsessions(sender, **kwargs):
|
||||
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()
|
||||
|
||||
@@ -62,7 +62,6 @@ from pretix.base.models.tax import EU_CURRENCIES
|
||||
from pretix.base.services.tasks import TransactionAwareTask
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.base.signals import invoice_line_text, periodic_task
|
||||
from pretix.base.templatetags.country import country_name
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -123,7 +122,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
invoice.invoice_to = "\n".join(
|
||||
a.strip() for a in addr_template.format(
|
||||
i=ia,
|
||||
country=country_name(ia.country, invoice.event.settings.country_names_common) if ia.country else ia.country_old,
|
||||
country=ia.country.name if ia.country else ia.country_old,
|
||||
state=ia.state_for_address
|
||||
).split("\n") if a.strip()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
: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 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.
|
||||
|
||||
@@ -502,11 +502,11 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
rc.expire(redis_key, 300)
|
||||
|
||||
max_retries = 10
|
||||
retry_after = min(30 + cnt * 10, 1800)
|
||||
retry_after = 30 + cnt * 10
|
||||
else:
|
||||
# Most likely some other kind of temporary failure, retry again (but pretty soon)
|
||||
max_retries = 5
|
||||
retry_after = [10, 30, 60, 300, 900][self.request.retries]
|
||||
retry_after = 2 ** (self.request.retries * 3) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
|
||||
|
||||
try:
|
||||
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):
|
||||
# Not a permanent failure (mailbox full, service unavailable), retry later, but with large intervals
|
||||
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:
|
||||
# ignore and go on with logging the error
|
||||
pass
|
||||
@@ -567,7 +567,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
except Exception as e:
|
||||
if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)):
|
||||
try:
|
||||
self.retry(max_retries=5, countdown=[10, 30, 60, 300, 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:
|
||||
if log_target:
|
||||
log_target.log_action(
|
||||
|
||||
@@ -36,7 +36,6 @@ from pretix.base.models import (
|
||||
from pretix.base.models.orders import Transaction
|
||||
from pretix.base.orderimport import get_all_columns
|
||||
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.signals import order_paid, order_placed
|
||||
from pretix.celery_app import app
|
||||
@@ -86,9 +85,9 @@ def setif(record, obj, attr, setting):
|
||||
|
||||
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
|
||||
def import_orders(event: Event, fileid: str, settings: dict, locale: str, user) -> None:
|
||||
# TODO: quotacheck?
|
||||
cf = CachedFile.objects.get(id=fileid)
|
||||
user = User.objects.get(pk=user)
|
||||
seats_used = False
|
||||
with language(locale, event.settings.region):
|
||||
cols = get_all_columns(event)
|
||||
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.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
||||
position.meta_info = {}
|
||||
if position.seat is not None:
|
||||
seats_used = True
|
||||
order._positions.append(position)
|
||||
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))
|
||||
)
|
||||
|
||||
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats
|
||||
# are in use
|
||||
lockfn = event.lock if seats_used else NoLockManager
|
||||
|
||||
try:
|
||||
with lockfn(), transaction.atomic():
|
||||
# quota check?
|
||||
with event.lock():
|
||||
with transaction.atomic():
|
||||
save_transactions = []
|
||||
for o in orders:
|
||||
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()
|
||||
if gen_invoice:
|
||||
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()
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from xml.etree import ElementTree
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import requests
|
||||
import vat_moss.errors
|
||||
import vat_moss.id
|
||||
from django.conf import settings
|
||||
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
|
||||
|
||||
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):
|
||||
@@ -60,107 +50,33 @@ class VATIDTemporaryError(VATIDError):
|
||||
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):
|
||||
# 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):
|
||||
raise VATIDFinalError(error_messages['country_mismatch'])
|
||||
|
||||
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)
|
||||
raise VATIDFinalError(_('Your VAT ID does not match the selected country.'))
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://ec.europa.eu/taxation_customs/vies/services/checkVatService',
|
||||
data=payload,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
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:
|
||||
result = vat_moss.id.validate(vat_id)
|
||||
if result:
|
||||
country_code, normalized_id, company_name = result
|
||||
return normalized_id
|
||||
except (vat_moss.errors.InvalidError, ValueError):
|
||||
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
|
||||
except vat_moss.errors.WebServiceUnavailableError:
|
||||
logger.exception('VAT ID checking failed for country {}'.format(country_code))
|
||||
raise VATIDTemporaryError(error_messages['unavailable'])
|
||||
else:
|
||||
return vat_id
|
||||
raise VATIDTemporaryError(_(
|
||||
'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.'
|
||||
))
|
||||
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):
|
||||
@@ -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', ''))
|
||||
try:
|
||||
transport = Transport(
|
||||
cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")),
|
||||
timeout=10
|
||||
)
|
||||
transport = Transport(cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")))
|
||||
client = Client(
|
||||
'https://www.uid-wse.admin.ch/V5.0/PublicServices.svc?wsdl',
|
||||
transport=transport,
|
||||
transport=transport
|
||||
)
|
||||
result = client.service.ValidateUID(uid=vat_id)
|
||||
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):
|
||||
if not vat_id:
|
||||
return vat_id
|
||||
country_code = str(country_code)
|
||||
if is_eu_country(country_code):
|
||||
return _validate_vat_id_EU(vat_id, country_code)
|
||||
elif country_code == 'CH':
|
||||
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}')
|
||||
|
||||
@@ -57,7 +57,6 @@ from django_countries.fields import Country
|
||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from phonenumbers import PhoneNumber, parse
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.fields import (
|
||||
@@ -146,17 +145,6 @@ DEFAULTS = {
|
||||
"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': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -2604,14 +2592,6 @@ Your {organizer} team"""))
|
||||
help_text=_('This will be displayed on the organizer homepage.')
|
||||
)
|
||||
},
|
||||
'country_names_common': {
|
||||
# Hidden setting to toggle things like ("Taiwan" vs "Taiwan (Province of China)" or "Russia" vs
|
||||
# "Russian Federation". Can be turned to False on request for diplomacy-sensitive users.
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'serializer_kwargs': {},
|
||||
},
|
||||
'name_scheme': {
|
||||
'default': 'full', # default for new events is 'given_family'
|
||||
'type': str,
|
||||
@@ -3050,7 +3030,6 @@ settings_hierarkey.add_type(LazyI18nStringList,
|
||||
settings_hierarkey.add_type(RelativeDateWrapper,
|
||||
serialize=lambda rdw: rdw.to_string(),
|
||||
unserialize=lambda s: RelativeDateWrapper.from_string(s))
|
||||
settings_hierarkey.add_type(PhoneNumber, lambda pn: pn.as_international, lambda s: parse(s))
|
||||
|
||||
|
||||
@settings_hierarkey.set_global(cache_namespace='global')
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Unknown host" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-question-circle-o fa-fw big-icon"></i>
|
||||
<div class="error-details">
|
||||
<h1>{% trans "Unknown host" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed with host=header_host %}
|
||||
Your browser told us that you want to access "{{ header_host }}". Unfortunately, we don't have
|
||||
any content for this domain.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if is_fresh_install %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
It looks like this is a fresh installation of pretix. This error message is probably caused due to
|
||||
the fact that either your configuration includes the wrong site URL or your reverse proxy is sending
|
||||
the wrong header.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<dl>
|
||||
<dt>{% trans "Expected host according to configuration" %}</dt>
|
||||
<dd><code>{{ site_host }}</code></dd>
|
||||
<dt>{% trans "Received headers" %}</dt>
|
||||
<dd>
|
||||
<code>Host: {{ request.headers.Host }}</code>
|
||||
{% if xfh %}
|
||||
<br>
|
||||
<code>X-Forwarded-For: {{ xfh }}</code>
|
||||
{% if not settings.USE_X_FORWARDED_HOST %}({% trans "ignored" %}){% endif %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Derived host from headers" %}</dt>
|
||||
<dd><code>{{ header_host }}</code></dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you just configured this as a domain for your ticket shop, you now need to set this up as a "custom domain"
|
||||
in your organizer account.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="links">
|
||||
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
|
||||
· <a id='reload' href='#'>{% trans "Try again" %}</a>
|
||||
</p>
|
||||
<img src="{% static "pretixbase/img/pretix-logo.svg" %}" class="logo"/>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -199,7 +199,7 @@
|
||||
<tr>
|
||||
<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="
|
||||
style="max-height: 60px;" alt="">
|
||||
style="max-height: 60px;">
|
||||
</td>
|
||||
</tr>
|
||||
<!--<![endif]-->
|
||||
@@ -233,7 +233,7 @@
|
||||
<td style="line-height: 0">
|
||||
<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="
|
||||
style="max-height: 60px;" alt="">
|
||||
style="max-height: 60px;">
|
||||
</td>
|
||||
</tr>
|
||||
<!--<![endif]-->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<td>
|
||||
<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"
|
||||
style="max-height: 4px;" alt="">
|
||||
style="max-height: 4px;">
|
||||
<div style="line-height: 18px;height: 18px;"> </div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from django import template
|
||||
from django_countries.fields import Country
|
||||
|
||||
from pretix.helpers.countries import get_cached_countries_class
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter("country_name")
|
||||
def country_name(country: Country, common_names=True):
|
||||
if not country:
|
||||
return ''
|
||||
if not isinstance(country, Country):
|
||||
country = Country(country)
|
||||
|
||||
klass = get_cached_countries_class(common_names=common_names)
|
||||
return klass().name(country.code)
|
||||
@@ -216,8 +216,7 @@ class AsyncFormView(AsyncMixin, FormView):
|
||||
task_base = ProfiledEventTask
|
||||
|
||||
def __init_subclass__(cls):
|
||||
def async_execute(self, *, request_path, query_string, form_kwargs, locale, tz, url_kwargs=None, url_args=None,
|
||||
organizer=None, event=None, user=None, session_key=None):
|
||||
def async_execute(self, *, request_path, query_string, form_kwargs, locale, tz, organizer=None, event=None, user=None, session_key=None):
|
||||
view_instance = cls()
|
||||
form_kwargs['data'] = QueryDict(form_kwargs['data'])
|
||||
req = RequestFactory().post(
|
||||
@@ -226,8 +225,6 @@ class AsyncFormView(AsyncMixin, FormView):
|
||||
content_type='application/x-www-form-urlencoded'
|
||||
)
|
||||
view_instance.request = req
|
||||
view_instance.kwargs = url_kwargs
|
||||
view_instance.args = url_args
|
||||
if event:
|
||||
view_instance.request.event = event
|
||||
view_instance.request.organizer = event.organizer
|
||||
@@ -287,8 +284,6 @@ class AsyncFormView(AsyncMixin, FormView):
|
||||
'request_path': self.request.path,
|
||||
'query_string': self.request.GET.urlencode(),
|
||||
'form_kwargs': form_kwargs,
|
||||
'url_args': self.args,
|
||||
'url_kwargs': self.kwargs,
|
||||
'locale': get_language(),
|
||||
'tz': get_current_timezone().zone,
|
||||
}
|
||||
@@ -341,8 +336,6 @@ class AsyncPostView(AsyncMixin, View):
|
||||
content_type='application/x-www-form-urlencoded'
|
||||
)
|
||||
view_instance.request = req
|
||||
view_instance.kwargs = url_kwargs
|
||||
view_instance.args = url_args
|
||||
if event:
|
||||
view_instance.request.event = event
|
||||
view_instance.request.organizer = event.organizer
|
||||
|
||||
@@ -71,7 +71,7 @@ def _default_context(request):
|
||||
except Resolver404:
|
||||
return {}
|
||||
|
||||
if not request.path.startswith(get_script_prefix() + 'control') or not hasattr(request, 'user'):
|
||||
if not request.path.startswith(get_script_prefix() + 'control'):
|
||||
return {}
|
||||
ctx = {
|
||||
'url_name': url.url_name,
|
||||
|
||||
@@ -48,8 +48,6 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
|
||||
from ...base.forms import I18nModelForm
|
||||
|
||||
# Import for backwards compatibility with okd import paths
|
||||
@@ -129,7 +127,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
||||
def __str__(self):
|
||||
if hasattr(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
|
||||
def url(self):
|
||||
|
||||
@@ -39,7 +39,7 @@ from urllib.parse import urlencode, urlparse
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, validate_email
|
||||
from django.core.validators import validate_email
|
||||
from django.db.models import Prefetch, Q, prefetch_related_objects
|
||||
from django.forms import (
|
||||
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
|
||||
@@ -848,7 +848,6 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
self.fields['invoice_generate_sales_channels'].choices = (
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
)
|
||||
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
|
||||
@@ -129,11 +129,6 @@ class QuestionForm(I18nModelForm):
|
||||
|
||||
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):
|
||||
d = super().clean()
|
||||
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.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.questions import (
|
||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget, get_country_by_locale,
|
||||
@@ -62,7 +61,6 @@ from pretix.base.models import (
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
|
||||
MembershipType, Organizer, Team,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.organizer import OrganizerFooterLink
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
|
||||
from pretix.control.forms import ExtFileField, SplitDateTimeField
|
||||
@@ -161,7 +159,7 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
instance = super().save(commit)
|
||||
|
||||
if self.domain:
|
||||
current_domain = instance.domains.filter(event__isnull=True).first()
|
||||
current_domain = instance.domains.first()
|
||||
if self.cleaned_data['domain']:
|
||||
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
|
||||
current_domain.delete()
|
||||
@@ -356,7 +354,6 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
auto_fields = [
|
||||
'allowed_restricted_plugins',
|
||||
'customer_accounts',
|
||||
'customer_accounts_native',
|
||||
'customer_accounts_link_by_email',
|
||||
'invoice_regenerate_allowed',
|
||||
'contact_mail',
|
||||
@@ -634,10 +631,6 @@ class CustomerUpdateForm(forms.ModelForm):
|
||||
titles=self.instance.organizer.settings.name_scheme_titles,
|
||||
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):
|
||||
email = self.cleaned_data.get('email')
|
||||
@@ -713,120 +706,3 @@ OrganizerFooterLinkFormset = inlineformset_factory(
|
||||
formset=BaseOrganizerFooterLinkFormSet,
|
||||
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.webhook.created': _('The webhook has been created.'),
|
||||
'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.changed': _('The membership type has been changed.'),
|
||||
'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,
|
||||
}
|
||||
)
|
||||
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:
|
||||
nav.append({
|
||||
'label': _('Customer accounts'),
|
||||
|
||||
@@ -72,10 +72,10 @@
|
||||
</div>
|
||||
{% 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>
|
||||
{% for field in filter_form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
<input name="returnquery" type="hidden" value="{{ request.META.QUERY_STRING }}">
|
||||
<div class="hidden">
|
||||
{{ filter_form.as_p }}
|
||||
<input name="returnquery" type="hidden" value="{{ request.META.QUERY_STRING }}">
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
|
||||
@@ -207,16 +207,14 @@
|
||||
{% bootstrap_field sform.logo_show_title layout="control" %}
|
||||
{% bootstrap_field sform.og_image layout="control" %}
|
||||
{% 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_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.theme_color_success layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_background layout="control" %}
|
||||
{% bootstrap_field sform.theme_round_borders layout="control" %}
|
||||
{% bootstrap_field sform.primary_font layout="control" %}
|
||||
{% endpropagated %}
|
||||
{% endwith %}
|
||||
{% propagated request.event org_url "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.theme_color_success layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_danger layout="control" %}
|
||||
{% bootstrap_field sform.theme_color_background layout="control" %}
|
||||
{% bootstrap_field sform.theme_round_borders layout="control" %}
|
||||
{% bootstrap_field sform.primary_font layout="control" %}
|
||||
{% endpropagated %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Timeline" %}</legend>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
{% load country %}
|
||||
{% load rich_text %}
|
||||
{% load safelink %}
|
||||
{% load eventsignal %}
|
||||
@@ -243,7 +242,7 @@
|
||||
<dt>{% trans "Invoices" %}</dt>
|
||||
<dd>
|
||||
{% 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 %}
|
||||
{{ i.number }}</a>
|
||||
({{ i.date|date:"SHORT_DATE_FORMAT" }})
|
||||
@@ -499,7 +498,7 @@
|
||||
{% if line.street or line.zipcode or line.city or line.country %}
|
||||
{{ line.street|default_if_none:""|linebreaksbr }}<br>
|
||||
{{ line.zipcode|default_if_none:"" }} {{ line.city|default_if_none:"" }}<br>
|
||||
{{ line.country|country_name:request.event.settings.country_names_common }}
|
||||
{{ line.country.name|default_if_none:"" }}
|
||||
{% if line.state %}<br>{{ line.state }}{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
@@ -890,9 +889,7 @@
|
||||
<dt>{% trans "ZIP code and city" %}</dt>
|
||||
<dd>{{ order.invoice_address.zipcode }} {{ order.invoice_address.city }}</dd>
|
||||
<dt>{% trans "Country" %}</dt>
|
||||
<dd>
|
||||
{{ order.invoice_address.country|country_name:request.event.settings.country_names_common|default:order.invoice_address.country_old }}
|
||||
</dd>
|
||||
<dd>{{ order.invoice_address.country.name|default:order.invoice_address.country_old }}</dd>
|
||||
{% if order.invoice_address.state %}
|
||||
<dt>{% trans "State" context "address" %}</dt>
|
||||
<dd>{{ order.invoice_address.state_name }}</dd>
|
||||
|
||||
@@ -27,14 +27,10 @@
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Customer ID" %}</dt>
|
||||
<dd>#{{ customer.identifier }}</dd>
|
||||
{% if customer.provider %}
|
||||
<dt>{% trans "SSO provider" %}</dt>
|
||||
<dd>{{ customer.provider.name }}</dd>
|
||||
{% endif %}
|
||||
{% if customer.external_identifier %}
|
||||
<dt>{% trans "External identifier" %}</dt>
|
||||
<dd>{{ customer.external_identifier }}</dd>
|
||||
{% endif %}
|
||||
{% if customer.external_identifier %}
|
||||
<dt>{% trans "External identifier" %}</dt>
|
||||
<dd>{{ customer.external_identifier }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if not customer.is_active %}
|
||||
@@ -48,7 +44,7 @@
|
||||
<dt>{% trans "E-mail" %}</dt>
|
||||
<dd>
|
||||
{{ 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">
|
||||
{% trans "Send password reset link" %}
|
||||
</button>
|
||||
|
||||
@@ -57,9 +57,9 @@
|
||||
</p>
|
||||
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in filter_form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
<div class="hidden">
|
||||
{{ filter_form.as_p }}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-quotas">
|
||||
<thead>
|
||||
|
||||
@@ -131,7 +131,6 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Customer accounts" %}</legend>
|
||||
{% 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.name_scheme 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>
|
||||
{% trans "This page shows all calls to your webhook in the past 30 days." %}
|
||||
</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 %}
|
||||
<details class="panel panel-default">
|
||||
<summary class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||
{% if c.is_retry %}
|
||||
<span class="fa fa-repeat fa-fw" data-toggle="tooltip"
|
||||
title="{% trans "This webhook was retried since it previously failed." %}"></span>
|
||||
<span class="fa fa-repeat fa-fw" data-toggle="tooltip" title="{% trans "This webhook was retried since it previously failed." %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
{% endif %}
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
{% endif %}
|
||||
<form action="{% url "control:event.subevents.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in filter_form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
<div class="hidden">
|
||||
{{ filter_form.as_p }}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
|
||||
@@ -55,7 +55,7 @@ class PropagatedNode(Node):
|
||||
</div>
|
||||
<div class="panel-body help-text">
|
||||
{text_expl}<br>
|
||||
<a href="{url}" target="_blank" class="btn btn-default">
|
||||
<a href="{url}" target="_blank">
|
||||
{text_orga}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -133,20 +133,6 @@ urlpatterns = [
|
||||
name='organizer.membershiptype.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/membershiptype/(?P<type>[^/]+)/delete$', organizer.MembershipTypeDeleteView.as_view(),
|
||||
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/select2$', typeahead.customer_select2, name='organizer.customers.select2'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/add$',
|
||||
|
||||
@@ -1210,8 +1210,7 @@ class OrderTransition(OrderView):
|
||||
OrderPayment.PAYMENT_STATE_CREATED)):
|
||||
try:
|
||||
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', {
|
||||
'local_id': p.local_id,
|
||||
'provider': p.provider,
|
||||
|
||||
@@ -62,7 +62,6 @@ from django.views.generic import (
|
||||
)
|
||||
|
||||
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.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import language
|
||||
@@ -71,7 +70,6 @@ from pretix.base.models import (
|
||||
Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer,
|
||||
Team, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
||||
from pretix.base.models.giftcards import (
|
||||
GiftCardTransaction, gen_giftcard_secret,
|
||||
@@ -95,8 +93,7 @@ from pretix.control.forms.organizer import (
|
||||
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, SSOClientForm, SSOProviderForm,
|
||||
TeamForm, WebHookForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
from pretix.control.permissions import (
|
||||
@@ -1255,7 +1252,6 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['webhook'] = self.webhook
|
||||
ctx['retry_count'] = self.webhook.retries.count()
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
@@ -1267,25 +1263,6 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
def get_queryset(self):
|
||||
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):
|
||||
model = GiftCard
|
||||
@@ -1926,247 +1903,6 @@ class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequ
|
||||
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):
|
||||
model = Customer
|
||||
template_name = 'pretixcontrol/organizers/customers.html'
|
||||
@@ -2212,7 +1948,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
)
|
||||
|
||||
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)
|
||||
ctx = self.customer.get_email_context()
|
||||
token = TokenGenerator().make_token(self.customer)
|
||||
|
||||
@@ -39,8 +39,8 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import TemplateView
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
from PyPDF2.errors import PdfReadError
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
from PyPDF2.utils import PdfReadError
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
from pretix.base.i18n import language
|
||||
@@ -153,9 +153,9 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "emptybackground" in request.POST:
|
||||
p = PdfWriter()
|
||||
p = PdfFileWriter()
|
||||
try:
|
||||
p.add_blank_page(
|
||||
p.addBlankPage(
|
||||
width=float(request.POST.get('width')) * mm,
|
||||
height=float(request.POST.get('height')) * mm,
|
||||
)
|
||||
@@ -203,7 +203,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
try:
|
||||
bg_bytes = c.file.read()
|
||||
PdfReader(BytesIO(bg_bytes), strict=False)
|
||||
PdfFileReader(BytesIO(bg_bytes), strict=False)
|
||||
except PdfReadError as e:
|
||||
return JsonResponse({
|
||||
"status": "error",
|
||||
|
||||
@@ -228,9 +228,9 @@ def nav_context_list(request):
|
||||
if query:
|
||||
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
|
||||
|
||||
if query and len(query) >= 3:
|
||||
if query:
|
||||
qs_orders = Order.objects.filter(
|
||||
code__istartswith=query
|
||||
code__icontains=query
|
||||
).select_related('event', 'event__organizer').only('event', 'code', 'pk').order_by()
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
qs_orders = qs_orders.filter(
|
||||
@@ -241,7 +241,7 @@ def nav_context_list(request):
|
||||
)
|
||||
|
||||
qs_vouchers = Voucher.objects.filter(
|
||||
code__istartswith=query
|
||||
code__icontains=query
|
||||
).select_related('event', 'event__organizer').only('event', 'code', 'pk').order_by()
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
qs_vouchers = qs_vouchers.filter(
|
||||
|
||||
@@ -23,8 +23,7 @@ import pyuca
|
||||
from babel.core import Locale
|
||||
from django.core.cache import cache
|
||||
from django.utils import translation
|
||||
from django.utils.encoding import force_str
|
||||
from django_countries import Countries, CountryTuple
|
||||
from django_countries import Countries
|
||||
from django_countries.fields import CountryField
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
|
||||
@@ -36,7 +35,6 @@ _collator = pyuca.Collator()
|
||||
class CachedCountries(Countries):
|
||||
_cached_lists = {}
|
||||
cache_subkey = None
|
||||
common_names = True
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
@@ -44,7 +42,7 @@ class CachedCountries(Countries):
|
||||
django-countries performs a unicode-aware sorting based on pyuca which is incredibly
|
||||
slow.
|
||||
"""
|
||||
cache_key = "countries:all:{}:{}".format(get_language_without_region(), self.common_names)
|
||||
cache_key = "countries:all:{}".format(get_language_without_region())
|
||||
if self.cache_subkey:
|
||||
cache_key += ":" + self.cache_subkey
|
||||
if cache_key in self._cached_lists:
|
||||
@@ -62,33 +60,6 @@ class CachedCountries(Countries):
|
||||
cache.set(cache_key, val, 3600 * 24 * 30)
|
||||
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 CachedCountriesOfficialNames(CachedCountries):
|
||||
common_names = False
|
||||
|
||||
|
||||
def get_cached_countries_class(common_names=True):
|
||||
if common_names:
|
||||
return CachedCountries
|
||||
else:
|
||||
return CachedCountriesOfficialNames
|
||||
|
||||
|
||||
class FastCountryField(CountryField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-19 16:32+0000\n"
|
||||
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -486,48 +486,48 @@ msgstr "الدقائق"
|
||||
msgid "Check-in 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:"
|
||||
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Group of objects"
|
||||
msgstr "مجموعة من العناصر"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Text object"
|
||||
msgstr "عنصر نص"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Barcode area"
|
||||
msgstr "منطقة باركود"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
msgid "Image area"
|
||||
msgstr "منطقة صورة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Powered by pretix"
|
||||
msgstr "مدعوم من pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
msgid "Object"
|
||||
msgstr "عنصر"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
msgid "Ticket design"
|
||||
msgstr "تصميم التذكرة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
msgid "Saving failed."
|
||||
msgstr "فشلت عملية الحفظ."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
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?"
|
||||
msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟"
|
||||
|
||||
@@ -573,15 +573,15 @@ msgstr "البحث في الاستفسارات"
|
||||
msgid "Selected only"
|
||||
msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||
msgid "Use a different name internally"
|
||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||
msgid "Click to close"
|
||||
msgstr "اضغط لاغلاق الصفحة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "لم تقم بحفظ التعديلات!"
|
||||
|
||||
@@ -655,20 +655,20 @@ msgstr "ستسترد %(currency)%(amount)"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
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."
|
||||
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||
msgid "required"
|
||||
msgstr "مطلوب"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||
msgid "Time zone:"
|
||||
msgstr "المنطقة الزمنية:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||
msgid "Your local time:"
|
||||
msgstr "التوقيت المحلي:"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-19 16:32+0000\n"
|
||||
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -469,48 +469,48 @@ msgstr ""
|
||||
msgid "Check-in QR"
|
||||
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:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Group of objects"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Text object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Barcode area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
msgid "Image area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Powered by pretix"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
msgid "Object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
msgid "Ticket design"
|
||||
msgstr "Disseny del tiquet"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
msgid "Saving failed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
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?"
|
||||
msgstr ""
|
||||
|
||||
@@ -556,15 +556,15 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:966
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:963
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -628,22 +628,22 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Cistella expirada"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-19 16:32+0000\n"
|
||||
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||
"PO-Revision-Date: 2021-12-06 23:00+0000\n"
|
||||
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -483,48 +483,48 @@ msgstr "minuty"
|
||||
msgid "Check-in QR"
|
||||
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:"
|
||||
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"
|
||||
msgstr "Skupina objektů"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Text object"
|
||||
msgstr "Textový objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Barcode area"
|
||||
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"
|
||||
msgstr "Oblast obrazu"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Powered by 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"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
msgid "Ticket design"
|
||||
msgstr "Design vstupenky"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
msgid "Saving failed."
|
||||
msgstr "Uložení se nepodařilo."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
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."
|
||||
|
||||
#: 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?"
|
||||
msgstr "Opravdu chcete opustit editor bez uložení změn?"
|
||||
|
||||
@@ -573,15 +573,15 @@ msgstr "Hledaný výraz"
|
||||
msgid "Selected only"
|
||||
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"
|
||||
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"
|
||||
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!"
|
||||
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."
|
||||
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."
|
||||
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"
|
||||
msgstr "povinný"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||
msgid "Time zone:"
|
||||
msgstr "Časové pásmo:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||
msgid "Your local time:"
|
||||
msgstr "Místní čas:"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-19 16:32+0000\n"
|
||||
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||
"PO-Revision-Date: 2022-04-01 13:36+0000\n"
|
||||
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -511,50 +511,50 @@ msgstr ""
|
||||
msgid "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:"
|
||||
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"
|
||||
msgstr "Gruppe af objekter"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Text object"
|
||||
msgstr "Tekstobjekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-kode-område"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#, fuzzy
|
||||
#| msgid "Barcode area"
|
||||
msgid "Image area"
|
||||
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"
|
||||
msgstr "Drevet af pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
msgid "Ticket design"
|
||||
msgstr "Billetdesign"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
msgid "Saving failed."
|
||||
msgstr "Gem fejlede."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
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?"
|
||||
msgstr ""
|
||||
"Er du sikker på at du vil forlade editoren uden at gemme dine ændringer?"
|
||||
@@ -601,15 +601,15 @@ msgstr ""
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:858
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:886
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:894
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:922
|
||||
msgid "Click to close"
|
||||
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!"
|
||||
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."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:416
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:414
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Kurv udløbet"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||
msgid "Time zone:"
|
||||
msgstr "Tidszone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||
msgid "Your local time:"
|
||||
msgstr "Din lokaltid:"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-19 16:32+0000\n"
|
||||
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -484,49 +484,49 @@ msgstr "Minuten"
|
||||
msgid "Check-in QR"
|
||||
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:"
|
||||
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"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
msgid "Image area"
|
||||
msgstr "Bildbereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Event-Ticketshop von pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"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?"
|
||||
msgstr ""
|
||||
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
@@ -577,15 +577,15 @@ msgstr "Suchbegriff"
|
||||
msgid "Selected only"
|
||||
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"
|
||||
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"
|
||||
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!"
|
||||
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."
|
||||
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."
|
||||
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"
|
||||
msgstr "verpflichtend"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
@@ -119,7 +119,6 @@ ggf
|
||||
GiroCode
|
||||
giropay
|
||||
GPL
|
||||
Grants
|
||||
Guide
|
||||
Gutscheineinlöser
|
||||
herunterscrollen
|
||||
@@ -139,7 +138,6 @@ innennamen
|
||||
Installations
|
||||
integrationen
|
||||
INV
|
||||
invalidieren
|
||||
invalidiert
|
||||
ISU
|
||||
iOS
|
||||
@@ -178,10 +176,8 @@ name
|
||||
Nr
|
||||
number
|
||||
OK
|
||||
On
|
||||
Open
|
||||
OpenCage
|
||||
OpenID
|
||||
OpenStreetMap
|
||||
Opera
|
||||
Output
|
||||
@@ -240,15 +236,12 @@ Scanergebnis
|
||||
Scanning
|
||||
schiefgeht
|
||||
schiefgelaufen
|
||||
Scope
|
||||
Scopes
|
||||
sechsstelligen
|
||||
Secret
|
||||
Security
|
||||
SEPA
|
||||
Shirts
|
||||
Signaturverfahren
|
||||
Sign
|
||||
Sitzplanmoduls
|
||||
Social
|
||||
Sofort
|
||||
@@ -257,7 +250,6 @@ Sorry
|
||||
Source
|
||||
SPF
|
||||
SSL
|
||||
SSO
|
||||
STARTTLS
|
||||
Steuerschuldnerschaft
|
||||
Store
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-19 16:32+0000\n"
|
||||
"POT-Creation-Date: 2022-07-25 16:59+0000\n"
|
||||
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
@@ -483,49 +483,49 @@ msgstr "Minuten"
|
||||
msgid "Check-in QR"
|
||||
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:"
|
||||
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"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:654
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:656
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:658
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
msgid "Image area"
|
||||
msgstr "Bildbereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:660
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Event-Ticketshop von pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:662
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:666
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:956
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1045
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"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?"
|
||||
msgstr ""
|
||||
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
@@ -576,15 +576,15 @@ msgstr "Suchbegriff"
|
||||
msgid "Selected only"
|
||||
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"
|
||||
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"
|
||||
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!"
|
||||
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."
|
||||
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."
|
||||
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"
|
||||
msgstr "verpflichtend"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:519
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:538
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:517
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:536
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:527
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
@@ -119,7 +119,6 @@ ggf
|
||||
GiroCode
|
||||
giropay
|
||||
GPL
|
||||
Grants
|
||||
Guide
|
||||
Gutscheineinlöser
|
||||
herunterscrollen
|
||||
@@ -139,7 +138,6 @@ innennamen
|
||||
Installations
|
||||
integrationen
|
||||
INV
|
||||
invalidieren
|
||||
invalidiert
|
||||
ISU
|
||||
iOS
|
||||
@@ -178,10 +176,8 @@ name
|
||||
Nr
|
||||
number
|
||||
OK
|
||||
On
|
||||
Open
|
||||
OpenCage
|
||||
OpenID
|
||||
OpenStreetMap
|
||||
Opera
|
||||
Output
|
||||
@@ -240,15 +236,12 @@ Scanergebnis
|
||||
Scanning
|
||||
schiefgeht
|
||||
schiefgelaufen
|
||||
Scope
|
||||
Scopes
|
||||
sechsstelligen
|
||||
Secret
|
||||
Security
|
||||
SEPA
|
||||
Shirts
|
||||
Signaturverfahren
|
||||
Sign
|
||||
Sitzplanmoduls
|
||||
Social
|
||||
Sofort
|
||||
@@ -257,7 +250,6 @@ Sorry
|
||||
Source
|
||||
SPF
|
||||
SSL
|
||||
SSO
|
||||
STARTTLS
|
||||
Steuerschuldnerschaft
|
||||
Store
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user