forked from CGM_Public/pretix_original
Compare commits
135
Commits
@@ -97,6 +97,9 @@ Example::
|
||||
|
||||
``csp_log``
|
||||
Log violations of the Content Security Policy (CSP). Defaults to ``on``.
|
||||
|
||||
``loglevel``
|
||||
Set console and file loglevel (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR`` or ``CRITICAL``). Defaults to ``INFO``.
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
+9
-10
@@ -25,7 +25,7 @@ Obtaining an authorization grant
|
||||
--------------------------------
|
||||
|
||||
To authorize a new user, link or redirect them to the ``authorize`` endpoint, passing your client ID as a query
|
||||
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, or ``read write``)
|
||||
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, ``read write`` or ``profile``)
|
||||
and an URL the user should be redirected to after successful or failed authorization. You also need to pass the
|
||||
``response_type`` parameter with a value of ``code``. Example::
|
||||
|
||||
@@ -47,11 +47,9 @@ You will need this ``code`` parameter to perform the next step.
|
||||
|
||||
On a failed registration, a query string like ``?error=access_denied`` will be appended to the redirection URL.
|
||||
|
||||
.. note:: In this step, the user is allowed to restrict your access to certain organizer accounts. If you try to
|
||||
re-authenticate the user later, the user might be instantly redirected back to you if authorization is already
|
||||
given and would therefore be unable to review their organizer restriction settings. You can append the
|
||||
``approval_prompt=force`` query parameter if you want to make sure the user actively needs to confirm the
|
||||
authorization.
|
||||
.. note:: By default, the user is asked to give permission on every call to this URL. If you **only** request the
|
||||
``profile`` scope, i.e. no access to organizer data, you can pass the ``approval_prompt=auto`` parameter
|
||||
to skip user interaction on subsequen calls.
|
||||
|
||||
Getting an access token
|
||||
-----------------------
|
||||
@@ -193,10 +191,11 @@ If you need the user's meta data, you can fetch it here:
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
email: "admin@localhost",
|
||||
fullname: "John Doe",
|
||||
locale: "de",
|
||||
timezone: "Europe/Berlin"
|
||||
"email": "admin@localhost",
|
||||
"fullname": "John Doe",
|
||||
"locale": "de",
|
||||
"is_staff": false,
|
||||
"timezone": "Europe/Berlin"
|
||||
}
|
||||
|
||||
:statuscode 200: no error
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
.. spelling:: fullname
|
||||
|
||||
.. _`rest-devices`:
|
||||
|
||||
Devices
|
||||
=======
|
||||
|
||||
See also :ref:`rest-deviceauth`.
|
||||
|
||||
Device resource
|
||||
----------------
|
||||
|
||||
The device resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
device_id integer Internal ID of the device within this organizer
|
||||
unique_serial string Unique identifier of this device
|
||||
name string Device name
|
||||
all_events boolean Whether this device has access to all events
|
||||
limit_events list List of event slugs this device has access to
|
||||
hardware_brand string Device hardware manufacturer (read-only)
|
||||
hardware_model string Device hardware model (read-only)
|
||||
software_brand string Device software product (read-only)
|
||||
software_version string Device software version (read-only)
|
||||
created datetime Creation time
|
||||
initialized datetime Time of initialization (or ``null``)
|
||||
initialization_token string Token for initialization
|
||||
revoked boolean Whether this device no longer has access
|
||||
security_profile string The name of a supported security profile restricting API access
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Device endpoints
|
||||
----------------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/devices/
|
||||
|
||||
Returns a list of all devices within a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/devices/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"device_id": 1,
|
||||
"unique_serial": "UOS3GNZ27O39V3QS",
|
||||
"initialization_token": "frkso3m2w58zuw70",
|
||||
"all_events": false,
|
||||
"limit_events": [
|
||||
"museum"
|
||||
],
|
||||
"revoked": false,
|
||||
"name": "Scanner",
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"initialized": "2020-09-18T14:17:44.190021Z",
|
||||
"security_profile": "full",
|
||||
"hardware_brand": "Zebra",
|
||||
"hardware_model": "TC25",
|
||||
"software_brand": "pretixSCAN",
|
||||
"software_version": "1.5.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/devices/(device_id)/
|
||||
|
||||
Returns information on one device, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/devices/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"unique_serial": "UOS3GNZ27O39V3QS",
|
||||
"initialization_token": "frkso3m2w58zuw70",
|
||||
"all_events": false,
|
||||
"limit_events": [
|
||||
"museum"
|
||||
],
|
||||
"revoked": false,
|
||||
"name": "Scanner",
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"initialized": "2020-09-18T14:17:44.190021Z",
|
||||
"security_profile": "full",
|
||||
"hardware_brand": "Zebra",
|
||||
"hardware_model": "TC25",
|
||||
"software_brand": "pretixSCAN",
|
||||
"software_version": "1.5.1"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param device_id: The ``device_id`` field of the device to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/devices/
|
||||
|
||||
Creates a new device
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/devices/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Scanner",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"unique_serial": "UOS3GNZ27O39V3QS",
|
||||
"initialization_token": "frkso3m2w58zuw70",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"revoked": false,
|
||||
"name": "Scanner",
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"security_profile": "full",
|
||||
"initialized": null
|
||||
"hardware_brand": null,
|
||||
"hardware_model": null,
|
||||
"software_brand": null,
|
||||
"software_version": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a device for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The device could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/devices/(device_id)/
|
||||
|
||||
Update a device.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/devices/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"name": "Foo"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Foo",
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param device_id: The ``device_id`` field of the deviec to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The device could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
|
||||
@@ -209,14 +209,15 @@ Endpoints
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
|
||||
POST /api/v1/organizers/bigevents/giftcards/1/transact/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
Content-Length: 79
|
||||
|
||||
{
|
||||
"value": "2.00"
|
||||
"value": "2.00",
|
||||
"text": "Optional value explaining the transaction"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@@ -24,6 +24,7 @@ Resources and endpoints
|
||||
giftcards
|
||||
carts
|
||||
teams
|
||||
devices
|
||||
webhooks
|
||||
seatingplans
|
||||
billing_invoices
|
||||
|
||||
@@ -201,6 +201,7 @@ addon_to integer Internal ID of
|
||||
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
||||
pseudonymization_id string A random ID, e.g. for use in lead scanning apps
|
||||
checkins list of objects List of check-ins with this ticket
|
||||
├ id integer Internal ID of the check-in event
|
||||
├ list integer Internal ID of the check-in list
|
||||
├ datetime datetime Time of check-in
|
||||
├ type string Type of scan (defaults to ``entry``)
|
||||
@@ -1029,6 +1030,10 @@ Creating orders
|
||||
Order state operations
|
||||
----------------------
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``mark_paid`` operation now takes a ``send_email`` parameter.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
|
||||
|
||||
Marks a pending or expired order as successfully paid.
|
||||
@@ -1040,6 +1045,11 @@ Order state operations
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/mark_paid/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"send_email": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@@ -1722,6 +1732,10 @@ Order payment endpoints
|
||||
|
||||
Payments can now be created through the API.
|
||||
|
||||
.. versionchanged:: 3.12
|
||||
|
||||
The ``confirm`` operation now takes a ``send_email`` parameter.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/payments/
|
||||
|
||||
Returns a list of all payments for an order.
|
||||
@@ -1822,7 +1836,10 @@ Order payment endpoints
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{"force": false}
|
||||
{
|
||||
"send_email": true,
|
||||
"force": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ in the ``installed`` method::
|
||||
pass # Your code here
|
||||
|
||||
|
||||
Note that ``installed`` will *not* be called if the plugin in indirectly activated for an event
|
||||
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
|
||||
because the event is created with settings copied from another event.
|
||||
|
||||
Views
|
||||
@@ -151,8 +151,8 @@ your Django app label.
|
||||
with checking that the calling user is logged in, has appropriate permissions,
|
||||
etc. We plan on providing native support for this in a later version.
|
||||
|
||||
.. _Django app: https://docs.djangoproject.com/en/1.7/ref/applications/
|
||||
.. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
|
||||
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/
|
||||
.. _Django app: https://docs.djangoproject.com/en/3.0/ref/applications/
|
||||
.. _signal dispatcher: https://docs.djangoproject.com/en/3.0/topics/signals/
|
||||
.. _namespace packages: https://legacy.python.org/dev/peps/pep-0420/
|
||||
.. _entry point: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#locating-plugins
|
||||
.. _cookiecutter: https://cookiecutter.readthedocs.io/en/latest/
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.11.1"
|
||||
__version__ = "3.12.0.dev0"
|
||||
|
||||
@@ -3,6 +3,9 @@ from django_scopes import scopes_disabled
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
from pretix.api.auth.devicesecurity import (
|
||||
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
|
||||
)
|
||||
from pretix.base.models import Device
|
||||
|
||||
|
||||
@@ -25,3 +28,11 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
||||
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
||||
|
||||
return AnonymousUser(), device
|
||||
|
||||
def authenticate(self, request):
|
||||
r = super().authenticate(request)
|
||||
if r and isinstance(r[1], Device):
|
||||
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
|
||||
if not profile.is_allowed(request):
|
||||
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
||||
return r
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class FullAccessSecurityProfile:
|
||||
identifier = 'full'
|
||||
verbose_name = _('Full access')
|
||||
|
||||
def is_allowed(self, request):
|
||||
return True
|
||||
|
||||
|
||||
class AllowListSecurityProfile:
|
||||
allowlist = tuple()
|
||||
|
||||
def is_allowed(self, request):
|
||||
key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}")
|
||||
return key in self.allowlist
|
||||
|
||||
|
||||
class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
identifier = 'pretixscan'
|
||||
verbose_name = _('pretixSCAN')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.update'),
|
||||
('GET', 'api-v1:device.revoke'),
|
||||
('GET', 'api-v1:event-list'),
|
||||
('GET', 'api-v1:event-detail'),
|
||||
('GET', 'api-v1:subevent-list'),
|
||||
('GET', 'api-v1:subevent-detail'),
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('GET', 'api-v1:checkinlistpos-list'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
|
||||
|
||||
class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
identifier = 'pretixscan_online_kiosk'
|
||||
verbose_name = _('pretixSCAN (kiosk mode, online only)')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.update'),
|
||||
('GET', 'api-v1:device.revoke'),
|
||||
('GET', 'api-v1:event-list'),
|
||||
('GET', 'api-v1:event-detail'),
|
||||
('GET', 'api-v1:subevent-list'),
|
||||
('GET', 'api-v1:subevent-detail'),
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:checkinlist-list'),
|
||||
('GET', 'api-v1:checkinlist-status'),
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
|
||||
|
||||
class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
identifier = 'pretixpos'
|
||||
verbose_name = _('pretixPOS')
|
||||
allowlist = (
|
||||
('GET', 'api-v1:version'),
|
||||
('GET', 'api-v1:device.update'),
|
||||
('GET', 'api-v1:device.revoke'),
|
||||
('GET', 'api-v1:event-list'),
|
||||
('GET', 'api-v1:event-detail'),
|
||||
('GET', 'api-v1:subevent-list'),
|
||||
('GET', 'api-v1:subevent-detail'),
|
||||
('GET', 'api-v1:itemcategory-list'),
|
||||
('GET', 'api-v1:item-list'),
|
||||
('GET', 'api-v1:question-list'),
|
||||
('GET', 'api-v1:quota-list'),
|
||||
('GET', 'api-v1:taxrule-list'),
|
||||
('GET', 'api-v1:ticketlayout-list'),
|
||||
('GET', 'api-v1:ticketlayoutitem-list'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('POST', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:order-detail'),
|
||||
('DELETE', 'api-v1:orderposition-detail'),
|
||||
('POST', 'api-v1:order-mark_canceled'),
|
||||
('POST', 'api-v1:orderrefund-list'),
|
||||
('POST', 'api-v1:orderrefund-done'),
|
||||
('POST', 'api-v1:cartposition-list'),
|
||||
('DELETE', 'api-v1:cartposition-detail'),
|
||||
('GET', 'api-v1:giftcard-list'),
|
||||
('POST', 'api-v1:giftcard-transact'),
|
||||
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
|
||||
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
)
|
||||
|
||||
|
||||
DEVICE_SECURITY_PROFILES = {
|
||||
k.identifier: k() for k in (
|
||||
FullAccessSecurityProfile,
|
||||
PretixScanSecurityProfile,
|
||||
PretixScanNoSyncSecurityProfile,
|
||||
PretixPosSecurityProfile,
|
||||
)
|
||||
}
|
||||
@@ -84,3 +84,15 @@ class EventCRUDPermission(EventPermission):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ProfilePermission(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if isinstance(request.auth, OAuthAccessToken):
|
||||
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
|
||||
return False
|
||||
return True
|
||||
|
||||
+11
-8
@@ -9,7 +9,7 @@ from oauth2_provider.settings import oauth2_settings
|
||||
class Validator(OAuth2Validator):
|
||||
|
||||
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||
if not getattr(request, 'organizers', None):
|
||||
if not getattr(request, 'organizers', None) and request.scopes != ['profile']:
|
||||
raise FatalClientError('No organizers selected.')
|
||||
|
||||
expires = timezone.now() + timedelta(
|
||||
@@ -18,7 +18,8 @@ class Validator(OAuth2Validator):
|
||||
expires=expires, redirect_uri=request.redirect_uri,
|
||||
scope=" ".join(request.scopes))
|
||||
g.save()
|
||||
g.organizers.add(*request.organizers.all())
|
||||
if request.scopes != ['profile']:
|
||||
g.organizers.add(*request.organizers.all())
|
||||
|
||||
def validate_code(self, client_id, code, client, request, *args, **kwargs):
|
||||
try:
|
||||
@@ -34,12 +35,14 @@ class Validator(OAuth2Validator):
|
||||
return False
|
||||
|
||||
def _create_access_token(self, expires, request, token, source_refresh_token=None):
|
||||
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token'):
|
||||
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token', None) and token["scope"] != 'profile':
|
||||
raise FatalClientError('No organizers selected.')
|
||||
if hasattr(request, 'organizers'):
|
||||
orgs = list(request.organizers.all())
|
||||
else:
|
||||
orgs = list(source_refresh_token.access_token.organizers.all())
|
||||
if token['scope'] != 'profile':
|
||||
if hasattr(request, 'organizers'):
|
||||
orgs = list(request.organizers.all())
|
||||
else:
|
||||
orgs = list(source_refresh_token.access_token.organizers.all())
|
||||
access_token = super()._create_access_token(expires, request, token, source_refresh_token=None)
|
||||
access_token.organizers.add(*orgs)
|
||||
if token['scope'] != 'profile':
|
||||
access_token.organizers.add(*orgs)
|
||||
return access_token
|
||||
|
||||
@@ -122,7 +122,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Checkin
|
||||
fields = ('datetime', 'list', 'auto_checked_in', 'type')
|
||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'type')
|
||||
|
||||
|
||||
class OrderDownloadsField(serializers.Field):
|
||||
|
||||
@@ -9,7 +9,8 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.models import (
|
||||
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -66,9 +67,6 @@ class EventSlugField(serializers.SlugRelatedField):
|
||||
class TeamSerializer(serializers.ModelSerializer):
|
||||
limit_events = EventSlugField(slug_field='slug', many=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
@@ -86,6 +84,28 @@ class TeamSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
limit_events = EventSlugField(slug_field='slug', many=True)
|
||||
device_id = serializers.IntegerField(read_only=True)
|
||||
unique_serial = serializers.CharField(read_only=True)
|
||||
hardware_brand = serializers.CharField(read_only=True)
|
||||
hardware_model = serializers.CharField(read_only=True)
|
||||
software_brand = serializers.CharField(read_only=True)
|
||||
software_version = serializers.CharField(read_only=True)
|
||||
created = serializers.DateTimeField(read_only=True)
|
||||
revoked = serializers.BooleanField(read_only=True)
|
||||
initialized = serializers.DateTimeField(read_only=True)
|
||||
initialization_token = serializers.DateTimeField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = (
|
||||
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
|
||||
'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model',
|
||||
'software_brand', 'software_version', 'security_profile'
|
||||
)
|
||||
|
||||
|
||||
class TeamInviteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TeamInvite
|
||||
|
||||
@@ -21,6 +21,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
orga_router.register(r'teams', organizer.TeamViewSet)
|
||||
orga_router.register(r'devices', organizer.DeviceViewSet)
|
||||
|
||||
team_router = routers.DefaultRouter()
|
||||
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||
@@ -44,7 +45,7 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet)
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||
|
||||
question_router = routers.DefaultRouter()
|
||||
question_router.register(r'options', item.QuestionOptionViewSet)
|
||||
|
||||
@@ -18,6 +18,7 @@ from pretix.api.serializers.item import QuestionSerializer
|
||||
from pretix.api.serializers.order import CheckinListOrderPositionSerializer
|
||||
from pretix.api.views import RichOrderingFilter
|
||||
from pretix.api.views.order import OrderPositionFilter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Event, Order, OrderPosition,
|
||||
)
|
||||
@@ -87,73 +88,74 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action(detail=True, methods=['GET'])
|
||||
def status(self, *args, **kwargs):
|
||||
clist = self.get_object()
|
||||
cqs = Checkin.objects.filter(
|
||||
position__order__event=clist.event,
|
||||
position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||
list=clist
|
||||
)
|
||||
pqs = OrderPosition.objects.filter(
|
||||
order__event=clist.event,
|
||||
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||
)
|
||||
if clist.subevent:
|
||||
pqs = pqs.filter(subevent=clist.subevent)
|
||||
if not clist.all_products:
|
||||
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
||||
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
|
||||
with language(self.request.event.settings.locale):
|
||||
clist = self.get_object()
|
||||
cqs = Checkin.objects.filter(
|
||||
position__order__event=clist.event,
|
||||
position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||
list=clist
|
||||
)
|
||||
pqs = OrderPosition.objects.filter(
|
||||
order__event=clist.event,
|
||||
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
|
||||
)
|
||||
if clist.subevent:
|
||||
pqs = pqs.filter(subevent=clist.subevent)
|
||||
if not clist.all_products:
|
||||
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
|
||||
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
|
||||
|
||||
ev = clist.subevent or clist.event
|
||||
response = {
|
||||
'event': {
|
||||
'name': str(ev.name),
|
||||
},
|
||||
'checkin_count': cqs.count(),
|
||||
'position_count': pqs.count()
|
||||
}
|
||||
|
||||
op_by_item = {
|
||||
p['item']: p['cnt']
|
||||
for p in pqs.order_by().values('item').annotate(cnt=Count('id'))
|
||||
}
|
||||
op_by_variation = {
|
||||
p['variation']: p['cnt']
|
||||
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
|
||||
}
|
||||
c_by_item = {
|
||||
p['position__item']: p['cnt']
|
||||
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
|
||||
}
|
||||
c_by_variation = {
|
||||
p['position__variation']: p['cnt']
|
||||
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
|
||||
}
|
||||
|
||||
if not clist.all_products:
|
||||
items = clist.limit_products
|
||||
else:
|
||||
items = clist.event.items
|
||||
|
||||
response['items'] = []
|
||||
for item in items.order_by('category__position', 'position', 'pk').prefetch_related('variations'):
|
||||
i = {
|
||||
'id': item.pk,
|
||||
'name': str(item),
|
||||
'admission': item.admission,
|
||||
'checkin_count': c_by_item.get(item.pk, 0),
|
||||
'position_count': op_by_item.get(item.pk, 0),
|
||||
'variations': []
|
||||
ev = clist.subevent or clist.event
|
||||
response = {
|
||||
'event': {
|
||||
'name': str(ev.name),
|
||||
},
|
||||
'checkin_count': cqs.count(),
|
||||
'position_count': pqs.count()
|
||||
}
|
||||
for var in item.variations.all():
|
||||
i['variations'].append({
|
||||
'id': var.pk,
|
||||
'value': str(var),
|
||||
'checkin_count': c_by_variation.get(var.pk, 0),
|
||||
'position_count': op_by_variation.get(var.pk, 0),
|
||||
})
|
||||
response['items'].append(i)
|
||||
|
||||
return Response(response)
|
||||
op_by_item = {
|
||||
p['item']: p['cnt']
|
||||
for p in pqs.order_by().values('item').annotate(cnt=Count('id'))
|
||||
}
|
||||
op_by_variation = {
|
||||
p['variation']: p['cnt']
|
||||
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
|
||||
}
|
||||
c_by_item = {
|
||||
p['position__item']: p['cnt']
|
||||
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
|
||||
}
|
||||
c_by_variation = {
|
||||
p['position__variation']: p['cnt']
|
||||
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
|
||||
}
|
||||
|
||||
if not clist.all_products:
|
||||
items = clist.limit_products
|
||||
else:
|
||||
items = clist.event.items
|
||||
|
||||
response['items'] = []
|
||||
for item in items.order_by('category__position', 'position', 'pk').prefetch_related('variations'):
|
||||
i = {
|
||||
'id': item.pk,
|
||||
'name': str(item),
|
||||
'admission': item.admission,
|
||||
'checkin_count': c_by_item.get(item.pk, 0),
|
||||
'position_count': op_by_item.get(item.pk, 0),
|
||||
'variations': []
|
||||
}
|
||||
for var in item.variations.all():
|
||||
i['variations'].append({
|
||||
'id': var.pk,
|
||||
'value': str(var),
|
||||
'checkin_count': c_by_variation.get(var.pk, 0),
|
||||
'position_count': op_by_variation.get(var.pk, 0),
|
||||
})
|
||||
response['items'].append(i)
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -35,7 +35,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
model = Device
|
||||
fields = [
|
||||
'organizer', 'device_id', 'unique_serial', 'api_token',
|
||||
'name'
|
||||
'name', 'security_profile'
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from pretix.api.serializers.event import (
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, ItemCategory, TaxRule, TeamAPIToken,
|
||||
CartPosition, Device, Event, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
@@ -73,6 +73,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
queryset = Event.objects.none()
|
||||
lookup_field = 'slug'
|
||||
lookup_url_kwarg = 'event'
|
||||
lookup_value_regex = '[^/]+'
|
||||
permission_classes = (EventCRUDPermission,)
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
ordering = ('slug',)
|
||||
@@ -228,7 +229,7 @@ with scopes_disabled():
|
||||
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = SubEventSerializer
|
||||
queryset = ItemCategory.objects.none()
|
||||
queryset = SubEvent.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filterset_class = SubEventFilter
|
||||
|
||||
@@ -3,8 +3,9 @@ import logging
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.exceptions import OAuthToolkitError
|
||||
from oauth2_provider.exceptions import FatalClientError, OAuthToolkitError
|
||||
from oauth2_provider.forms import AllowForm
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
from oauth2_provider.views import (
|
||||
AuthorizationView as BaseAuthorizationView,
|
||||
RevokeTokenView as BaseRevokeTokenView, TokenView as BaseTokenView,
|
||||
@@ -24,9 +25,12 @@ class OAuthAllowForm(AllowForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user')
|
||||
scope = kwargs.pop('scope')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['organizers'].queryset = Organizer.objects.filter(
|
||||
pk__in=user.teams.values_list('organizer', flat=True))
|
||||
if scope == 'profile':
|
||||
del self.fields['organizers']
|
||||
|
||||
|
||||
class AuthorizationView(BaseAuthorizationView):
|
||||
@@ -36,6 +40,7 @@ class AuthorizationView(BaseAuthorizationView):
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
kwargs['scope'] = self.request.GET.get('scope')
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -43,8 +48,14 @@ class AuthorizationView(BaseAuthorizationView):
|
||||
ctx['settings'] = settings
|
||||
return ctx
|
||||
|
||||
def create_authorization_response(self, request, scopes, credentials, allow, organizers):
|
||||
credentials["organizers"] = organizers
|
||||
def validate_authorization_request(self, request):
|
||||
require_approval = request.GET.get("approval_prompt", oauth2_settings.REQUEST_APPROVAL_PROMPT)
|
||||
if require_approval != 'force' and request.GET.get('scope') != 'profile':
|
||||
raise FatalClientError('Combnination of require_approval and scope values not allowed.')
|
||||
return super().validate_authorization_request(request)
|
||||
|
||||
def create_authorization_response(self, request, scopes, credentials, allow, organizers=None):
|
||||
credentials["organizers"] = organizers or []
|
||||
return super().create_authorization_response(request, scopes, credentials, allow)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
@@ -228,6 +228,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||
|
||||
@@ -269,6 +270,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
try:
|
||||
p.confirm(auth=self.request.auth,
|
||||
user=self.request.user if request.user.is_authenticated else None,
|
||||
send_mail=send_mail,
|
||||
count_waitinglist=False)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -976,6 +978,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
def confirm(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
force = request.data.get('force', False)
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
|
||||
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -984,6 +987,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
payment.confirm(user=self.request.user if self.request.user.is_authenticated else None,
|
||||
auth=self.request.auth,
|
||||
count_waitinglist=False,
|
||||
send_mail=send_mail,
|
||||
force=force)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -6,20 +6,22 @@ from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import filters, serializers, status, viewsets
|
||||
from rest_framework import filters, mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.api.serializers.organizer import (
|
||||
GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer,
|
||||
TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer,
|
||||
TeamSerializer,
|
||||
DeviceSerializer, GiftCardSerializer, OrganizerSerializer,
|
||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
||||
TeamMemberSerializer, TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
)
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
@@ -29,6 +31,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = Organizer.objects.none()
|
||||
lookup_field = 'slug'
|
||||
lookup_url_kwarg = 'organizer'
|
||||
lookup_value_regex = '[^/]+'
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('slug',)
|
||||
ordering_fields = ('name', 'slug')
|
||||
@@ -352,3 +355,44 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
serializer = self.get_serializer_class()(instance)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
|
||||
|
||||
|
||||
class DeviceViewSet(mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet):
|
||||
serializer_class = DeviceSerializer
|
||||
queryset = Device.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
lookup_field = 'device_id'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.devices.order_by('pk')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
inst.log_action(
|
||||
'pretix.device.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save()
|
||||
inst.log_action(
|
||||
'pretix.device.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
return inst
|
||||
|
||||
@@ -3,14 +3,18 @@ from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.auth.permission import ProfilePermission
|
||||
|
||||
|
||||
class MeView(APIView):
|
||||
authentication_classes = (SessionAuthentication, OAuth2Authentication)
|
||||
permission_classes = (ProfilePermission,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
'email': request.user.email,
|
||||
'fullname': request.user.fullname,
|
||||
'locale': request.user.locale,
|
||||
'is_staff': request.user.is_staff,
|
||||
'timezone': request.user.timezone
|
||||
})
|
||||
|
||||
@@ -85,6 +85,8 @@ class ParametrizedOrderWebhookEvent(WebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
order = logentry.content_object
|
||||
if not order:
|
||||
return None
|
||||
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
@@ -99,6 +101,8 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
d = super().build_payload(logentry)
|
||||
if d is None:
|
||||
return None
|
||||
d['orderposition_id'] = logentry.parsed_data.get('position')
|
||||
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
|
||||
d['checkin_list'] = logentry.parsed_data.get('list')
|
||||
@@ -218,6 +222,10 @@ def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
return # Ignore, e.g. plugin not installed
|
||||
|
||||
payload = event_type.build_payload(logentry)
|
||||
if payload is None:
|
||||
# Content object deleted?
|
||||
return
|
||||
|
||||
t = time.time()
|
||||
|
||||
try:
|
||||
|
||||
@@ -98,7 +98,10 @@ class BaseAuthBackend:
|
||||
|
||||
class NativeAuthBackend(BaseAuthBackend):
|
||||
identifier = 'native'
|
||||
verbose_name = _('pretix User')
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('{system} User').format(system=settings.PRETIX_INSTANCE_NAME)
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
|
||||
@@ -114,7 +114,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
'site_url': settings.SITE_URL,
|
||||
'body': body_md,
|
||||
'subject': str(subject),
|
||||
'color': '#8E44B3',
|
||||
'color': settings.PRETIX_PRIMARY_COLOR,
|
||||
'rtl': get_language() in settings.LANGUAGES_RTL
|
||||
}
|
||||
if self.event:
|
||||
@@ -222,6 +222,7 @@ class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
|
||||
def get_available_placeholders(event, base_parameters):
|
||||
if 'order' in base_parameters:
|
||||
base_parameters.append('invoice_address')
|
||||
base_parameters.append('position_or_address')
|
||||
params = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
@@ -240,7 +241,9 @@ def get_email_context(**kwargs):
|
||||
try:
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
kwargs['invoice_address'] = InvoiceAddress()
|
||||
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
|
||||
finally:
|
||||
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
|
||||
ctx = {}
|
||||
for r, val in register_mail_placeholders.send(sender=event):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
@@ -268,7 +271,8 @@ def get_best_name(position_or_address, parts=False):
|
||||
if isinstance(position_or_address, InvoiceAddress):
|
||||
if position_or_address.name:
|
||||
return position_or_address.name_parts if parts else position_or_address.name
|
||||
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
||||
elif position_or_address.order:
|
||||
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
||||
|
||||
if isinstance(position_or_address, OrderPosition):
|
||||
if position_or_address.attendee_name:
|
||||
|
||||
@@ -388,6 +388,11 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Pseudonymization ID'),
|
||||
_('Seat ID'),
|
||||
_('Seat name'),
|
||||
_('Seat zone'),
|
||||
_('Seat row'),
|
||||
_('Seat number'),
|
||||
]
|
||||
|
||||
questions = list(Question.objects.filter(event__in=self.events))
|
||||
@@ -471,6 +476,18 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
]
|
||||
|
||||
if op.seat:
|
||||
row += [
|
||||
op.seat.seat_guid,
|
||||
str(op.seat),
|
||||
op.seat.zone_name,
|
||||
op.seat.row_name,
|
||||
op.seat.seat_number,
|
||||
]
|
||||
else:
|
||||
row += ['', '', '', '', '']
|
||||
|
||||
acache = {}
|
||||
for a in op.answers.all():
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
@@ -645,11 +662,13 @@ class GiftcardRedemptionListExporter(ListExporter):
|
||||
def iterate_list(self, form_data):
|
||||
payments = OrderPayment.objects.filter(
|
||||
order__event__in=self.events,
|
||||
provider='giftcard'
|
||||
provider='giftcard',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
).order_by('created')
|
||||
refunds = OrderRefund.objects.filter(
|
||||
order__event__in=self.events,
|
||||
provider='giftcard'
|
||||
provider='giftcard',
|
||||
state=OrderRefund.REFUND_STATE_DONE
|
||||
).order_by('created')
|
||||
|
||||
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import hashlib
|
||||
import ipaddress
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
@@ -23,7 +18,6 @@ class LoginForm(forms.Form):
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
@@ -45,36 +39,10 @@ class LoginForm(forms.Form):
|
||||
else:
|
||||
move_to_end(self.fields, 'keep_logged_in')
|
||||
|
||||
@cached_property
|
||||
def ratelimit_key(self):
|
||||
if not settings.HAS_REDIS:
|
||||
return None
|
||||
client_ip = get_client_ip(self.request)
|
||||
if not client_ip:
|
||||
return None
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
# Web server not set up correctly
|
||||
return None
|
||||
if client_ip.is_private:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||
|
||||
def clean(self):
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
if self.ratelimit_key:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.get(self.ratelimit_key)
|
||||
if cnt and int(cnt) > 10:
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
if self.ratelimit_key:
|
||||
rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 300)
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
|
||||
@@ -36,8 +36,8 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
||||
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
||||
from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
|
||||
PERSON_NAME_TITLE_GROUPS,
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.control.forms import ExtFileField, SplitDateTimeField
|
||||
@@ -49,7 +49,7 @@ from pretix.presale.signals import question_form_fields
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIRED_NAME_PARTS = ['given_name', 'family_name', 'full_name']
|
||||
REQUIRED_NAME_PARTS = ['salutation', 'given_name', 'family_name', 'full_name']
|
||||
|
||||
|
||||
class NamePartsWidget(forms.MultiWidget):
|
||||
@@ -73,6 +73,8 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
a['data-fname'] = fname
|
||||
if fname == 'title' and self.titles:
|
||||
widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]]))
|
||||
elif fname == 'salutation':
|
||||
widgets.append(Select(attrs=a, choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS]))
|
||||
else:
|
||||
widgets.append(self.widget(attrs=a))
|
||||
super().__init__(widgets, attrs)
|
||||
@@ -162,12 +164,18 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
**d,
|
||||
choices=[('', '')] + [(d, d) for d in self.scheme_titles[1]]
|
||||
)
|
||||
field.part_name = fname
|
||||
fields.append(field)
|
||||
|
||||
elif fname == 'salutation':
|
||||
d = dict(defaults)
|
||||
d.pop('max_length', None)
|
||||
field = forms.ChoiceField(
|
||||
**d,
|
||||
choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS]
|
||||
)
|
||||
else:
|
||||
field = forms.CharField(**defaults)
|
||||
field.part_name = fname
|
||||
fields.append(field)
|
||||
field.part_name = fname
|
||||
fields.append(field)
|
||||
super().__init__(
|
||||
fields=fields, require_all_fields=False, *args, **kwargs
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
@@ -20,7 +19,6 @@ class UserSettingsForm(forms.ModelForm):
|
||||
"address or password."),
|
||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||
'pw_mismatch': _("Please enter the same password twice"),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
}
|
||||
|
||||
old_pw = forms.CharField(max_length=255,
|
||||
@@ -66,18 +64,6 @@ class UserSettingsForm(forms.ModelForm):
|
||||
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
|
||||
if old_pw and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
|
||||
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if old_pw and not check_password(old_pw, self.user.password):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_current_wrong'],
|
||||
|
||||
@@ -396,13 +396,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' +
|
||||
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
|
||||
from_date=self.invoice.event.get_date_from_display(),
|
||||
to_date=self.invoice.event.get_date_to_display()
|
||||
from_date=self.invoice.event.get_date_from_display(show_times=False),
|
||||
to_date=self.invoice.event.get_date_to_display(show_times=False)
|
||||
)
|
||||
)
|
||||
else:
|
||||
p_str = (
|
||||
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
|
||||
shorten(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display(show_times=False)
|
||||
)
|
||||
else:
|
||||
p_str = shorten(self.invoice.event.name)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 3.0.11 on 2020-12-18 18:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0162_remove_seat_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cachedfile',
|
||||
name='session_key',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedfile',
|
||||
name='web_download',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.9 on 2020-10-13 08:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0162_remove_seat_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='security_profile',
|
||||
field=models.CharField(default='full', max_length=190, null=True),
|
||||
),
|
||||
]
|
||||
@@ -28,8 +28,6 @@ class CachedFile(models.Model):
|
||||
filename = models.CharField(max_length=255)
|
||||
type = models.CharField(max_length=255)
|
||||
file = models.FileField(null=True, blank=True, upload_to=cachedfile_name, max_length=255)
|
||||
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
||||
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedFile)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
|
||||
@@ -74,6 +75,13 @@ class Device(LoggedModel):
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
security_profile = models.CharField(
|
||||
max_length=190,
|
||||
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
|
||||
default='full',
|
||||
null=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='organizer')
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.formats import date_format
|
||||
@@ -89,7 +89,7 @@ class EventMixin:
|
||||
self.date_from.astimezone(tz), "TIME_FORMAT"
|
||||
)
|
||||
|
||||
def get_date_to_display(self, tz=None, short=False) -> str:
|
||||
def get_date_to_display(self, tz=None, show_times=True, short=False) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting. Returns an empty string
|
||||
@@ -100,14 +100,14 @@ class EventMixin:
|
||||
return ""
|
||||
return _date(
|
||||
self.date_to.astimezone(tz),
|
||||
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")
|
||||
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
|
||||
)
|
||||
|
||||
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date and the end date
|
||||
of the event with respect to the current locale and to the ``show_times`` and
|
||||
``show_date_to`` settings.
|
||||
of the event with respect to the current locale and to the ``show_date_to``
|
||||
setting. Times are not shown.
|
||||
"""
|
||||
tz = tz or self.timezone
|
||||
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
|
||||
@@ -189,7 +189,9 @@ class EventMixin:
|
||||
).order_by().values_list('quotas__pk').annotate(
|
||||
items=GroupConcat('pk', delimiter=',')
|
||||
).values('items')
|
||||
return qs.prefetch_related(
|
||||
return qs.annotate(
|
||||
has_paid_item=Exists(Item.objects.filter(event_id=OuterRef(cls._event_id), default_price__gt=0))
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
'quotas',
|
||||
to_attr='active_quotas',
|
||||
@@ -280,6 +282,7 @@ class Event(EventMixin, LoggedModel):
|
||||
"""
|
||||
|
||||
settings_namespace = 'event'
|
||||
_event_id = 'pk'
|
||||
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
|
||||
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
|
||||
testmode = models.BooleanField(default=False)
|
||||
@@ -786,7 +789,12 @@ class Event(EventMixin, LoggedModel):
|
||||
'name_ascending': ('name', 'date_from'),
|
||||
'name_descending': ('-name', 'date_from'),
|
||||
}[ordering]
|
||||
subevs = queryset.filter(
|
||||
subevs = queryset.annotate(
|
||||
has_paid_item=Value(
|
||||
self.cache.get_or_set('has_paid_item', lambda: self.items.filter(default_price__gt=0).exists(), 3600),
|
||||
output_field=models.BooleanField()
|
||||
)
|
||||
).filter(
|
||||
Q(active=True) & Q(is_public=True) & (
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
|
||||
| Q(date_to__gte=now() - timedelta(hours=24))
|
||||
@@ -980,6 +988,7 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
:type location: str
|
||||
"""
|
||||
|
||||
_event_id = 'event_id'
|
||||
event = models.ForeignKey(Event, related_name="subevents", on_delete=models.PROTECT)
|
||||
active = models.BooleanField(default=False, verbose_name=_("Active"),
|
||||
help_text=_("Only with this checkbox enabled, this date is visible in the "
|
||||
|
||||
@@ -1140,6 +1140,8 @@ class Question(LoggedModel):
|
||||
return None
|
||||
|
||||
if self.type == Question.TYPE_CHOICE:
|
||||
if isinstance(answer, QuestionOption):
|
||||
return answer
|
||||
q = Q(identifier=answer)
|
||||
if isinstance(answer, int) or answer.isdigit():
|
||||
q |= Q(pk=answer)
|
||||
@@ -1154,6 +1156,8 @@ class Question(LoggedModel):
|
||||
Q(identifier__in=answer.split(","))
|
||||
))
|
||||
llen = len(answer.split(','))
|
||||
elif all(isinstance(o, QuestionOption) for o in answer):
|
||||
return o
|
||||
else:
|
||||
l_ = list(self.options.filter(
|
||||
Q(pk__in=[a for a in answer if isinstance(a, int) or a.isdigit()]) |
|
||||
|
||||
@@ -174,7 +174,7 @@ class TaxRule(LoggedModel):
|
||||
return Decimal(self.rate)
|
||||
|
||||
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
|
||||
subtract_from_gross=Decimal('0.00')):
|
||||
subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None):
|
||||
from .event import Event
|
||||
try:
|
||||
currency = currency or self.event.currency
|
||||
@@ -186,7 +186,9 @@ class TaxRule(LoggedModel):
|
||||
rate = override_tax_rate
|
||||
elif invoice_address:
|
||||
adjust_rate = self.tax_rate_for(invoice_address)
|
||||
if adjust_rate != rate:
|
||||
if adjust_rate == gross_price_is_tax_rate and base_price_is == 'gross':
|
||||
rate = adjust_rate
|
||||
elif adjust_rate != rate:
|
||||
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
|
||||
base_price = normal_price.net
|
||||
base_price_is = 'net'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal, DecimalException
|
||||
|
||||
import pycountry
|
||||
@@ -12,11 +13,13 @@ from django.utils.translation import (
|
||||
)
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms.questions import guess_country
|
||||
from pretix.base.models import (
|
||||
ItemVariation, OrderPosition, QuestionAnswer, QuestionOption, Seat,
|
||||
ItemVariation, OrderPosition, Question, QuestionAnswer, QuestionOption,
|
||||
Seat,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.settings import (
|
||||
@@ -417,7 +420,7 @@ class AttendeeStreet(ImportColumn):
|
||||
return _('Attendee address') + ': ' + _('Address')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.address = value or ''
|
||||
position.street = value or ''
|
||||
|
||||
|
||||
class AttendeeZip(ImportColumn):
|
||||
@@ -528,7 +531,7 @@ class Secret(ImportColumn):
|
||||
super().__init__(*args)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and (value in self._cached or OrderPosition.all.filter(order__event=self.event, secret=value).exists()):
|
||||
if value and (value in self._cached or OrderPosition.all.filter(order__event__organizer=self.event.organizer, secret=value).exists()):
|
||||
raise ValidationError(
|
||||
_('You cannot assign a position secret that already exists.')
|
||||
)
|
||||
@@ -626,6 +629,22 @@ class Comment(ImportColumn):
|
||||
class QuestionColumn(ImportColumn):
|
||||
def __init__(self, event, q):
|
||||
self.q = q
|
||||
self.option_resolve_cache = defaultdict(set)
|
||||
|
||||
for opt in q.options.all():
|
||||
self.option_resolve_cache[str(opt.id)].add(opt)
|
||||
self.option_resolve_cache[opt.identifier].add(opt)
|
||||
|
||||
if isinstance(opt.answer, LazyI18nString):
|
||||
|
||||
if isinstance(opt.answer.data, dict):
|
||||
for v in opt.answer.data.values():
|
||||
self.option_resolve_cache[v.strip()].add(opt)
|
||||
else:
|
||||
self.option_resolve_cache[opt.answer.strip()].add(opt)
|
||||
|
||||
else:
|
||||
self.option_resolve_cache[opt.answer.strip()].add(opt)
|
||||
super().__init__(event)
|
||||
|
||||
@property
|
||||
@@ -638,7 +657,23 @@ class QuestionColumn(ImportColumn):
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
return self.q.clean_answer(value)
|
||||
if self.q.type == Question.TYPE_CHOICE:
|
||||
if value not in self.option_resolve_cache:
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
if len(self.option_resolve_cache[value]) > 1:
|
||||
raise ValidationError(_('Ambigous option selected.'))
|
||||
return list(self.option_resolve_cache[value])[0]
|
||||
|
||||
elif self.q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
values = value.split(',')
|
||||
if any(v.strip() not in self.option_resolve_cache for v in values):
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
if any(len(self.option_resolve_cache[v.strip()]) > 1 for v in values):
|
||||
raise ValidationError(_('Ambigous option selected.'))
|
||||
return [list(self.option_resolve_cache[v.strip()])[0] for v in values]
|
||||
|
||||
else:
|
||||
return self.q.clean_answer(value)
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
if value:
|
||||
@@ -702,7 +737,7 @@ def get_all_columns(event):
|
||||
SeatColumn(event),
|
||||
Comment(event)
|
||||
]
|
||||
for q in event.questions.exclude(type='F'):
|
||||
for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE):
|
||||
default.append(QuestionColumn(event, q))
|
||||
|
||||
for recv, resp in order_import_columns.send(sender=event):
|
||||
|
||||
@@ -1134,7 +1134,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
cart['raw']
|
||||
)
|
||||
total += sum([f.value for f in fees])
|
||||
remainder = total - gc.value
|
||||
remainder = total
|
||||
if remainder > Decimal('0.00'):
|
||||
del cs['payment']
|
||||
messages.success(request, _("Your gift card has been applied, but {} still need to be paid. Please select a payment method.").format(
|
||||
|
||||
@@ -66,6 +66,7 @@ error_messages = {
|
||||
"%(min)s items of it."),
|
||||
'not_started': _('The presale period for this event has not yet started.'),
|
||||
'ended': _('The presale period for this event has ended.'),
|
||||
'payment_ended': _('All payments for this event need to be confirmed already, so no new orders can be created.'),
|
||||
'some_subevent_not_started': _('The presale period for this event has not yet started. The affected positions '
|
||||
'have been removed from your cart.'),
|
||||
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
|
||||
@@ -169,7 +170,7 @@ class CartManager:
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.event.timezone)
|
||||
if term_last < self.now_dt:
|
||||
raise CartError(error_messages['ended'])
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
|
||||
def _extend_expiry_of_valid_existing_positions(self):
|
||||
# Extend this user's cart session to ensure all items in the cart expire at the same time
|
||||
@@ -304,7 +305,7 @@ class CartManager:
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.event.timezone)
|
||||
if term_last < self.now_dt:
|
||||
raise CartError(error_messages['ended'])
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
if op.item.category and op.item.category.is_addon and not (op.addon_to and op.addon_to != 'FAKE'):
|
||||
@@ -1085,16 +1086,14 @@ def get_fees(event, request, total, invoice_address, provider, positions):
|
||||
if cs.get('gift_cards'):
|
||||
gcs = cs['gift_cards']
|
||||
gc_qs = event.organizer.accepted_gift_cards.filter(pk__in=cs.get('gift_cards'), currency=event.currency)
|
||||
summed = 0
|
||||
for gc in gc_qs:
|
||||
if gc.testmode != event.testmode:
|
||||
gcs.remove(gc.pk)
|
||||
continue
|
||||
fval = Decimal(gc.value) # TODO: don't require an extra query
|
||||
fval = min(fval, total - summed)
|
||||
fval = min(fval, total)
|
||||
if fval > 0:
|
||||
total -= fval
|
||||
summed += fval
|
||||
fees.append(OrderFee(
|
||||
fee_type=OrderFee.FEE_TYPE_GIFTCARD,
|
||||
internal_type='giftcard',
|
||||
|
||||
@@ -7,7 +7,7 @@ import ssl
|
||||
import warnings
|
||||
from email.mime.image import MIMEImage
|
||||
from email.utils import formataddr
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any, Dict, List, Sequence, Union
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
import cssutils
|
||||
@@ -27,7 +27,7 @@ from i18nfield.strings import LazyI18nString
|
||||
from pretix.base.email import ClassicMailRenderer
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
Event, Invoice, InvoiceAddress, Order, OrderPosition, User,
|
||||
CachedFile, Event, Invoice, InvoiceAddress, Order, OrderPosition, User,
|
||||
)
|
||||
from pretix.base.services.invoices import invoice_pdf_task
|
||||
from pretix.base.services.tasks import TransactionAwareTask
|
||||
@@ -52,10 +52,11 @@ class SendMailException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
|
||||
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
|
||||
invoices: list=None, attach_tickets=False, auto_email=True, user=None, attach_ical=False):
|
||||
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any] = None, event: Event = None, locale: str = None,
|
||||
order: Order = None, position: OrderPosition = None, headers: dict = None, sender: str = None,
|
||||
invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None, attach_ical=False,
|
||||
attach_cached_files: Sequence = None):
|
||||
"""
|
||||
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
|
||||
|
||||
@@ -96,6 +97,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
|
||||
:param user: The user this email is sent to
|
||||
|
||||
:param attach_cached_files: A list of cached file to attach to this email.
|
||||
|
||||
:raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean
|
||||
that the email has been sent, just that it has been queued by the email backend.
|
||||
"""
|
||||
@@ -214,7 +217,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
body_html = None
|
||||
|
||||
send_task = mail_send_task.si(
|
||||
to=[email],
|
||||
to=[email] if isinstance(email, str) else list(email),
|
||||
bcc=bcc,
|
||||
subject=subject,
|
||||
body=body_plain,
|
||||
@@ -227,7 +230,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
position=position.pk if position else None,
|
||||
attach_tickets=attach_tickets,
|
||||
attach_ical=attach_ical,
|
||||
user=user.pk if user else None
|
||||
user=user.pk if user else None,
|
||||
attach_cached_files=[cf.id for cf in attach_cached_files] if attach_cached_files else [],
|
||||
)
|
||||
|
||||
if invoices:
|
||||
@@ -255,9 +259,9 @@ class CustomEmail(EmailMultiAlternatives):
|
||||
|
||||
@app.task(base=TransactionAwareTask, bind=True, acks_late=True)
|
||||
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
|
||||
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
|
||||
invoices: List[int]=None, order: int=None, attach_tickets=False, user=None,
|
||||
attach_ical=False) -> bool:
|
||||
event: int = None, position: int = None, headers: dict = None, bcc: List[str] = None,
|
||||
invoices: List[int] = None, order: int = None, attach_tickets=False, user=None,
|
||||
attach_ical=False, attach_cached_files: List[int] = None) -> bool:
|
||||
email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers)
|
||||
if html is not None:
|
||||
html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET)
|
||||
@@ -349,6 +353,19 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
logger.exception('Could not attach invoice to email')
|
||||
pass
|
||||
|
||||
if attach_cached_files:
|
||||
for cf in CachedFile.objects.filter(id__in=attach_cached_files):
|
||||
if cf.file:
|
||||
try:
|
||||
email.attach(
|
||||
cf.filename,
|
||||
cf.file.file.read(),
|
||||
cf.type,
|
||||
)
|
||||
except:
|
||||
logger.exception('Could not attach file to email')
|
||||
pass
|
||||
|
||||
email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order)
|
||||
|
||||
try:
|
||||
|
||||
@@ -91,7 +91,7 @@ def send_notification_mail(notification: Notification, user: User):
|
||||
ctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
'color': '#8E44B3',
|
||||
'color': settings.PRETIX_PRIMARY_COLOR,
|
||||
'notification': notification,
|
||||
'settings_url': build_absolute_uri(
|
||||
'control:user.settings.notifications',
|
||||
|
||||
@@ -103,7 +103,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
|
||||
order._address.name_parts = {'_scheme': event.settings.name_scheme}
|
||||
orders.append(order)
|
||||
|
||||
position = OrderPosition()
|
||||
position = OrderPosition(positionid=len(order._positions) + 1)
|
||||
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
||||
position.meta_info = {}
|
||||
order._positions.append(position)
|
||||
|
||||
@@ -618,8 +618,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
except ItemBundle.MultipleObjectsReturned:
|
||||
raise OrderError("Invalid product configuration (duplicate bundle)")
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
changed_prices[cp.pk] = bprice
|
||||
else:
|
||||
@@ -631,10 +633,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||
max_discount=max_discount)
|
||||
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
|
||||
pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||
max_discount=max_discount)
|
||||
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
|
||||
|
||||
if max_discount is not None:
|
||||
v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
|
||||
@@ -1051,7 +1053,7 @@ def send_download_reminders(sender, **kwargs):
|
||||
download_reminder_sent=False,
|
||||
datetime__lte=now() - timedelta(hours=2),
|
||||
first_date__gte=today,
|
||||
).only('pk', 'event_id').order_by('event_id')
|
||||
).only('pk', 'event_id', 'sales_channel').order_by('event_id')
|
||||
event_id = None
|
||||
days = None
|
||||
event = None
|
||||
@@ -1882,7 +1884,7 @@ class OrderChangeManager:
|
||||
def _reissue_invoice(self):
|
||||
i = self.order.invoices.filter(is_cancellation=False).last()
|
||||
if self.reissue_invoice and self._invoice_dirty:
|
||||
if i:
|
||||
if i and not i.refered.exists():
|
||||
self._invoices.append(generate_cancellation(i))
|
||||
if invoice_qualified(self.order) and \
|
||||
(i or
|
||||
@@ -2205,8 +2207,11 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
|
||||
|
||||
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
|
||||
@receiver(order_changed, dispatch_uid="pretixbase_order_changed_giftcards")
|
||||
@transaction.atomic()
|
||||
def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
||||
if order.status != Order.STATUS_PAID:
|
||||
return
|
||||
any_giftcards = False
|
||||
for p in order.positions.all():
|
||||
if p.item.issue_giftcard:
|
||||
|
||||
@@ -11,6 +11,7 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
def get_price(item: Item, variation: ItemVariation = None,
|
||||
voucher: Voucher = None, custom_price: Decimal = None,
|
||||
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
||||
custom_price_is_tax_rate: Decimal=None,
|
||||
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None,
|
||||
force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00'),
|
||||
max_discount: Decimal = None, tax_rule=None) -> TaxedPrice:
|
||||
@@ -66,7 +67,7 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net',
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross',
|
||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross', gross_price_is_tax_rate=custom_price_is_tax_rate,
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
|
||||
@@ -17,7 +17,7 @@ from pretix.celery_app import app
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask)
|
||||
def export(event: Event, shredders: List[str], session_key=None) -> None:
|
||||
def export(event: Event, shredders: List[str]) -> None:
|
||||
known_shredders = event.get_data_shredders()
|
||||
|
||||
with NamedTemporaryFile() as rawfile:
|
||||
@@ -55,8 +55,6 @@ def export(event: Event, shredders: List[str], session_key=None) -> None:
|
||||
cf.date = now()
|
||||
cf.filename = event.slug + '.zip'
|
||||
cf.type = 'application/zip'
|
||||
cf.session_key = session_key
|
||||
cf.web_download = True
|
||||
cf.expires = now() + timedelta(hours=1)
|
||||
cf.save()
|
||||
cf.file.save(cachedfile_name(cf, cf.filename), rawfile)
|
||||
|
||||
@@ -668,8 +668,8 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': country_choice_kwargs,
|
||||
'form_kwargs': country_choice_kwargs,
|
||||
'serializer_kwargs': lambda: dict(**country_choice_kwargs()),
|
||||
'form_kwargs': lambda: dict(label=_('Country'), **country_choice_kwargs()),
|
||||
},
|
||||
'invoice_address_from_tax_id': {
|
||||
'default': '',
|
||||
@@ -1580,7 +1580,7 @@ Your {event} team"""))
|
||||
'type': bool
|
||||
},
|
||||
'primary_color': {
|
||||
'default': '#8E44B3',
|
||||
'default': settings.PRETIX_PRIMARY_COLOR,
|
||||
'type': str,
|
||||
},
|
||||
'theme_color_success': {
|
||||
@@ -1825,6 +1825,15 @@ Your {event} team"""))
|
||||
'seating_distance_within_row': {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
},
|
||||
'checkout_show_copy_answers_button': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Show button to copy user input from other products"),
|
||||
),
|
||||
}
|
||||
}
|
||||
PERSON_NAME_TITLE_GROUPS = OrderedDict([
|
||||
@@ -1836,7 +1845,7 @@ PERSON_NAME_TITLE_GROUPS = OrderedDict([
|
||||
'Mx',
|
||||
'Dr',
|
||||
'Professor',
|
||||
'Sir'
|
||||
'Sir',
|
||||
))),
|
||||
('german_common', (_('Most common German titles'), (
|
||||
'Dr.',
|
||||
@@ -1844,9 +1853,16 @@ PERSON_NAME_TITLE_GROUPS = OrderedDict([
|
||||
'Prof. Dr.',
|
||||
)))
|
||||
])
|
||||
|
||||
PERSON_NAME_SALUTATIONS = [
|
||||
pgettext_lazy("person_name_salutation", "Ms"),
|
||||
pgettext_lazy("person_name_salutation", "Mr"),
|
||||
]
|
||||
|
||||
PERSON_NAME_SCHEMES = OrderedDict([
|
||||
('given_family', {
|
||||
'fields': (
|
||||
# field_name, label, weight for widget width
|
||||
('given_name', _('Given name'), 1),
|
||||
('family_name', _('Family name'), 1),
|
||||
),
|
||||
@@ -2001,6 +2017,24 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
'_scheme': 'full_transcription',
|
||||
},
|
||||
}),
|
||||
('salutation_title_given_family', {
|
||||
'fields': (
|
||||
('salutation', pgettext_lazy('person_name', 'Salutation'), 1),
|
||||
('title', pgettext_lazy('person_name', 'Title'), 1),
|
||||
('given_name', _('Given name'), 2),
|
||||
('family_name', _('Family name'), 2),
|
||||
),
|
||||
'concatenation': lambda d: ' '.join(
|
||||
str(p) for p in (d.get(key, '') for key in ["title", "given_name", "family_name"]) if p
|
||||
),
|
||||
'sample': {
|
||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
'family_name': pgettext_lazy('person_name_sample', 'Doe'),
|
||||
'_scheme': 'title_salutation_given_family',
|
||||
},
|
||||
}),
|
||||
])
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS = {
|
||||
# Source: http://www.bitboost.com/ref/international-address-formats.html
|
||||
@@ -2016,7 +2050,6 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
|
||||
'US': (['State', 'Outlying area', 'District'], 'short'),
|
||||
}
|
||||
|
||||
|
||||
settings_hierarkey = Hierarkey(attribute_name='settings')
|
||||
|
||||
for k, v in DEFAULTS.items():
|
||||
@@ -2086,7 +2119,7 @@ class SettingsSandbox:
|
||||
def __delattr__(self, key: str) -> None:
|
||||
del self._event.settings[self._convert_key(key)]
|
||||
|
||||
def get(self, key: str, default: Any=None, as_type: type=str):
|
||||
def get(self, key: str, default: Any = None, as_type: type = str):
|
||||
return self._event.settings.get(self._convert_key(key), default=default, as_type=as_type)
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
import bleach
|
||||
@@ -72,10 +71,6 @@ EMAIL_RE = build_email_re(tlds=sorted(tld_set, key=len, reverse=True))
|
||||
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
"""
|
||||
Makes sure that all links to a different domain are passed through a redirection handler
|
||||
to ensure there's no passing of referers with secrets inside them.
|
||||
"""
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
@@ -85,42 +80,7 @@ def safelink_callback(attrs, new=False):
|
||||
return attrs
|
||||
|
||||
|
||||
def truelink_callback(attrs, new=False):
|
||||
"""
|
||||
Tries to prevent "phishing" attacks in which a link looks like it points to a safe place but instead
|
||||
points somewhere else, e.g.
|
||||
|
||||
<a href="https://evilsite.com">https://google.com</a>
|
||||
|
||||
At the same time, custom texts are still allowed:
|
||||
|
||||
<a href="https://maps.google.com">Get to the event</a>
|
||||
|
||||
Suffixes are also allowed:
|
||||
|
||||
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
||||
"""
|
||||
text = re.sub('[^a-zA-Z0-9.-/_]', '', attrs.get('_text')) # clean up link text
|
||||
if URL_RE.match(text):
|
||||
# link text looks like a url
|
||||
if text.startswith('//'):
|
||||
text = 'https:' + text
|
||||
elif not text.startswith('http'):
|
||||
text = 'https://' + text
|
||||
|
||||
text_url = urllib.parse.urlparse(text)
|
||||
href_url = urllib.parse.urlparse(attrs[None, 'href'])
|
||||
if text_url.netloc != href_url.netloc or not href_url.path.startswith(href_url.path):
|
||||
# link text contains an URL that has a different base than the actual URL
|
||||
attrs['_text'] = attrs[None, 'href']
|
||||
return attrs
|
||||
|
||||
|
||||
def abslink_callback(attrs, new=False):
|
||||
"""
|
||||
Makes sure that all links will be absolute links and will be opened in a new page with no
|
||||
window.opener attribute.
|
||||
"""
|
||||
url = attrs.get((None, 'href'), '/')
|
||||
if not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, url)
|
||||
@@ -133,7 +93,6 @@ def markdown_compile_email(source):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
return linker.linkify(bleach.clean(
|
||||
@@ -186,7 +145,7 @@ def rich_text(text: str, **kwargs):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
|
||||
parse_email=True
|
||||
)
|
||||
body_md = linker.linkify(markdown_compile(text))
|
||||
@@ -202,7 +161,7 @@ def rich_text_snippet(text: str, **kwargs):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||
callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]),
|
||||
parse_email=True
|
||||
)
|
||||
body_md = linker.linkify(markdown_compile(text, snippet=True))
|
||||
|
||||
@@ -13,11 +13,7 @@ class DownloadView(TemplateView):
|
||||
@cached_property
|
||||
def object(self) -> CachedFile:
|
||||
try:
|
||||
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
|
||||
if o.session_key:
|
||||
if o.session_key != self.request.session.session_key:
|
||||
raise Http404()
|
||||
return o
|
||||
return get_object_or_404(CachedFile, id=self.kwargs['id'])
|
||||
except ValueError: # Invalid URLs
|
||||
raise Http404()
|
||||
|
||||
|
||||
@@ -53,7 +53,15 @@ class BaseQuestionsViewMixin:
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
files=(self.request.FILES if self.request.method == 'POST' else None))
|
||||
form.pos = cartpos or orderpos
|
||||
form.show_copy_answers_to_addon_button = form.pos.addon_to and set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all())
|
||||
form.show_copy_answers_to_addon_button = form.pos.addon_to and (
|
||||
set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or
|
||||
(form.pos.addon_to.item.admission and form.pos.item.admission and (
|
||||
self.request.event.settings.attendee_names_asked or
|
||||
self.request.event.settings.attendee_emails_asked or
|
||||
self.request.event.settings.attendee_company_asked or
|
||||
self.request.event.settings.attendee_addresses_asked
|
||||
))
|
||||
)
|
||||
if len(form.fields) > 0:
|
||||
formlist.append(form)
|
||||
return formlist
|
||||
|
||||
@@ -83,6 +83,7 @@ class SimpleCheckinListForm(forms.ModelForm):
|
||||
'all_products',
|
||||
'limit_products',
|
||||
'include_pending',
|
||||
'allow_entry_after_exit',
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
|
||||
@@ -522,6 +522,7 @@ class EventSettingsForm(SettingsForm):
|
||||
'banner_text_bottom',
|
||||
'order_email_asked_twice',
|
||||
'last_order_modification_date',
|
||||
'checkout_show_copy_answers_button',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -287,11 +287,11 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
for i in self.event.items.prefetch_related('variations').all():
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=str(i))))
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||
else:
|
||||
choices.append((str(i.pk), i.name))
|
||||
choices.append((str(i.pk), str(i)))
|
||||
self.fields['item'].choices = choices
|
||||
|
||||
def filter_qs(self, qs):
|
||||
@@ -827,6 +827,8 @@ class CheckInFilterForm(FilterForm):
|
||||
'-item': ('-item__name', '-variation__value', '-order__code'),
|
||||
'seat': ('seat__sorting_rank', 'seat__guid'),
|
||||
'-seat': ('-seat__sorting_rank', '-seat__guid'),
|
||||
'date': ('subevent__date_from', 'order__code'),
|
||||
'-date': ('-subevent__date_from', '-order__code'),
|
||||
'name': {'_order': F('display_name').asc(nulls_first=True),
|
||||
'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached')},
|
||||
'-name': {'_order': F('display_name').desc(nulls_last=True),
|
||||
|
||||
@@ -193,7 +193,7 @@ class DeviceForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'all_events', 'limit_events']
|
||||
fields = ['name', 'all_events', 'limit_events', 'security_profile']
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events',
|
||||
|
||||
@@ -19,6 +19,12 @@ def render_label(content, label_for=None, label_class=None, label_title='', opti
|
||||
attrs['class'] = label_class
|
||||
if label_title:
|
||||
attrs['title'] = label_title
|
||||
|
||||
if text_value(content) == ' ':
|
||||
# Empty label, e.g. checkbox
|
||||
attrs.setdefault('class', '')
|
||||
attrs['class'] += ' label-empty'
|
||||
|
||||
builder = '<{tag}{attrs}>{content}{opt}</{tag}>'
|
||||
return format_html(
|
||||
builder,
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import formset_factory
|
||||
from django.urls import reverse
|
||||
from django.utils.dates import MONTHS, WEEKDAYS
|
||||
@@ -382,12 +381,6 @@ class TimeForm(forms.Form):
|
||||
required=False
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('time_from') and d.get('time_to') and d['time_from'] > d['time_to']:
|
||||
raise ValidationError({'time_to': _('The end of the event has to be later than its start.')})
|
||||
return d
|
||||
|
||||
|
||||
TimeFormSet = formset_factory(
|
||||
TimeForm,
|
||||
|
||||
@@ -128,7 +128,7 @@ class VoucherForm(I18nModelForm):
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
|
||||
if not self._errors and self.data.get('itemvar'):
|
||||
if not self._errors:
|
||||
try:
|
||||
itemid = quotaid = None
|
||||
iv = self.data.get('itemvar', '')
|
||||
@@ -136,8 +136,10 @@ class VoucherForm(I18nModelForm):
|
||||
quotaid = iv[2:]
|
||||
elif '-' in iv:
|
||||
itemid, varid = iv.split('-')
|
||||
else:
|
||||
elif iv:
|
||||
itemid, varid = iv, None
|
||||
else:
|
||||
itemid, varid = None, None
|
||||
|
||||
if itemid:
|
||||
self.instance.item = self.instance.event.items.get(pk=itemid)
|
||||
@@ -146,11 +148,15 @@ class VoucherForm(I18nModelForm):
|
||||
else:
|
||||
self.instance.variation = None
|
||||
self.instance.quota = None
|
||||
|
||||
else:
|
||||
elif quotaid:
|
||||
self.instance.quota = self.instance.event.quotas.get(pk=quotaid)
|
||||
self.instance.item = None
|
||||
self.instance.variation = None
|
||||
else:
|
||||
self.instance.quota = None
|
||||
self.instance.item = None
|
||||
self.instance.variation = None
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError(_("Invalid product selected."))
|
||||
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
<li>{{ scope }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>{% trans "Please select the organizer accounts this application should get access to:" %}</p>
|
||||
{% bootstrap_field form.organizers layout="inline" %}
|
||||
{% if form.organizers %}
|
||||
<p>{% trans "Please select the organizer accounts this application should get access to:" %}</p>
|
||||
{% bootstrap_field form.organizers layout="inline" %}
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
<p class="text-danger">
|
||||
|
||||
@@ -10,6 +10,23 @@
|
||||
<dd>Stripe Terminal</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.payment_intent }}</dd>
|
||||
{% elif payment_info.payment_type == "terminal_zvt" %}
|
||||
<dt>{% trans "Payment provider" %}</dt>
|
||||
<dd>{% trans "ZVT Terminal" %}</dd>
|
||||
<dt>{% trans "Trace number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.traceNumber }}</dd>
|
||||
<dt>{% trans "Payment type" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.paymentType }}</dd>
|
||||
<dt>{% trans "Additional text" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.additionalText }}</dd>
|
||||
<dt>{% trans "Turnover number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.turnoverNumber }}</dd>
|
||||
<dt>{% trans "Receipt number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.receiptNumber }}</dd>
|
||||
<dt>{% trans "Card type" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.cardName }}</dd>
|
||||
<dt>{% trans "Card expiration" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.expiry }}</dd>
|
||||
{% elif payment_info.payment_type == "sumup" %}
|
||||
<dt>{% trans "Payment provider" %}</dt>
|
||||
<dd>SumUp</dd>
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'item'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
{% if request.event.has_subevents and not checkinlist.subevent %}
|
||||
<th>{% trans "Date" context "subevents" %} <a href="?{% url_replace request 'ordering' '-date'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'date'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
{% endif %}
|
||||
{% if seats %}
|
||||
<th>{% trans "Seat" %} <a href="?{% url_replace request 'ordering' '-seat'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'seat'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
@@ -99,6 +103,11 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ e.item }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||
{% if request.event.has_subevents and not checkinlist.subevent %}
|
||||
<td>
|
||||
{{ e.subevent.name }} – {{ e.subevent.get_date_range_display }} {{ e.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if seats %}
|
||||
<td>{{ e.seat|default_if_none:"" }}</td>
|
||||
{% endif %}
|
||||
|
||||
@@ -98,7 +98,9 @@
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
{% if cl.subevent %}
|
||||
<td>{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }}</td>
|
||||
<td>
|
||||
{{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }} {{ cl.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<em>{% trans "All" %}</em>
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
{% bootstrap_field sform.attendee_company_required layout="control" %}
|
||||
{% bootstrap_field sform.attendee_addresses_asked layout="control" %}
|
||||
{% bootstrap_field sform.attendee_addresses_required layout="control" %}
|
||||
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Texts" %}</legend>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Quotas" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Quotas" %}</h1>
|
||||
@@ -41,12 +42,21 @@
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Quota name" %}</th>
|
||||
<th>{% trans "Quota name" %}
|
||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>{% trans "Date" context "subevent" %}</th>
|
||||
<th>{% trans "Date" context "subevent" %}
|
||||
<a href="?{% url_replace request 'ordering' '-date' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'date' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Total capacity" %}</th>
|
||||
<th>{% trans "Total capacity" %}
|
||||
<a href="?{% url_replace request 'ordering' '-size' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'size' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>{% trans "Capacity left" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
@@ -71,7 +81,9 @@
|
||||
</ul>
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>{{ q.subevent.name }} – {{ q.subevent.get_date_range_display }}</td>
|
||||
<td>
|
||||
{{ q.subevent.name }} – {{ q.subevent.get_date_range_display }} {{ q.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
||||
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<td>
|
||||
{{ r.payment_provider.verbose_name }}
|
||||
</td>
|
||||
<td>{{ o.created|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>{{ r.created|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>{{ r.get_source_display }}</td>
|
||||
<td>
|
||||
<span class="label label-{% if r.state == "external" or r.state == "transit" or r.state == "created" %}warning{% elif r.state == "done" %}success{% else %}danger{% endif %}">
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.all_events layout="control" %}
|
||||
{% bootstrap_field form.limit_events layout="control" %}
|
||||
{% bootstrap_field form.security_profile layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -508,6 +508,7 @@
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -536,6 +537,7 @@
|
||||
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -220,6 +221,7 @@
|
||||
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -74,15 +74,13 @@ def login(request):
|
||||
backend = [b for b in backends if b.visible][0]
|
||||
if request.user.is_authenticated:
|
||||
next_url = backend.get_next_url(request) or 'control:index'
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect(next_url)
|
||||
return redirect(reverse('control:index'))
|
||||
return redirect(next_url)
|
||||
if request.method == 'POST':
|
||||
form = LoginForm(backend=backend, data=request.POST, request=request)
|
||||
form = LoginForm(backend=backend, data=request.POST)
|
||||
if form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier:
|
||||
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
|
||||
else:
|
||||
form = LoginForm(backend=backend, request=request)
|
||||
form = LoginForm(backend=backend)
|
||||
ctx['form'] = form
|
||||
ctx['can_register'] = settings.PRETIX_REGISTRATION
|
||||
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
|
||||
|
||||
@@ -698,6 +698,19 @@ class QuotaList(PaginationMixin, ListView):
|
||||
if self.request.GET.get("subevent", "") != "":
|
||||
s = self.request.GET.get("subevent", "")
|
||||
qs = qs.filter(subevent_id=s)
|
||||
|
||||
valid_orders = {
|
||||
'-date': ('-subevent__date_from', 'name'),
|
||||
'date': ('subevent__date_from', '-name'),
|
||||
'size': ('size', 'name'),
|
||||
'-size': ('-size', '-name'),
|
||||
'name': ('name',),
|
||||
'-name': ('-name',),
|
||||
}
|
||||
|
||||
if self.request.GET.get("ordering", "-date") in valid_orders:
|
||||
qs = qs.order_by(*valid_orders[self.request.GET.get("ordering", "-date")])
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -1956,9 +1956,9 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
cf = CachedFile(web_download=True, session_key=request.session.session_key)
|
||||
cf = CachedFile()
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
cf.expires = now() + timedelta(days=3)
|
||||
cf.save()
|
||||
return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data)
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Max, Min, Prefetch, ProtectedError, Sum
|
||||
from django.db.models import (
|
||||
Count, Max, Min, OuterRef, Prefetch, ProtectedError, Subquery, Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.forms import DecimalField, inlineformset_factory
|
||||
from django.http import JsonResponse
|
||||
@@ -30,7 +32,9 @@ from pretix.base.models import (
|
||||
TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
||||
from pretix.base.models.giftcards import gen_giftcard_secret
|
||||
from pretix.base.models.giftcards import (
|
||||
GiftCardTransaction, gen_giftcard_secret,
|
||||
)
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.export import multiexport
|
||||
@@ -967,8 +971,11 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
||||
context_object_name = 'giftcards'
|
||||
|
||||
def get_queryset(self):
|
||||
s = GiftCardTransaction.objects.filter(
|
||||
card=OuterRef('pk')
|
||||
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
||||
qs = self.request.organizer.issued_gift_cards.annotate(
|
||||
cached_value=Coalesce(Sum('transactions__value'), Decimal('0.00'))
|
||||
cached_value=Coalesce(Subquery(s), Decimal('0.00'))
|
||||
).order_by('-issuance')
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
@@ -1238,9 +1245,9 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, V
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
cf = CachedFile(web_download=True, session_key=request.session.session_key)
|
||||
cf = CachedFile()
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
cf.expires = now() + timedelta(days=3)
|
||||
cf.save()
|
||||
return self.do(
|
||||
organizer=self.request.organizer.id,
|
||||
|
||||
@@ -137,7 +137,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
buffer = BytesIO()
|
||||
p.write(buffer)
|
||||
buffer.seek(0)
|
||||
c = CachedFile(web_download=True)
|
||||
c = CachedFile()
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
@@ -162,7 +162,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
"status": "error",
|
||||
"error": error
|
||||
})
|
||||
c = CachedFile(web_download=True)
|
||||
c = CachedFile()
|
||||
c.expires = now() + timedelta(days=7)
|
||||
c.date = now()
|
||||
c.filename = 'background_preview.pdf'
|
||||
|
||||
@@ -75,7 +75,7 @@ class ShredExportView(RecentAuthenticationRequiredMixin, EventPermissionRequired
|
||||
if constr:
|
||||
return self.error(ShredError(self.get_error_url()))
|
||||
|
||||
return self.do(self.request.event.id, request.POST.getlist("shredder"), self.request.session.session_key)
|
||||
return self.do(self.request.event.id, request.POST.getlist("shredder"))
|
||||
|
||||
|
||||
class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
|
||||
from django.contrib import messages
|
||||
@@ -739,11 +739,15 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
se = copy.copy(form.instance)
|
||||
|
||||
se.date_from = make_aware(datetime.combine(rdate, t['time_from']), tz)
|
||||
se.date_to = (
|
||||
make_aware(datetime.combine(rdate, t['time_to']), tz)
|
||||
if t.get('time_to')
|
||||
else None
|
||||
)
|
||||
|
||||
if t.get('time_to'):
|
||||
se.date_to = (
|
||||
make_aware(datetime.combine(rdate, t['time_to']), tz)
|
||||
if t.get('time_to') > t.get('time_from')
|
||||
else make_aware(datetime.combine(rdate + timedelta(days=1), t['time_to']), tz)
|
||||
)
|
||||
else:
|
||||
se.date_to = None
|
||||
se.date_admission = (
|
||||
make_aware(datetime.combine(rdate, t['time_admission']), tz)
|
||||
if t.get('time_admission')
|
||||
|
||||
@@ -114,6 +114,9 @@ class ReauthView(TemplateView):
|
||||
u = backend.request_authenticate(request)
|
||||
if u and u == request.user:
|
||||
next_url = backend.get_next_url(request)
|
||||
t = int(time.time())
|
||||
request.session['pretix_auth_login_time'] = t
|
||||
request.session['pretix_auth_last_used'] = t
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect(next_url)
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
|
||||
def date_fromisocalendar(isoyear, isoweek, isoday):
|
||||
if sys.version_info < (3, 8):
|
||||
return datetime.datetime.strptime(f'{isoyear}-W{isoweek}-{isoday}', "%G-W%V-%u")
|
||||
else:
|
||||
return datetime.datetime.fromisocalendar(isoyear, isoweek, isoday)
|
||||
+1101
-1004
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-07-30 19:00+0000\n"
|
||||
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -48,16 +48,16 @@ msgstr "إجمالي الإيرادات"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "الاتصال الشريط ..."
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "مجموع"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "تأكيد الدفع الخاص بك ..."
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "الاتصال البنك الذي تتعامل معه ..."
|
||||
|
||||
@@ -366,30 +366,30 @@ msgstr[5] "سيتم إلغاء الحجز تلقائيا بعد {num} دقيقة
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "الرجاء إدخال كمية التذاكر لأحد أنواع التذاكر."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "من٪ (العملة) ق٪ (سعر) ق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "من٪ (العملة) ق٪ (سعر) ق"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -45,16 +45,16 @@ msgstr ""
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr ""
|
||||
|
||||
@@ -335,24 +335,24 @@ msgstr[1] ""
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -45,16 +45,16 @@ msgstr ""
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr ""
|
||||
|
||||
@@ -337,24 +337,24 @@ msgstr[2] ""
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1009
-1036
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"PO-Revision-Date: 2020-05-19 09:19+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-09-15 02:00+0000\n"
|
||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"da/>\n"
|
||||
@@ -46,16 +46,16 @@ msgstr "Omsætning i alt"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontakter Stripe …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "Bekræfter din betaling …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Kontakter din bank …"
|
||||
|
||||
@@ -312,11 +312,11 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:755
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
msgstr "Klik for at lukke"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:770
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
msgstr "Du har ændringer, der ikke er gemt!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
|
||||
#, fuzzy
|
||||
@@ -334,7 +334,7 @@ msgstr "Antal"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:131
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
msgstr "Ja"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:132
|
||||
#, fuzzy
|
||||
@@ -366,32 +366,32 @@ msgstr[1] "Varerne i din kurv er reserveret for dig i {num} minutter."
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "fra %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "fra %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
msgstr "Tidszone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
msgstr "Din lokaltid:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:17
|
||||
msgctxt "widget"
|
||||
@@ -452,7 +452,7 @@ msgstr "tilgængelig: %s"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
msgctxt "widget"
|
||||
msgid "Only available with a voucher"
|
||||
msgstr "Kun tilgængelig med en voucher"
|
||||
msgstr "Kun tilgængelig med en rabatkode"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||
#, javascript-format
|
||||
@@ -497,7 +497,7 @@ msgstr "Fortsæt booking"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr "Indløs voucher"
|
||||
msgstr "Indløs rabatkode"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
@@ -507,7 +507,7 @@ msgstr "Indløs"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr "Voucherkode"
|
||||
msgstr "Rabatkode"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
@@ -552,7 +552,7 @@ msgstr "Forrige måned"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgctxt "widget"
|
||||
msgid "Next week"
|
||||
msgstr ""
|
||||
msgstr "Næste uge"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
#, fuzzy
|
||||
@@ -565,7 +565,7 @@ msgstr "Forrige måned"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgctxt "widget"
|
||||
msgid "Open seat selection"
|
||||
msgstr ""
|
||||
msgstr "Åbn sædevalg"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Mo"
|
||||
|
||||
+1096
-1008
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -47,16 +47,16 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Gesamt"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "Zahlung wird bestätigt …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Kontaktiere Ihre Bank …"
|
||||
|
||||
@@ -359,24 +359,24 @@ msgstr[1] ""
|
||||
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:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "Der Veranstalter behält %(currency)s %(amount)s ein"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "Sie erhalten %(currency)s %(amount)s zurück"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
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:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
@@ -47,16 +47,16 @@ msgstr "Gesamtumsatz"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kontaktiere Stripe …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Gesamt"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "Zahlung wird bestätigt …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Kontaktiere deine Bank …"
|
||||
|
||||
@@ -358,24 +358,24 @@ msgstr[1] ""
|
||||
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:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "Der Veranstalter behält %(currency)s %(amount)s ein"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "Du erhältst %(currency)s %(amount)s zurück"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
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:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
+885
-983
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -46,16 +46,16 @@ msgstr ""
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr ""
|
||||
|
||||
@@ -336,24 +336,24 @@ msgstr[1] ""
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1074
-1010
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -47,16 +47,16 @@ msgstr "Συνολικά κέρδη"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Επικοινωνία με το Stripe …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Σύνολο"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
#, fuzzy
|
||||
#| msgid "Contacting Stripe …"
|
||||
msgid "Contacting your bank …"
|
||||
@@ -376,30 +376,30 @@ msgstr[1] "Τα είδη στο καλάθι θα παραμείνουν δεσ
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "απο %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "απο %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1115
-1019
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-04-27 20:00+0000\n"
|
||||
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -47,16 +47,16 @@ msgstr "Ingresos totales"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Contactando con Stripe…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "Confirmando el pago…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Contactando con el banco…"
|
||||
|
||||
@@ -373,30 +373,30 @@ msgstr[1] ""
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Por favor, introduce un valor para cada tipo de entrada."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "a partir de %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "a partir de %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+1083
-1026
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"PO-Revision-Date: 2020-02-26 03:00+0000\n"
|
||||
"Last-Translator: David100mark <david.hundertmark@gmx.net>\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-09-15 17:00+0000\n"
|
||||
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"fr/>\n"
|
||||
"Language: fr\n"
|
||||
@@ -46,16 +46,16 @@ msgstr "chiffre d'affaires total"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Contacter Stripe …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "Confirmation de votre paiment…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Communication avec votre banque …"
|
||||
|
||||
@@ -164,7 +164,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:54
|
||||
msgid "Product"
|
||||
msgstr ""
|
||||
msgstr "Produit"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:58
|
||||
#, fuzzy
|
||||
@@ -369,30 +369,30 @@ msgstr[1] "Les articles de votre panier vous sont réservés pour {num} minutes.
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "SVP entrez une quantité pour un de vos types de billets."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "de %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "de %(currency)s %(price)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
|
||||
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -47,16 +47,16 @@ msgstr "Teljes bevétel"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Kapcsolatfelvétel Stripe-pal…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Teljes"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "A fizetés megerősítése…"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Kapcsolatfelvétel a bankjával…"
|
||||
|
||||
@@ -364,30 +364,30 @@ msgstr[1] "A kosár tartalma {num} percig foglalva van számodra."
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Adjon meg egy mennyiséget az egyik jegytípusból."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "%(currency) %(price)-tól"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid "from %(currency)s %(price)s"
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "%(currency) %(price)-tól"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-14 12:00+0000\n"
|
||||
"POT-Creation-Date: 2020-10-13 16:13+0000\n"
|
||||
"PO-Revision-Date: 2020-06-12 20:00+0000\n"
|
||||
"Last-Translator: Frank <webappconcept@gmail.com>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -47,16 +47,16 @@ msgstr "Ricavi totali"
|
||||
msgid "Contacting Stripe …"
|
||||
msgstr "Sto contattando Stripe …"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:57
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Totale"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:146
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:177
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
msgid "Confirming your payment …"
|
||||
msgstr "Stiamo processando il tuo pagamento..."
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:153
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Sto contattando la tua banca …"
|
||||
|
||||
@@ -363,24 +363,24 @@ msgstr[1] "Gli elementi nel tuo carrello sono riservati per {num} minuti."
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Inserisci la quantità per una tipologia di biglietto."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:387
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:386
|
||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||
msgstr "L'organizzatore trattiene %(currency)s %(amount)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:395
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:394
|
||||
msgid "You get %(currency)s %(amount)s back"
|
||||
msgstr "Ricevi indietro %(currency)s %(amount)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:411
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:410
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Inserisci l'importo che l'organizzatore può trattenere."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:425
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:438
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:424
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:430
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:429
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user