Compare commits

..

5 Commits

Author SHA1 Message Date
Richard Schreiber
bbca2f6c40 remove console.log 2022-08-05 11:04:09 +02:00
Richard Schreiber
6c0f19a1f6 use event.target instead of this 2022-08-05 11:01:23 +02:00
Richard Schreiber
37454dd84e add prefill fix to qr-code as well 2022-08-04 17:55:39 +02:00
Richard Schreiber
9bba61a495 use content from other if switching between other/other-i18n 2022-08-04 17:13:31 +02:00
Richard Schreiber
4cd4b94a4b set toolbox other/other_i18n input.value to empty string if not needed 2022-08-04 17:12:57 +02:00
224 changed files with 66522 additions and 94193 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View File

@@ -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.)

View File

@@ -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!

View File

@@ -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).

View File

@@ -2,7 +2,6 @@
file=/tmp/supervisor.sock
[supervisord]
environment = AUTOMIGRATE="skip"
logfile=/dev/stdout
logfile_maxbytes=0
loglevel=info

View File

@@ -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 {

View File

@@ -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
----------------------

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(){

View File

@@ -12,7 +12,6 @@ wanting to use pretix to sell tickets.
events/settings
events/structureguide
events/widget
customers/index
events/giftcards
faq
markdown

View File

@@ -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')},
},
),
]

View File

@@ -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)

View File

@@ -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',

View File

@@ -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"),

View File

@@ -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,)

View File

@@ -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)

View File

@@ -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

View File

@@ -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/>.
#

View File

@@ -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",
)

View File

@@ -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 ''),

View File

@@ -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'),

View File

@@ -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 ''

View File

@@ -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),

View File

@@ -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'),
),
]

View File

@@ -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')),
],
),
]

View File

@@ -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,
),
]

View File

@@ -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')},
),
]

View File

@@ -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)

View File

@@ -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}:

View File

@@ -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

View File

@@ -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):

View File

@@ -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()])

View File

@@ -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.'))

View File

@@ -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")

View File

@@ -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):

View File

@@ -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

View File

@@ -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',
})

View File

@@ -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()

View File

@@ -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()
)

View File

@@ -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(

View File

@@ -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()

View File

@@ -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}')

View File

@@ -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')

View File

@@ -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>
&middot; <a id='reload' href='#'>{% trans "Try again" %}</a>
</p>
<img src="{% static "pretixbase/img/pretix-logo.svg" %}" class="logo"/>
</div>
{% endblock %}

View File

@@ -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]-->

View File

@@ -3,7 +3,7 @@
<td>
<div style="line-height: 18px;height: 18px;">&nbsp;</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;">&nbsp;</div>
</td>
</tr>

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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):

View File

@@ -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()

View File

@@ -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'):

View File

@@ -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']

View File

@@ -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.'),

View File

@@ -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'),

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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$',

View File

@@ -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,

View File

@@ -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)

View File

@@ -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",

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:"

View File

@@ -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

View File

@@ -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:"

View File

@@ -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