forked from CGM_Public/pretix_original
Compare commits
3 Commits
v3.15.0
...
validate-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5a2286a4 | ||
|
|
3d66bfee7f | ||
|
|
0b495a4070 |
@@ -95,12 +95,6 @@ pretix_model_instances
|
||||
the ``model`` name. Starting with pretix 3.11, these numbers might only be approximate for
|
||||
most tables when running on PostgreSQL to mitigate performance impact.
|
||||
|
||||
pretix_celery_tasks_queued_count
|
||||
The number of background tasks in the worker queue, labeled with ``queue``.
|
||||
|
||||
pretix_celery_tasks_queued_age_seconds
|
||||
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
||||
|
||||
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||
.. _Prometheus: https://prometheus.io/
|
||||
.. _cProfile: https://docs.python.org/3/library/profile.html
|
||||
|
||||
@@ -183,9 +183,6 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
|
||||
constructed from a number of
|
||||
days before the base point
|
||||
and the base point.
|
||||
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
|
||||
specifiers in requests
|
||||
(see below).
|
||||
===================== ============================ ===================================
|
||||
|
||||
Query parameters
|
||||
@@ -230,48 +227,4 @@ We store idempotency keys for 24 hours, so you should never retry a request afte
|
||||
All ``POST``, ``PUT``, ``PATCH``, or ``DELETE`` api calls support idempotency keys. Adding an idempotency key to a
|
||||
``GET``, ``HEAD``, or ``OPTIONS`` request has no effect.
|
||||
|
||||
|
||||
File upload
|
||||
-----------
|
||||
|
||||
In some places, the API supports working with files, for example when setting the picture of a product. In this case,
|
||||
you will first need to make a separate request to our file upload endpoint:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/upload HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
|
||||
Content-Type: image/png
|
||||
Content-Disposition: attachment; filename="logo.png"
|
||||
Content-Length: 1234
|
||||
|
||||
<raw file content>
|
||||
|
||||
Note that the ``Content-Type`` and ``Content-Disposition`` headers are required. If the upload was successful, you will
|
||||
receive a JSON response with the ID of the file:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
|
||||
}
|
||||
|
||||
You can then use this file ID in the request you want to use it in. File IDs are currently valid for 24 hours and can only
|
||||
be used using the same authorization method and user that was used to upload them.
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/test/events/test/items/3/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"picture": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
|
||||
}
|
||||
|
||||
|
||||
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
||||
|
||||
@@ -36,8 +36,8 @@ admission boolean ``true`` for it
|
||||
(such as primary tickets) and ``false`` for others
|
||||
(such as add-ons or merchandise).
|
||||
position integer An integer, used for sorting
|
||||
picture file A product picture to be displayed in the shop
|
||||
(can be ``null``).
|
||||
picture string A product picture to be displayed in the shop
|
||||
(read-only, can be ``null``).
|
||||
sales_channels list of strings Sales channels this product is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
|
||||
@@ -325,8 +325,7 @@ state string Payment state,
|
||||
source string How this refund has been created, one of ``buyer``, ``admin``, or ``external``
|
||||
amount money (string) Payment amount
|
||||
created datetime Date and time of creation of this payment
|
||||
comment string Reason for refund (shown to the customer in some cases, can be ``null``).
|
||||
execution_date datetime Date and time of completion of this refund (or ``null``)
|
||||
payment_date datetime Date and time of completion of this payment (or ``null``)
|
||||
provider string Identification string of the payment provider
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -1707,67 +1706,6 @@ Order position ticket download
|
||||
Manipulating individual positions
|
||||
---------------------------------
|
||||
|
||||
.. versionchanged:: 3.15
|
||||
|
||||
The ``PATCH`` method has been added for individual positions.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||
|
||||
Updates specific fields on an order position. Currently, only the following fields are supported:
|
||||
|
||||
* ``attendee_email``
|
||||
|
||||
* ``attendee_name_parts`` or ``attendee_name``
|
||||
|
||||
* ``company``
|
||||
|
||||
* ``street``
|
||||
|
||||
* ``zipcode``
|
||||
|
||||
* ``city``
|
||||
|
||||
* ``country``
|
||||
|
||||
* ``state``
|
||||
|
||||
* ``answers``: If specified, you will need to provide **all** answers for this order position.
|
||||
Validation is handled the same way as when creating orders through the API. You are therefore
|
||||
expected to provide ``question``, ``answer``, and possibly ``options``. ``question_identifier``
|
||||
and ``option_identifiers`` will be ignored.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"attendee_email": "other@example.org"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full order resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param id: The ``id`` field of the order position to update
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||
|
||||
Deletes an order position, identified by its internal ID.
|
||||
@@ -2120,7 +2058,6 @@ Order refund endpoints
|
||||
"payment": 1,
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": "2017-12-04T12:13:12Z",
|
||||
"comment": "Cancellation",
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
]
|
||||
@@ -2163,7 +2100,6 @@ Order refund endpoints
|
||||
"payment": 1,
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": "2017-12-04T12:13:12Z",
|
||||
"comment": "Cancellation",
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
|
||||
@@ -2198,7 +2134,6 @@ Order refund endpoints
|
||||
"amount": "23.00",
|
||||
"payment": 1,
|
||||
"execution_date": null,
|
||||
"comment": "Cancellation",
|
||||
"provider": "manual",
|
||||
"mark_canceled": false,
|
||||
"mark_pending": true
|
||||
@@ -2220,7 +2155,6 @@ Order refund endpoints
|
||||
"payment": 1,
|
||||
"created": "2017-12-01T10:00:00Z",
|
||||
"execution_date": null,
|
||||
"comment": "Cancellation",
|
||||
"provider": "manual"
|
||||
}
|
||||
|
||||
|
||||
@@ -106,22 +106,14 @@ The provider class
|
||||
|
||||
.. automethod:: payment_control_render
|
||||
|
||||
.. automethod:: payment_control_render_short
|
||||
|
||||
.. automethod:: payment_refund_supported
|
||||
|
||||
.. automethod:: payment_partial_refund_supported
|
||||
|
||||
.. automethod:: payment_presale_render
|
||||
|
||||
.. automethod:: execute_refund
|
||||
|
||||
.. automethod:: refund_control_render
|
||||
|
||||
.. automethod:: new_refund_control_form_render
|
||||
|
||||
.. automethod:: new_refund_control_form_process
|
||||
|
||||
.. automethod:: api_payment_details
|
||||
|
||||
.. automethod:: matching_id
|
||||
|
||||
@@ -64,35 +64,20 @@ is valid in every text):
|
||||
Placeholder Description
|
||||
============================== ===============================================================================
|
||||
event The event name
|
||||
event_slug The event's short form
|
||||
code In case of the waiting list, the voucher code to redeem
|
||||
currency The currency used for the event (three-letter code)
|
||||
total The order's total value
|
||||
total_with_currency The order's total value with a localized currency sign
|
||||
refund_amount (For cancellation emails) The amount of money that will be refunded, including
|
||||
the currency
|
||||
currency The currency used for the event (three-letter code)
|
||||
payment_info Information text specific to the payment method (e.g. banking details)
|
||||
url An URL pointing to the download/status page of the order
|
||||
url_info_change An URL pointing to the page of the order that can be used to change ticket
|
||||
information
|
||||
url_products_change An URL pointing to the page of the order that can be used to change the products
|
||||
in the order
|
||||
url_cancel An URL pointing to the page of the order that can be used to cancel the order
|
||||
name, name_* Any name that can be used to address the recipient (e.g. name from invoice address,
|
||||
name from first ticket, …)
|
||||
invoice_name, invoice_name_* The name field of the invoice address
|
||||
invoice_name The name field of the invoice address
|
||||
invoice_company The company field of the invoice address
|
||||
attendee_name, attendee_name_* The name of the attendee represented by the ticket
|
||||
expire_date The order's expiration date
|
||||
comment When rejecting an order, this will contain the reason for the rejection
|
||||
date The same as ``expire_date``, but in a different e-mail (for backwards
|
||||
compatibility)
|
||||
orders A list of orders including links to their status pages, specific to the "resend
|
||||
link (requested by user)" e-mail
|
||||
code In case of the waiting list, the voucher code to redeem
|
||||
hours In case of the waiting list, the number of hours the voucher code is valid
|
||||
product In case of the waiting list, the product that has become available
|
||||
voucher_list When sending out vouchers in bulk, this will be replaced with the list of
|
||||
vouchers
|
||||
============================== ===============================================================================
|
||||
|
||||
The different e-mails are explained in the following:
|
||||
|
||||
@@ -304,92 +304,8 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
* If you use the campaigns plugin, you can pass a campaign ID as a value to ``data-campaign``. This way, all orders
|
||||
made through this widget will be counted towards this campaign.
|
||||
|
||||
* If you use the tracking plugin, you can enable cross-domain tracking. To do so, you need to initialize the
|
||||
pretix-widget manually. Use the html code to embed the widget and add one the following code snippets. Make sure to
|
||||
replace all occurrences of <MEASUREMENT_ID> with your Google Analytics MEASUREMENT_ID (UA-XXXXXXX-X or G-XXXXXXXX)
|
||||
|
||||
Please also make sure to add the embedding website to your `Referral exclusions
|
||||
<https://support.google.com/analytics/answer/2795830>`_ in your Google Analytics settings.
|
||||
|
||||
If you use Google Analytics 4 (GA4 – G-XXXXXXXX)::
|
||||
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
|
||||
<script type="text/javascript">
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<MEASUREMENT_ID>');
|
||||
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.build_widgets = false;
|
||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||
if (!window['google_tag_manager']) {
|
||||
window.PretixWidget.buildWidgets();
|
||||
return;
|
||||
}
|
||||
|
||||
var clientId;
|
||||
var sessionId;
|
||||
var loadingTimeout;
|
||||
function build() {
|
||||
// use loadingTimeout to make sure build() is only called once
|
||||
if (!loadingTimeout) return;
|
||||
window.clearTimeout(loadingTimeout);
|
||||
loadingTimeout = null;
|
||||
if (clientId) window.PretixWidget.widget_data["tracking-ga-id"] = clientId;
|
||||
if (sessionId) window.PretixWidget.widget_data["tracking-ga-sessid"] = sessionId;
|
||||
window.PretixWidget.buildWidgets();
|
||||
};
|
||||
// make sure to build pretix-widgets if gtag fails to load either client_id or session_id
|
||||
loadingTimeout = window.setTimeout(build, 2000);
|
||||
|
||||
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
||||
clientId = id;
|
||||
if (sessionId !== undefined) build();
|
||||
});
|
||||
gtag('get', '<MEASUREMENT_ID>', 'session_id', function(id) {
|
||||
sessionId = id;
|
||||
if (clientId !== undefined) build();
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
If you use Universal Analytics with ``gtag.js`` (UA-XXXXXXX-X)::
|
||||
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<MEASUREMENT_ID>"></script>
|
||||
<script type="text/javascript">
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<MEASUREMENT_ID>');
|
||||
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.build_widgets = false;
|
||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||
if (!window['google_tag_manager']) {
|
||||
window.PretixWidget.buildWidgets();
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure to build pretix-widgets if gtag fails to load client_id
|
||||
var loadingTimeout = window.setTimeout(function() {
|
||||
loadingTimeout = null;
|
||||
window.PretixWidget.buildWidgets();
|
||||
}, 1000);
|
||||
|
||||
gtag('get', '<MEASUREMENT_ID>', 'client_id', function(id) {
|
||||
if (loadingTimeout) {
|
||||
window.clearTimeout(loadingTimeout);
|
||||
window.PretixWidget.widget_data["tracking-ga-id"] = id;
|
||||
window.PretixWidget.buildWidgets();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
If you use ```analytics.js` (Universal Analytics)::
|
||||
* If you use the tracking plugin, you can pass a Google Analytics User ID to enable cross-domain tracking. This will
|
||||
require you to dynamically load the widget, like this::
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
@@ -397,33 +313,27 @@ Hosted or pretix Enterprise are active, you can pass the following fields:
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '<MEASUREMENT_ID>', 'auto');
|
||||
ga('create', 'UA-XXXXXX-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
window.pretixWidgetCallback = function () {
|
||||
window.PretixWidget.build_widgets = false;
|
||||
window.addEventListener('load', function() { // Wait for GA to be loaded
|
||||
if (!window['ga'] || !ga.create) {
|
||||
// Tracking is probably blocked
|
||||
window.PretixWidget.buildWidgets()
|
||||
return;
|
||||
}
|
||||
|
||||
var loadingTimeout = window.setTimeout(function() {
|
||||
loadingTimeout = null;
|
||||
window.PretixWidget.buildWidgets();
|
||||
}, 1000);
|
||||
ga(function(tracker) {
|
||||
if (loadingTimeout) {
|
||||
window.clearTimeout(loadingTimeout);
|
||||
if(window.ga && ga.create) {
|
||||
ga(function(tracker) {
|
||||
window.PretixWidget.widget_data["tracking-ga-id"] = tracker.get('clientId');
|
||||
window.PretixWidget.buildWidgets();
|
||||
}
|
||||
});
|
||||
window.PretixWidget.buildWidgets()
|
||||
});
|
||||
} else { // Tracking is probably blocked
|
||||
window.PretixWidget.buildWidgets()
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
In some combinations with Google Tag Manager, the widget does not load this way. In this case, try replacing
|
||||
``tracker.get('clientId')`` with ``ga.getAll()[0].get('clientId')``.
|
||||
|
||||
|
||||
.. versionchanged:: 2.3
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.15.0"
|
||||
__version__ = "3.15.0.dev0"
|
||||
|
||||
@@ -42,7 +42,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
)
|
||||
|
||||
|
||||
@@ -69,7 +68,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
)
|
||||
|
||||
|
||||
@@ -115,7 +113,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'plugins:pretix_seating:event.event.subevent'),
|
||||
('GET', 'plugins:pretix_seating:event.plan'),
|
||||
('GET', 'plugins:pretix_seating:selection.simple'),
|
||||
('POST', 'api-v1:upload'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -89,38 +89,10 @@ class EventCRUDPermission(EventPermission):
|
||||
class ProfilePermission(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
|
||||
if not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
assert_session_valid(request)
|
||||
except SessionInvalid:
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
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
|
||||
|
||||
|
||||
class AnyAuthenticatedClientPermission(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
|
||||
return False
|
||||
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
assert_session_valid(request)
|
||||
except SessionInvalid:
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files import File
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy
|
||||
@@ -101,15 +100,8 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options')
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
answ = cp.answers.create(**answ_data, answer='')
|
||||
answ.file.save(an.name, an, save=False)
|
||||
answ.answer = 'file://' + answ.file.name
|
||||
answ.save()
|
||||
else:
|
||||
answ = cp.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
answ = cp.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
return cp
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from pytz import common_timezones
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Event, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
from pretix.base.settings import validate_event_settings
|
||||
from pretix.base.settings import DEFAULTS, validate_event_settings
|
||||
from pretix.base.signals import api_event_settings_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetaDataField(Field):
|
||||
|
||||
@@ -562,7 +558,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||
|
||||
|
||||
class EventSettingsSerializer(SettingsSerializer):
|
||||
class EventSettingsSerializer(serializers.Serializer):
|
||||
default_fields = [
|
||||
'imprint_url',
|
||||
'checkout_email_helptext',
|
||||
@@ -658,7 +654,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_additional_text',
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'invoice_logo_image',
|
||||
'cancel_allow_user',
|
||||
'cancel_allow_user_until',
|
||||
'cancel_allow_user_paid',
|
||||
@@ -668,7 +663,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||
'cancel_allow_user_paid_adjust_fees_step',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'change_allow_user_variation',
|
||||
@@ -680,21 +674,45 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'logo_image',
|
||||
'logo_image_large',
|
||||
'logo_show_title',
|
||||
'og_image',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
self.fields[fname] = f
|
||||
|
||||
for recv, resp in api_event_settings_fields.send(sender=self.event):
|
||||
for fname, field in resp.items():
|
||||
field.required = False
|
||||
self.fields[fname] = field
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
settings_dict = self.instance.freeze()
|
||||
@@ -702,14 +720,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
validate_event_settings(self.event, settings_dict)
|
||||
return data
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
nonce = get_random_string(length=8)
|
||||
fname = '%s/%s/%s.%s.%s' % (
|
||||
self.event.organizer.slug, self.event.slug, name.split('/')[-1], nonce, name.split('.')[-1]
|
||||
)
|
||||
# TODO: make sure pub is always correct
|
||||
return 'pub/' + fname
|
||||
|
||||
|
||||
class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
default_fields = [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
@@ -28,50 +27,3 @@ class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(representation_data)
|
||||
|
||||
|
||||
class UploadedFileField(serializers.Field):
|
||||
default_error_messages = {
|
||||
'required': 'No file was submitted.',
|
||||
'not_found': 'The submitted file ID was not found.',
|
||||
'invalid_type': 'The submitted file has a file type that is not allowed in this field.',
|
||||
'size': 'The submitted file is too large to be used in this field.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.allowed_types = kwargs.pop('allowed_types', None)
|
||||
self.max_size = kwargs.pop('max_size', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
from pretix.base.models import CachedFile
|
||||
|
||||
request = self.context.get('request', None)
|
||||
try:
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}',
|
||||
file__isnull=False,
|
||||
pk=data[len("file:"):],
|
||||
)
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
self.fail('not_found')
|
||||
except CachedFile.DoesNotExist:
|
||||
self.fail('not_found')
|
||||
|
||||
if self.allowed_types and cf.type not in self.allowed_types:
|
||||
self.fail('invalid_type')
|
||||
if self.max_size and cf.file.size > self.max_size:
|
||||
self.fail('size')
|
||||
|
||||
return cf.file
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
url = value.url
|
||||
except AttributeError:
|
||||
return None
|
||||
request = self.context['request']
|
||||
return request.build_absolute_uri(url)
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
@@ -114,9 +113,6 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=10 * 1024 * 1024)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -127,7 +123,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
|
||||
read_only_fields = ('has_variations',)
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -3,7 +3,6 @@ from collections import Counter, defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
import pycountry
|
||||
from django.core.files import File
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy
|
||||
@@ -18,9 +17,8 @@ from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer, Seat,
|
||||
SubEvent, TaxRule, Voucher,
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
|
||||
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
|
||||
@@ -96,9 +94,12 @@ class AnswerQuestionIdentifierField(serializers.Field):
|
||||
|
||||
class AnswerQuestionOptionsIdentifierField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
if isinstance(instance, WrappedModel) or instance.pk:
|
||||
return [o.identifier for o in instance.options.all()]
|
||||
return []
|
||||
return [o.identifier for o in instance.options.all()]
|
||||
|
||||
|
||||
class AnswerQuestionOptionsField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
return [o.pk for o in instance.options.all()]
|
||||
|
||||
|
||||
class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
@@ -111,91 +112,12 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
|
||||
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
|
||||
options = AnswerQuestionOptionsField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = QuestionAnswer
|
||||
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
|
||||
|
||||
def validate_question(self, q):
|
||||
if q.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified question does not belong to this event.'
|
||||
)
|
||||
return q
|
||||
|
||||
def _handle_file_upload(self, data):
|
||||
try:
|
||||
ao = self.context["request"].user or self.context["request"].auth
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(ao))}-{ao.pk}',
|
||||
file__isnull=False,
|
||||
pk=data['answer'][len("file:"):],
|
||||
)
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
except CachedFile.DoesNotExist:
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
|
||||
allowed_types = (
|
||||
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
|
||||
)
|
||||
if cf.type not in allowed_types:
|
||||
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
|
||||
if cf.file.size > 10 * 1024 * 1024:
|
||||
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
|
||||
|
||||
data['options'] = []
|
||||
data['answer'] = cf.file
|
||||
return data
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('question').type == Question.TYPE_FILE:
|
||||
return self._handle_file_upload(data)
|
||||
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
if not data.get('options'):
|
||||
raise ValidationError(
|
||||
'You need to specify options if the question is of a choice type.'
|
||||
)
|
||||
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
|
||||
raise ValidationError(
|
||||
'You can specify at most one option for this question.'
|
||||
)
|
||||
for o in data.get('options'):
|
||||
if o.question_id != data.get('question').pk:
|
||||
raise ValidationError(
|
||||
'The specified option does not belong to this question.'
|
||||
)
|
||||
|
||||
data['answer'] = ", ".join([str(o) for o in data.get('options')])
|
||||
|
||||
else:
|
||||
if data.get('options'):
|
||||
raise ValidationError(
|
||||
'You should not specify options if the question is not of a choice type.'
|
||||
)
|
||||
|
||||
if data.get('question').type == Question.TYPE_BOOLEAN:
|
||||
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
|
||||
data['answer'] = 'True'
|
||||
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
|
||||
data['answer'] = 'False'
|
||||
else:
|
||||
raise ValidationError(
|
||||
'Please specify "true" or "false" for boolean questions.'
|
||||
)
|
||||
elif data.get('question').type == Question.TYPE_NUMBER:
|
||||
serializers.DecimalField(
|
||||
max_digits=50,
|
||||
decimal_places=25
|
||||
).to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATE:
|
||||
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_TIME:
|
||||
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATETIME:
|
||||
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
|
||||
return data
|
||||
|
||||
|
||||
class CheckinSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
@@ -283,14 +205,13 @@ class PdfDataSerializer(serializers.Field):
|
||||
|
||||
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
checkins = CheckinSerializer(many=True, read_only=True)
|
||||
checkins = CheckinSerializer(many=True)
|
||||
answers = AnswerSerializer(many=True)
|
||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||
downloads = PositionDownloadsField(source='*')
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*', read_only=True)
|
||||
pdf_data = PdfDataSerializer(source='*')
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
@@ -298,99 +219,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
||||
'seat', 'canceled'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||
self.fields.pop('pdf_data')
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('attendee_name') and data.get('attendee_name_parts'):
|
||||
raise ValidationError(
|
||||
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
|
||||
)
|
||||
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
|
||||
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
|
||||
|
||||
if data.get('country'):
|
||||
if not pycountry.countries.get(alpha_2=data.get('country').code):
|
||||
raise ValidationError(
|
||||
{'country': ['Invalid country code.']}
|
||||
)
|
||||
|
||||
if data.get('state'):
|
||||
cc = str(data.get('country') or self.instance.country or '')
|
||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(
|
||||
{'state': ['States are not supported in country "{}".'.format(cc)]}
|
||||
)
|
||||
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
|
||||
raise ValidationError(
|
||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||
)
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = [
|
||||
'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
|
||||
'state', 'attendee_email',
|
||||
]
|
||||
answers_data = validated_data.pop('answers', None)
|
||||
|
||||
name = validated_data.pop('attendee_name', '')
|
||||
if name and not validated_data.get('attendee_name_parts'):
|
||||
validated_data['attendee_name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
if attr in update_fields:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save(update_fields=update_fields)
|
||||
|
||||
if answers_data is not None:
|
||||
qs_seen = set()
|
||||
answercache = {
|
||||
a.question_id: a for a in instance.answers.all()
|
||||
}
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
if answ_data['question'].pk in qs_seen:
|
||||
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
|
||||
if answ_data['question'].pk in answercache:
|
||||
a = answercache[answ_data['question'].pk]
|
||||
if isinstance(answ_data['answer'], File):
|
||||
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
else:
|
||||
for attr, value in answ_data.items():
|
||||
setattr(a, attr, value)
|
||||
a.save()
|
||||
else:
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
a = instance.answers.create(**answ_data, answer='')
|
||||
a.file.save(an.name, an, save=False)
|
||||
a.answer = 'file://' + a.file.name
|
||||
a.save()
|
||||
else:
|
||||
a = instance.answers.create(**answ_data)
|
||||
a.options.set(options)
|
||||
qs_seen.add(a.question_id)
|
||||
for qid, a in answercache.items():
|
||||
if qid not in qs_seen:
|
||||
a.delete()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class RequireAttentionField(serializers.Field):
|
||||
def to_representation(self, instance: OrderPosition):
|
||||
@@ -502,7 +336,7 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderRefund
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'comment', 'provider')
|
||||
fields = ('local_id', 'state', 'source', 'amount', 'payment', 'created', 'execution_date', 'provider')
|
||||
|
||||
|
||||
class OrderURLField(serializers.URLField):
|
||||
@@ -591,17 +425,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
return instance
|
||||
|
||||
|
||||
class AnswerQuestionOptionsField(serializers.Field):
|
||||
def to_representation(self, instance: QuestionAnswer):
|
||||
return [o.pk for o in instance.options.all()]
|
||||
|
||||
|
||||
class SimulatedAnswerSerializer(AnswerSerializer):
|
||||
options = AnswerQuestionOptionsField(read_only=True, source='*')
|
||||
|
||||
|
||||
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
|
||||
answers = SimulatedAnswerSerializer(many=True)
|
||||
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
|
||||
|
||||
|
||||
@@ -628,8 +452,62 @@ class PriceCalcSerializer(serializers.Serializer):
|
||||
del self.fields['subevent']
|
||||
|
||||
|
||||
class AnswerCreateSerializer(AnswerSerializer):
|
||||
pass
|
||||
class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = QuestionAnswer
|
||||
fields = ('question', 'answer', 'options')
|
||||
|
||||
def validate_question(self, q):
|
||||
if q.event != self.context['event']:
|
||||
raise ValidationError(
|
||||
'The specified question does not belong to this event.'
|
||||
)
|
||||
return q
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('question').type == Question.TYPE_FILE:
|
||||
raise ValidationError(
|
||||
'File uploads are currently not supported via the API.'
|
||||
)
|
||||
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
if not data.get('options'):
|
||||
raise ValidationError(
|
||||
'You need to specify options if the question is of a choice type.'
|
||||
)
|
||||
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
|
||||
raise ValidationError(
|
||||
'You can specify at most one option for this question.'
|
||||
)
|
||||
data['answer'] = ", ".join([str(o) for o in data.get('options')])
|
||||
|
||||
else:
|
||||
if data.get('options'):
|
||||
raise ValidationError(
|
||||
'You should not specify options if the question is not of a choice type.'
|
||||
)
|
||||
|
||||
if data.get('question').type == Question.TYPE_BOOLEAN:
|
||||
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
|
||||
data['answer'] = 'True'
|
||||
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
|
||||
data['answer'] = 'False'
|
||||
else:
|
||||
raise ValidationError(
|
||||
'Please specify "true" or "false" for boolean questions.'
|
||||
)
|
||||
elif data.get('question').type == Question.TYPE_NUMBER:
|
||||
serializers.DecimalField(
|
||||
max_digits=50,
|
||||
decimal_places=25
|
||||
).to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATE:
|
||||
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_TIME:
|
||||
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
|
||||
elif data.get('question').type == Question.TYPE_DATETIME:
|
||||
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
|
||||
return data
|
||||
|
||||
|
||||
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
|
||||
@@ -1166,16 +1044,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
pos.save()
|
||||
for answ_data in answers_data:
|
||||
options = answ_data.pop('options', [])
|
||||
|
||||
if isinstance(answ_data['answer'], File):
|
||||
an = answ_data.pop('answer')
|
||||
answ = pos.answers.create(**answ_data, answer='')
|
||||
answ.file.save(an.name, an, save=False)
|
||||
answ.answer = 'file://' + answ.file.name
|
||||
answ.save()
|
||||
else:
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
pos_map[pos.positionid] = pos
|
||||
|
||||
if not simulate:
|
||||
@@ -1324,7 +1194,7 @@ class OrderRefundCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = OrderRefund
|
||||
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info', 'comment')
|
||||
fields = ('state', 'source', 'amount', 'payment', 'execution_date', 'provider', 'info')
|
||||
|
||||
def create(self, validated_data):
|
||||
pid = validated_data.pop('payment', None)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
@@ -18,11 +16,9 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.settings import validate_organizer_settings
|
||||
from pretix.base.settings import DEFAULTS, validate_organizer_settings
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
@@ -211,7 +207,7 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
class OrganizerSettingsSerializer(serializers.Serializer):
|
||||
default_fields = [
|
||||
'organizer_info_text',
|
||||
'event_list_type',
|
||||
@@ -229,13 +225,40 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
'theme_color_danger',
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'organizer_logo_image'
|
||||
'primary_font'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
self.fields[fname] = f
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -243,11 +266,3 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
settings_dict.update(data)
|
||||
validate_organizer_settings(self.organizer, settings_dict)
|
||||
return data
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
nonce = get_random_string(length=8)
|
||||
fname = '%s/%s.%s.%s' % (
|
||||
self.organizer.slug, name.split('/')[-1], nonce, name.split('.')[-1]
|
||||
)
|
||||
# TODO: make sure pub is always correct
|
||||
return 'pub/' + fname
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import logging
|
||||
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models.fields.files import FieldFile
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.base.settings import DEFAULTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsSerializer(serializers.Serializer):
|
||||
default_fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
f.parent = self
|
||||
self.fields[fname] = f
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if isinstance(value, FieldFile):
|
||||
# Delete old file
|
||||
fname = instance.get(attr, as_type=File)
|
||||
if fname:
|
||||
try:
|
||||
default_storage.delete(fname.name)
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
|
||||
# Create new file
|
||||
newname = default_storage.save(self.get_new_filename(value.name), value)
|
||||
instance.set(attr, File(file=value, name=newname))
|
||||
self.changed_data.append(attr)
|
||||
elif isinstance(self.fields[attr], UploadedFileField):
|
||||
if value is None:
|
||||
fname = instance.get(attr, as_type=File)
|
||||
if fname:
|
||||
try:
|
||||
default_storage.delete(fname.name)
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
instance.delete(attr)
|
||||
else:
|
||||
# file is unchanged
|
||||
continue
|
||||
elif value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
raise NotImplementedError()
|
||||
@@ -7,8 +7,8 @@ from rest_framework import routers
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, upload,
|
||||
user, version, voucher, waitinglist, webhooks,
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, user,
|
||||
version, voucher, waitinglist, webhooks,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -95,7 +95,6 @@ urlpatterns = [
|
||||
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
url(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||
url(r"^upload$", upload.UploadView.as_view(), name="upload"),
|
||||
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||
url(r"^version$", version.VersionView.as_view(), name="version"),
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@ from pretix.api.views import RichOrderingFilter
|
||||
from pretix.api.views.order import OrderPositionFilter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, CheckinList, Event, Order, OrderPosition, Question,
|
||||
Checkin, CheckinList, Event, Order, OrderPosition,
|
||||
)
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, perform_checkin,
|
||||
@@ -302,10 +302,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
for q in op.item.questions.filter(ask_during_checkin=True):
|
||||
if str(q.pk) in aws:
|
||||
try:
|
||||
if q.type == Question.TYPE_FILE:
|
||||
given_answers[q] = self._handle_file_upload(aws[str(q.pk)])
|
||||
else:
|
||||
given_answers[q] = q.clean_answer(aws[str(q.pk)])
|
||||
given_answers[q] = q.clean_answer(aws[str(q.pk)])
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
@@ -355,25 +352,3 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=201)
|
||||
|
||||
def _handle_file_upload(self, data):
|
||||
try:
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}',
|
||||
file__isnull=False,
|
||||
pk=data[len("file:"):],
|
||||
)
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
except CachedFile.DoesNotExist:
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
|
||||
allowed_types = (
|
||||
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
|
||||
)
|
||||
if cf.type not in allowed_types:
|
||||
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
|
||||
if cf.file.size > 10 * 1024 * 1024:
|
||||
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
|
||||
|
||||
return cf.file
|
||||
|
||||
@@ -365,13 +365,9 @@ class EventSettingsView(views.APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if isinstance(request.auth, Device):
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
elif 'can_change_event_settings' in request.eventpermset:
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
if 'explain' in request.GET:
|
||||
@@ -386,7 +382,7 @@ class EventSettingsView(views.APIView):
|
||||
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
||||
event=request.event, context={'request': request})
|
||||
event=request.event)
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
s.save()
|
||||
@@ -396,9 +392,6 @@ class EventSettingsView(views.APIView):
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_css.apply_async(args=(request.event.pk,))
|
||||
s = EventSettingsSerializer(
|
||||
instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
regenerate_css.apply_async(args=(request.organizer.pk,))
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
return Response(s.data)
|
||||
|
||||
@@ -763,7 +763,7 @@ with scopes_disabled():
|
||||
}
|
||||
|
||||
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPositionSerializer
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
@@ -783,11 +783,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
},
|
||||
}
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
qs = OrderPosition.all
|
||||
@@ -956,44 +951,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, vi
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.get('partial', False)
|
||||
if not partial:
|
||||
return Response(
|
||||
{"detail": "Method \"PUT\" not allowed."},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
with transaction.atomic():
|
||||
old_data = self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data
|
||||
serializer.save()
|
||||
new_data = serializer.data
|
||||
|
||||
if old_data != new_data:
|
||||
log_data = self.request.data
|
||||
if 'answers' in log_data:
|
||||
for a in new_data['answers']:
|
||||
log_data[f'question_{a["question"]}'] = a["answer"]
|
||||
log_data.pop('answers', None)
|
||||
serializer.instance.order.log_action(
|
||||
'pretix.event.order.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'data': [
|
||||
dict(
|
||||
position=serializer.instance.pk,
|
||||
**log_data
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
|
||||
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
|
||||
|
||||
|
||||
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPaymentSerializer
|
||||
|
||||
@@ -425,9 +425,7 @@ class OrganizerSettingsView(views.APIView):
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
|
||||
if 'explain' in request.GET:
|
||||
return Response({
|
||||
fname: {
|
||||
@@ -441,9 +439,7 @@ class OrganizerSettingsView(views.APIView):
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = OrganizerSettingsSerializer(
|
||||
instance=request.organizer.settings, data=request.data, partial=True,
|
||||
organizer=request.organizer, context={
|
||||
'request': request
|
||||
}
|
||||
organizer=request.organizer
|
||||
)
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
@@ -455,7 +451,5 @@ class OrganizerSettingsView(views.APIView):
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
|
||||
return Response(s.data)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from django.utils.timezone import now
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
|
||||
from pretix.api.auth.token import TeamTokenAuthentication
|
||||
from pretix.base.models import CachedFile
|
||||
|
||||
ALLOWED_TYPES = {
|
||||
'image/gif': {'.gif'},
|
||||
'image/jpeg': {'.jpg', '.jpeg'},
|
||||
'image/png': {'.png'},
|
||||
'application/pdf': {'.pdf'},
|
||||
}
|
||||
|
||||
|
||||
class UploadView(APIView):
|
||||
authentication_classes = (
|
||||
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
|
||||
)
|
||||
parser_classes = [FileUploadParser]
|
||||
permission_classes = [AnyAuthenticatedClientPermission]
|
||||
|
||||
def post(self, request):
|
||||
if 'file' not in request.data:
|
||||
raise ValidationError('No file has been submitted.')
|
||||
file_obj = request.data['file']
|
||||
content_type = file_obj.content_type.split(";")[0] # ignore e.g. "; charset=…"
|
||||
if content_type not in ALLOWED_TYPES:
|
||||
raise ValidationError('Content type "{type}" is not allowed'.format(type=content_type))
|
||||
if not any(file_obj.name.endswith(ext) for ext in ALLOWED_TYPES[content_type]):
|
||||
raise ValidationError('File name "{name}" has an invalid extension for type "{type}"'.format(
|
||||
name=file_obj.name,
|
||||
type=content_type
|
||||
))
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
date=now(),
|
||||
web_download=False,
|
||||
filename=file_obj.name,
|
||||
type=content_type,
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}'
|
||||
)
|
||||
cf.file.save(file_obj.name, file_obj)
|
||||
cf.save()
|
||||
return Response({
|
||||
'id': f'file:{cf.pk}'
|
||||
}, status=201)
|
||||
@@ -6,7 +6,6 @@ from rest_framework.views import APIView
|
||||
|
||||
from pretix import __version__
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
|
||||
from pretix.api.auth.token import TeamTokenAuthentication
|
||||
|
||||
|
||||
@@ -49,7 +48,6 @@ class VersionView(APIView):
|
||||
authentication_classes = (
|
||||
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
|
||||
)
|
||||
permission_classes = [AnyAuthenticatedClientPermission]
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
|
||||
@@ -427,30 +427,28 @@ def base_placeholders(sender, **kwargs):
|
||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
order.full_code,
|
||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_hash(),
|
||||
'secret': order.secret
|
||||
}),
|
||||
)
|
||||
for order in orders
|
||||
), lambda event: '\n' + '\n\n'.join(
|
||||
'* {} - {}'.format(
|
||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||
build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'order': order['code'],
|
||||
'secret': order['secret'],
|
||||
'hash': order['hash'],
|
||||
'secret': order['secret']
|
||||
}),
|
||||
)
|
||||
for order in [
|
||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
|
||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy'},
|
||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd'},
|
||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd'}
|
||||
]
|
||||
),
|
||||
),
|
||||
|
||||
@@ -59,12 +59,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
initial=False,
|
||||
required=False
|
||||
)),
|
||||
('group_multiple_choice',
|
||||
forms.BooleanField(
|
||||
label=_('Show multiple choice answers grouped in one column'),
|
||||
initial=False,
|
||||
required=False
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -455,14 +449,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for q in questions:
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
options[q.pk] = []
|
||||
if form_data['group_multiple_choice']:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
headers.append(str(q.question))
|
||||
else:
|
||||
for o in q.options.all():
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
for o in q.options.all():
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
else:
|
||||
headers.append(str(q.question))
|
||||
headers += [
|
||||
@@ -562,11 +551,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
acache[a.question_id] = str(a)
|
||||
for q in questions:
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
if form_data['group_multiple_choice']:
|
||||
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
|
||||
else:
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
else:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
|
||||
@@ -652,7 +638,7 @@ class PaymentListExporter(ListExporter):
|
||||
|
||||
headers = [
|
||||
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
|
||||
_('Status code'), _('Amount'), _('Payment method'), _('Comment')
|
||||
_('Status code'), _('Amount'), _('Payment method')
|
||||
]
|
||||
yield headers
|
||||
|
||||
@@ -674,8 +660,7 @@ class PaymentListExporter(ListExporter):
|
||||
obj.get_state_display(),
|
||||
obj.state,
|
||||
obj.amount * (-1 if isinstance(obj, OrderRefund) else 1),
|
||||
provider_names.get(obj.provider, obj.provider),
|
||||
obj.comment if isinstance(obj, OrderRefund) else "",
|
||||
provider_names.get(obj.provider, obj.provider)
|
||||
]
|
||||
yield row
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ import pycountry
|
||||
import pytz
|
||||
import vat_moss.errors
|
||||
import vat_moss.id
|
||||
from babel import Locale
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Select
|
||||
from django.utils import translation
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -26,7 +24,9 @@ from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from phonenumber_field.widgets import (
|
||||
PhoneNumberPrefixWidget, PhonePrefixSelect,
|
||||
)
|
||||
from phonenumbers import NumberParseException, national_significant_number
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
|
||||
@@ -205,39 +205,10 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
return value
|
||||
|
||||
|
||||
class WrappedPhonePrefixSelect(Select):
|
||||
initial = None
|
||||
|
||||
def __init__(self, initial=None):
|
||||
choices = [("", "---------")]
|
||||
language = get_babel_locale() # changed from default implementation that used the django locale
|
||||
locale = Locale(translation.to_locale(language))
|
||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
prefix = "+%d" % prefix
|
||||
if initial and initial in values:
|
||||
self.initial = prefix
|
||||
for country_code in values:
|
||||
country_name = locale.territories.get(country_code)
|
||||
if country_name:
|
||||
choices.append((prefix, "{} {}".format(country_name, prefix)))
|
||||
super().__init__(choices=sorted(choices, key=lambda item: item[1]))
|
||||
|
||||
def render(self, name, value, *args, **kwargs):
|
||||
return super().render(name, value or self.initial, *args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
if value and self.choices[1][0] != value:
|
||||
matching_choices = len([1 for p, c in self.choices if p == value])
|
||||
if matching_choices > 1:
|
||||
# Some countries share a phone prefix, for example +1 is used all over the Americas.
|
||||
# This causes a UX problem: If the default value or the existing data is +12125552368,
|
||||
# the widget will just show the first <option> entry with value="+1" as selected,
|
||||
# which alphabetically is America Samoa, although most numbers statistically are from
|
||||
# the US. As a workaround, we detect this case and add an aditional choice value with
|
||||
# just <option value="+1">+1</option> without an explicit country.
|
||||
self.choices.insert(1, (value, value))
|
||||
context = super().get_context(name, value, attrs)
|
||||
return context
|
||||
class WrappedPhonePrefixSelect(PhonePrefixSelect):
|
||||
def __init__(self, *args, **kwargs):
|
||||
with language(get_babel_locale()):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
@@ -690,9 +661,8 @@ class BaseQuestionsForm(forms.Form):
|
||||
if not self.all_optional:
|
||||
for q in question_cache.values():
|
||||
answer = d.get('question_%d' % q.pk)
|
||||
field = self['question_%d' % q.pk]
|
||||
if question_is_required(q) and not answer and answer != 0 and not field.errors:
|
||||
raise ValidationError({'question_%d' % q.pk: [_('This field is required.')]})
|
||||
if question_is_required(q) and not answer and answer != 0:
|
||||
raise ValidationError({'question_%d' % q.pk: [_('This field is required')]})
|
||||
|
||||
return d
|
||||
|
||||
|
||||
@@ -18,13 +18,10 @@ class DatePickerWidget(forms.DateInput):
|
||||
date_attrs['class'] += ' datepickerfield'
|
||||
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
|
||||
|
||||
def placeholder():
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
return now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
|
||||
date_attrs['placeholder'] = lazy(placeholder, str)
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
date_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
|
||||
forms.DateInput.__init__(self, date_attrs, date_format)
|
||||
|
||||
@@ -39,13 +36,10 @@ class TimePickerWidget(forms.TimeInput):
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
|
||||
|
||||
def placeholder():
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
return now().replace(
|
||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
time_attrs['placeholder'] = lazy(placeholder, str)
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
time_attrs['placeholder'] = now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
forms.TimeInput.__init__(self, time_attrs, time_format)
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from django.apps import apps
|
||||
@@ -8,7 +6,6 @@ from django.conf import settings
|
||||
from django.db import connection
|
||||
|
||||
from pretix.base.models import Event, Invoice, Order, OrderPosition, Organizer
|
||||
from pretix.celery_app import app
|
||||
|
||||
if settings.HAS_REDIS:
|
||||
import django_redis
|
||||
@@ -251,19 +248,6 @@ def metric_values():
|
||||
else:
|
||||
metrics['pretix_model_instances']['{model="%s"}' % m._meta] = estimate_count_fast(m)
|
||||
|
||||
if settings.HAS_CELERY:
|
||||
client = app.broker_connection().channel().client
|
||||
for q in settings.CELERY_TASK_QUEUES:
|
||||
llen = client.llen(q.name)
|
||||
lfirst = client.lindex(q.name, -1)
|
||||
metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = llen
|
||||
if lfirst:
|
||||
ldata = json.loads(lfirst)
|
||||
dt = time.time() - ldata.get('created', 0)
|
||||
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = dt
|
||||
else:
|
||||
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = 0
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.0.11 on 2021-01-15 09:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0174_merge_20201222_1031'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='orderrefund',
|
||||
name='comment',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -10,9 +10,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import (
|
||||
MaxValueValidator, MinLengthValidator, MinValueValidator, RegexValidator,
|
||||
)
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
|
||||
from django.template.defaultfilters import date as _date
|
||||
@@ -355,11 +353,8 @@ class Event(EventMixin, LoggedModel):
|
||||
"remembered, but you can also choose to use a random value. "
|
||||
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
||||
validators=[
|
||||
MinLengthValidator(
|
||||
limit_value=2,
|
||||
),
|
||||
RegexValidator(
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$",
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*$",
|
||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||
),
|
||||
EventSlugBanlistValidator()
|
||||
@@ -398,18 +393,10 @@ class Event(EventMixin, LoggedModel):
|
||||
geo_lat = models.FloatField(
|
||||
verbose_name=_("Latitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-90),
|
||||
MaxValueValidator(90),
|
||||
]
|
||||
)
|
||||
geo_lon = models.FloatField(
|
||||
verbose_name=_("Longitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-180),
|
||||
MaxValueValidator(180),
|
||||
]
|
||||
)
|
||||
plugins = models.TextField(
|
||||
null=False, blank=True,
|
||||
@@ -1134,18 +1121,10 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
geo_lat = models.FloatField(
|
||||
verbose_name=_("Latitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-90),
|
||||
MaxValueValidator(90),
|
||||
]
|
||||
)
|
||||
geo_lon = models.FloatField(
|
||||
verbose_name=_("Longitude"),
|
||||
null=True, blank=True,
|
||||
validators=[
|
||||
MinValueValidator(-180),
|
||||
MaxValueValidator(180),
|
||||
]
|
||||
null=True, blank=True
|
||||
)
|
||||
frontpage_text = I18nTextField(
|
||||
null=True, blank=True,
|
||||
|
||||
@@ -1026,7 +1026,7 @@ class Question(LoggedModel):
|
||||
(TYPE_PHONENUMBER, _("Phone number")),
|
||||
)
|
||||
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
|
||||
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_PHONENUMBER]
|
||||
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE, TYPE_PHONENUMBER]
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -1069,7 +1069,6 @@ class Question(LoggedModel):
|
||||
)
|
||||
ask_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
hidden = models.BooleanField(
|
||||
|
||||
@@ -615,26 +615,21 @@ class Order(LockModel, LoggedModel):
|
||||
return proposals
|
||||
|
||||
@staticmethod
|
||||
def normalize_code(code, is_fallback=False):
|
||||
d = {
|
||||
def normalize_code(code):
|
||||
tr = str.maketrans({
|
||||
'2': 'Z',
|
||||
'4': 'A',
|
||||
'5': 'S',
|
||||
'6': 'G',
|
||||
}
|
||||
if is_fallback:
|
||||
d['8'] = 'B'
|
||||
# 8 has been removed from the character set only in 2021, which means there are a lot of order codes
|
||||
# with an 8 in it around. We only want to replace this when this is used in a fallback.
|
||||
tr = str.maketrans(d)
|
||||
})
|
||||
return code.upper().translate(tr)
|
||||
|
||||
def assign_code(self):
|
||||
# This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
|
||||
# and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
|
||||
# handwriting (2/Z, 4/A, 5/S, 6/G, 8/B). This allows for better detection e.g. in incoming wire transfers that
|
||||
# handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
|
||||
# might include OCR'd handwritten text
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ379')
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
iteration = 0
|
||||
length = settings.ENTROPY['order_code']
|
||||
while True:
|
||||
@@ -1224,9 +1219,6 @@ class AbstractPosition(models.Model):
|
||||
else self.variation.quotas.filter(subevent=self.subevent))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
if 'attendee_name_parts' in update_fields:
|
||||
update_fields.append('attendee_name_cached')
|
||||
self.attendee_name_cached = self.attendee_name
|
||||
if self.attendee_name_parts is None:
|
||||
self.attendee_name_parts = {}
|
||||
@@ -1716,11 +1708,6 @@ class OrderRefund(models.Model):
|
||||
max_length=255,
|
||||
verbose_name=_("Payment provider")
|
||||
)
|
||||
comment = models.TextField(
|
||||
verbose_name=_("Refund reason"),
|
||||
help_text=_('May be shown to the end user or used e.g. as part of a payment reference.'),
|
||||
null=True, blank=True
|
||||
)
|
||||
info = models.TextField(
|
||||
verbose_name=_("Payment information"),
|
||||
null=True, blank=True
|
||||
@@ -1759,7 +1746,7 @@ class OrderRefund(models.Model):
|
||||
Marks the refund as complete. This does not modify the state of the order.
|
||||
|
||||
:param user: The user who performed the change
|
||||
:param auth: The API auth token that performed the change
|
||||
:param user: The API auth token that performed the change
|
||||
"""
|
||||
self.state = self.REFUND_STATE_DONE
|
||||
self.execution_date = self.execution_date or now()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import string
|
||||
from datetime import date, datetime, time
|
||||
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -38,11 +38,8 @@ class Organizer(LoggedModel):
|
||||
"Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used "
|
||||
"once. This is being used in URLs to refer to your organizer accounts and your events."),
|
||||
validators=[
|
||||
MinLengthValidator(
|
||||
limit_value=2,
|
||||
),
|
||||
RegexValidator(
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$",
|
||||
regex="^[a-zA-Z0-9][a-zA-Z0-9.-]+$",
|
||||
message=_("The slug may only contain letters, numbers, dots and dashes.")
|
||||
),
|
||||
OrganizerSlugBanlistValidator()
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytz
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
from django.forms import Form
|
||||
@@ -706,24 +706,12 @@ class BasePaymentProvider:
|
||||
It should return HTML code containing information regarding the current payment
|
||||
status and, if applicable, next steps.
|
||||
|
||||
The default implementation returns an empty string.
|
||||
The default implementation returns the verbose name of the payment provider.
|
||||
|
||||
:param order: The order object
|
||||
"""
|
||||
return ''
|
||||
|
||||
def payment_control_render_short(self, payment: OrderPayment) -> str:
|
||||
"""
|
||||
Will be called if the *event administrator* performs an action on the payment. Should
|
||||
return a very short version of the payment method. Usually, this should return e.g.
|
||||
a transaction ID or account identifier, but no information on status, dates, etc.
|
||||
|
||||
The default implementation falls back to payment_presa_elrender.
|
||||
|
||||
:param order: The order object
|
||||
"""
|
||||
return self.payment_presale_render(payment)
|
||||
|
||||
def refund_control_render(self, request: HttpRequest, refund: OrderRefund) -> str:
|
||||
"""
|
||||
Will be called if the *event administrator* views the details of a refund.
|
||||
@@ -737,19 +725,6 @@ class BasePaymentProvider:
|
||||
"""
|
||||
return ''
|
||||
|
||||
def payment_presale_render(self, payment: OrderPayment) -> str:
|
||||
"""
|
||||
Will be called if the *ticket customer* views the details of a payment. This is
|
||||
currently used e.g. when the customer requests a refund to show which payment
|
||||
method is used for the refund. This should only include very basic information
|
||||
about the payment, such as "VISA card ****9999", and never raw payment information.
|
||||
|
||||
The default implementation returns the public name of the payment provider.
|
||||
|
||||
:param order: The order object
|
||||
"""
|
||||
return self.public_name
|
||||
|
||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||
"""
|
||||
Will be called to check if the provider supports automatic refunding for this
|
||||
@@ -785,32 +760,6 @@ class BasePaymentProvider:
|
||||
"""
|
||||
raise PaymentException(_('Automatic refunds are not supported by this payment provider.'))
|
||||
|
||||
def new_refund_control_form_render(self, request: HttpRequest, order: Order) -> str:
|
||||
"""
|
||||
Render a form that will be shown to backend users when trying to create a new refund.
|
||||
|
||||
Usually, refunds are created from an existing payment object, e.g. if there is a credit card
|
||||
payment and the credit card provider returns ``True`` from ``payment_refund_supported``, the system
|
||||
will automatically create an ``OrderRefund`` and call ``execute_refund`` on that payment. This method
|
||||
can and should not be used in that situation! Instead, by implementing this method you can add a refund
|
||||
flow for this payment provider that starts without an existing payment. For example, even though an order
|
||||
was paid by credit card, it could easily be refunded by SEPA bank transfer. In that case, the SEPA bank
|
||||
transfer provider would implement this method and return a form that asks for the IBAN.
|
||||
|
||||
This method should return HTML or ``None``. All form fields should have a globally unique name.
|
||||
"""
|
||||
return
|
||||
|
||||
def new_refund_control_form_process(self, request: HttpRequest, amount: Decimal, order: Order) -> OrderRefund:
|
||||
"""
|
||||
Process a backend user's request to initiate a new refund with an amount of ``amount`` for ``order``.
|
||||
|
||||
This method should parse the input provided to the form created and either raise ``ValidationError``
|
||||
or return an ``OrderRefund`` object in ``created`` state that has not yet been saved to the database.
|
||||
The system will then call ``execute_refund`` on that object.
|
||||
"""
|
||||
raise ValidationError('Not implemented')
|
||||
|
||||
def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
|
||||
"""
|
||||
When personal data is removed from an event, this method is called to scrub payment-related data
|
||||
@@ -950,7 +899,7 @@ class ManualPayment(BasePaymentProvider):
|
||||
|
||||
@property
|
||||
def public_name(self):
|
||||
return str(self.settings.get('public_name', as_type=LazyI18nString) or _('Manual payment'))
|
||||
return str(self.settings.get('public_name', as_type=LazyI18nString))
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
|
||||
@@ -3,7 +3,6 @@ from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery
|
||||
from django.utils.translation import gettext
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -196,8 +195,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
if auto_refund:
|
||||
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
|
||||
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
|
||||
comment=gettext('Event canceled'))
|
||||
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions)
|
||||
finally:
|
||||
if send:
|
||||
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
|
||||
@@ -254,8 +252,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
|
||||
if auto_refund:
|
||||
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True,
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
|
||||
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
|
||||
comment=gettext('Event canceled'))
|
||||
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions)
|
||||
|
||||
if send:
|
||||
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import dateutil
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models.functions import TruncDate
|
||||
from django.dispatch import receiver
|
||||
@@ -24,7 +23,7 @@ def get_logic_environment(ev):
|
||||
elif t == 'date_from':
|
||||
return ev.date_from
|
||||
elif t == 'date_to':
|
||||
return ev.date_to or ev.date_from
|
||||
return ev.date_to
|
||||
elif t == 'date_admission':
|
||||
return ev.date_admission or ev.date_from
|
||||
|
||||
@@ -126,14 +125,6 @@ def _save_answers(op, answers, given_answers):
|
||||
else:
|
||||
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
|
||||
qa.options.add(*a)
|
||||
elif isinstance(a, File):
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
else:
|
||||
qa = op.answers.create(question=q, answer=str(a))
|
||||
qa.file.save(a.name, a, save=False)
|
||||
qa.answer = 'file://' + qa.file.name
|
||||
qa.save()
|
||||
else:
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
|
||||
@@ -2034,7 +2034,7 @@ _unset = object()
|
||||
|
||||
|
||||
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
|
||||
refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None, comment=None):
|
||||
refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None):
|
||||
notify_admin = False
|
||||
error = False
|
||||
if isinstance(order, int):
|
||||
@@ -2059,7 +2059,6 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
||||
order=order,
|
||||
payment=None,
|
||||
source=source,
|
||||
comment=comment,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
execution_date=now(),
|
||||
amount=can_auto_refund_sum,
|
||||
@@ -2097,7 +2096,6 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
||||
source=source,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=value,
|
||||
comment=comment,
|
||||
provider=p.provider
|
||||
)
|
||||
order.log_action('pretix.event.order.refund.created', {
|
||||
@@ -2127,7 +2125,6 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
||||
with transaction.atomic():
|
||||
r = order.refunds.create(
|
||||
source=source,
|
||||
comment=comment,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=refund_amount - can_auto_refund_sum,
|
||||
provider='manual'
|
||||
@@ -2152,14 +2149,13 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
@scopes_disabled()
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None):
|
||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False):
|
||||
try:
|
||||
try:
|
||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||
cancellation_fee)
|
||||
if try_auto_refund:
|
||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
|
||||
comment=comment)
|
||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard)
|
||||
return ret
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
|
||||
@@ -36,7 +36,7 @@ def validate_plan_change(event, subevent, plan):
|
||||
'already sold.'), leftovers[0])
|
||||
|
||||
|
||||
def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
|
||||
def generate_seats(event, subevent, plan, mapping):
|
||||
current_seats = {}
|
||||
for s in event.seats.select_related('product').annotate(
|
||||
has_op=Count('orderposition'), has_v=Count('vouchers')
|
||||
@@ -68,10 +68,7 @@ def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
|
||||
update(seat, 'seat_label', ss.seat_label),
|
||||
update(seat, 'x', ss.x),
|
||||
update(seat, 'y', ss.y),
|
||||
] + (
|
||||
[update(seat, 'blocked', ss.guid in blocked_guids)]
|
||||
if blocked_guids else []
|
||||
))
|
||||
])
|
||||
if updated:
|
||||
seat.save()
|
||||
else:
|
||||
@@ -87,7 +84,6 @@ def generate_seats(event, subevent, plan, mapping, blocked_guids=None):
|
||||
seat_label=ss.seat_label,
|
||||
x=ss.x,
|
||||
y=ss.y,
|
||||
blocked=bool(blocked_guids and ss.guid in blocked_guids),
|
||||
product=p,
|
||||
))
|
||||
|
||||
|
||||
@@ -75,19 +75,16 @@ def shred(event: Event, fileid: str, confirm_code: str) -> None:
|
||||
indexdata = json.loads(zipfile.read('index.json').decode())
|
||||
if indexdata['organizer'] != event.organizer.slug or indexdata['event'] != event.slug:
|
||||
raise ShredError(_("This file is from a different event."))
|
||||
shredders = []
|
||||
if indexdata['confirm_code'] != confirm_code:
|
||||
raise ShredError(_("The confirm code you entered was incorrect."))
|
||||
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):
|
||||
raise ShredError(_("Something happened in your event after the export, please try again."))
|
||||
|
||||
for s in indexdata['shredders']:
|
||||
shredder = known_shredders.get(s)
|
||||
if not shredder:
|
||||
continue
|
||||
shredders.append(shredder)
|
||||
if any(shredder.require_download_confirmation for shredder in shredders):
|
||||
if indexdata['confirm_code'] != confirm_code:
|
||||
raise ShredError(_("The confirm code you entered was incorrect."))
|
||||
if event.logentry_set.filter(datetime__gte=parse(indexdata['time'])):
|
||||
raise ShredError(_("Something happened in your event after the export, please try again."))
|
||||
|
||||
for shredder in shredders:
|
||||
shredder.shred_data()
|
||||
|
||||
cf.file.delete(save=False)
|
||||
|
||||
@@ -45,8 +45,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
continue
|
||||
if wle.subevent and not wle.subevent.presale_is_running:
|
||||
continue
|
||||
if not wle.item.is_available():
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
if not wle.item.active or (wle.variation and not wle.variation.active):
|
||||
continue
|
||||
|
||||
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
||||
|
||||
@@ -21,9 +21,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.fields import (
|
||||
ListMultipleChoiceField, UploadedFileField,
|
||||
)
|
||||
from pretix.api.serializers.fields import ListMultipleChoiceField
|
||||
from pretix.api.serializers.i18n import I18nField
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.reldate import (
|
||||
@@ -31,7 +29,7 @@ from pretix.base.reldate import (
|
||||
SerializerRelativeDateField, SerializerRelativeDateTimeField,
|
||||
)
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
|
||||
FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
|
||||
)
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
|
||||
@@ -1225,21 +1223,6 @@ DEFAULTS = {
|
||||
"e.g. to explain choosing a lower refund will help your organization.")
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_adjust_fees_step': {
|
||||
'default': None,
|
||||
'type': Decimal,
|
||||
'form_class': forms.DecimalField,
|
||||
'serializer_class': serializers.DecimalField,
|
||||
'serializer_kwargs': dict(
|
||||
max_digits=10, decimal_places=2
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
max_digits=10, decimal_places=2,
|
||||
label=_("Step size for reduction amount"),
|
||||
help_text=_('By default, customers can choose an arbitrary amount for you to keep. If you set this to e.g. '
|
||||
'10, they will only be able to choose values in increments of 10.')
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_require_approval': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -1801,66 +1784,19 @@ Your {event} team"""))
|
||||
},
|
||||
'logo_image': {
|
||||
'default': None,
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
|
||||
'type': File
|
||||
},
|
||||
'logo_image_large': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Use header image in its full size'),
|
||||
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
|
||||
)
|
||||
'type': bool
|
||||
},
|
||||
'logo_show_title': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Show event title even if a header image is present'),
|
||||
help_text=_('The title will only be shown on the event front page.'),
|
||||
)
|
||||
'type': bool
|
||||
},
|
||||
'organizer_logo_image': {
|
||||
'default': None,
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
'type': File
|
||||
},
|
||||
'organizer_logo_image_large': {
|
||||
'default': 'False',
|
||||
@@ -1874,43 +1810,11 @@ Your {event} team"""))
|
||||
},
|
||||
'og_image': {
|
||||
'default': None,
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Social media image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
|
||||
'only the center square is shown. If you do not fill this, we will use the logo given above.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
'type': File
|
||||
},
|
||||
'invoice_logo_image': {
|
||||
'default': None,
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
'type': File
|
||||
},
|
||||
'frontpage_text': {
|
||||
'default': '',
|
||||
|
||||
@@ -82,14 +82,6 @@ class BaseDataShredder:
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def require_download_confirmation(self):
|
||||
"""
|
||||
Indicates whether the data of this shredder needs to be downloaded, before it is actually shredded. By default
|
||||
this value is equal to the tax relevant flag.
|
||||
"""
|
||||
return self.tax_relevant
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
· <a id='reload' href='#'>{% trans "Try again" %}</a>
|
||||
</p>
|
||||
{% if request.user.is_staff and not staff_session %}
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
|
||||
<p>
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-default" id="button-sudo">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
|
||||
</p>
|
||||
{% if request.user.is_staff and not staff_session %}
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
|
||||
<p>
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-default" id="button-sudo">
|
||||
|
||||
@@ -100,9 +100,8 @@ def truelink_callback(attrs, new=False):
|
||||
|
||||
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
||||
"""
|
||||
text = re.sub(r'[^a-zA-Z0-9.\-/_]', '', attrs.get('_text')) # clean up link text
|
||||
href_url = urllib.parse.urlparse(attrs[None, 'href'])
|
||||
if URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
|
||||
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
|
||||
@@ -110,6 +109,7 @@ def truelink_callback(attrs, new=False):
|
||||
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']
|
||||
|
||||
@@ -27,7 +27,7 @@ from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
||||
)
|
||||
from pretix.control.forms import (
|
||||
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
||||
ExtFileField, MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
||||
SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -35,6 +35,7 @@ from pretix.helpers.countries import CachedCountries
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.banktransfer.payment import BankTransfer
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
|
||||
class EventWizardFoundationForm(forms.Form):
|
||||
@@ -391,7 +392,7 @@ class EventUpdateForm(I18nModelForm):
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
||||
'sales_channels': CheckboxSelectMultiple(),
|
||||
'sales_channels': CheckboxSelectMultiple()
|
||||
}
|
||||
|
||||
|
||||
@@ -412,6 +413,36 @@ class EventSettingsForm(SettingsForm):
|
||||
"restrict the set of selectable titles."),
|
||||
required=False,
|
||||
)
|
||||
logo_image = ExtFileField(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
)
|
||||
logo_image_large = forms.BooleanField(
|
||||
label=_('Use header image in its full size'),
|
||||
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
|
||||
required=False,
|
||||
)
|
||||
logo_show_title = forms.BooleanField(
|
||||
label=_('Show event title even if a header image is present'),
|
||||
help_text=_('The title will only be shown on the event front page.'),
|
||||
required=False,
|
||||
)
|
||||
og_image = ExtFileField(
|
||||
label=_('Social media image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
|
||||
'only the center square is shown. If you do not fill this, we will use the logo given above.')
|
||||
)
|
||||
|
||||
auto_fields = [
|
||||
'imprint_url',
|
||||
@@ -464,10 +495,6 @@ class EventSettingsForm(SettingsForm):
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'logo_image',
|
||||
'logo_image_large',
|
||||
'logo_show_title',
|
||||
'og_image',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@@ -522,6 +549,9 @@ class EventSettingsForm(SettingsForm):
|
||||
if not self.event.has_subevents:
|
||||
del self.fields['frontpage_subevent_ordering']
|
||||
del self.fields['event_list_type']
|
||||
self.fields['primary_font'].choices += [
|
||||
(a, {"title": a, "data": v}) for a, v in get_fonts().items()
|
||||
]
|
||||
|
||||
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
|
||||
self.virtual_keys = []
|
||||
@@ -568,7 +598,6 @@ class CancelSettingsForm(SettingsForm):
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_adjust_fees_explanation',
|
||||
'cancel_allow_user_paid_adjust_fees_step',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
'cancel_allow_user_paid_require_approval',
|
||||
'change_allow_user_variation',
|
||||
@@ -704,7 +733,6 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
'invoice_additional_text',
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'invoice_logo_image',
|
||||
]
|
||||
|
||||
invoice_generate_sales_channels = forms.MultipleChoiceField(
|
||||
@@ -724,6 +752,13 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
label=_("Invoice language"),
|
||||
choices=[('__user__', _('The user\'s language'))] + settings.LANGUAGES,
|
||||
)
|
||||
invoice_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.get('obj')
|
||||
|
||||
@@ -5,7 +5,7 @@ from urllib.parse import urlencode
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet
|
||||
from django.db.models import Exists, F, Model, OuterRef, Q, QuerySet
|
||||
from django.db.models.functions import Coalesce, ExtractWeekDay
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.formats import date_format, localize
|
||||
@@ -239,10 +239,6 @@ class OrderFilterForm(FilterForm):
|
||||
elif s == 'rc':
|
||||
qs = qs.filter(
|
||||
cancellation_requests__isnull=False
|
||||
).annotate(
|
||||
cancellation_request_time=Max('cancellation_requests__created')
|
||||
).order_by(
|
||||
'-cancellation_request_time'
|
||||
)
|
||||
elif s == 'pendingpaid':
|
||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
||||
@@ -462,16 +458,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
required=False,
|
||||
label=_('Total amount'),
|
||||
)
|
||||
payment_sum_min = forms.DecimalField(
|
||||
localize=True,
|
||||
required=False,
|
||||
label=_('Minimal sum of payments and refunds'),
|
||||
)
|
||||
payment_sum_max = forms.DecimalField(
|
||||
localize=True,
|
||||
required=False,
|
||||
label=_('Maximal sum of payments and refunds'),
|
||||
)
|
||||
sales_channel = forms.ChoiceField(
|
||||
label=_('Sales channel'),
|
||||
required=False,
|
||||
@@ -598,16 +584,6 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
qs = qs.filter(email_known_to_work=fdata.get('email_known_to_work'))
|
||||
if fdata.get('locale'):
|
||||
qs = qs.filter(locale=fdata.get('locale'))
|
||||
if fdata.get('payment_sum_min') is not None:
|
||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
||||
qs = qs.filter(
|
||||
computed_payment_refund_sum__gte=fdata['payment_sum_min'],
|
||||
)
|
||||
if fdata.get('payment_sum_max') is not None:
|
||||
qs = Order.annotate_overpayments(qs, refunds=False, results=False, sums=True)
|
||||
qs = qs.filter(
|
||||
computed_payment_refund_sum__lte=fdata['payment_sum_max'],
|
||||
)
|
||||
if fdata.get('invoice_address_company'):
|
||||
qs = qs.filter(invoice_address__company__icontains=fdata.get('invoice_address_company'))
|
||||
if fdata.get('invoice_address_name'):
|
||||
@@ -1510,9 +1486,6 @@ class VoucherTagFilterForm(FilterForm):
|
||||
|
||||
|
||||
class RefundFilterForm(FilterForm):
|
||||
orders = {'provider': 'provider', 'state': 'state', 'order': 'order__code',
|
||||
'source': 'source', 'amount': 'amount', 'created': 'created'}
|
||||
|
||||
provider = forms.ChoiceField(
|
||||
label=_('Payment provider'),
|
||||
choices=[
|
||||
@@ -1549,10 +1522,6 @@ class RefundFilterForm(FilterForm):
|
||||
qs = qs.filter(state__in=[OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_EXTERNAL])
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
else:
|
||||
qs = qs.order_by('-created')
|
||||
return qs
|
||||
|
||||
|
||||
|
||||
@@ -616,7 +616,7 @@ class EventCancelForm(forms.Form):
|
||||
required=False
|
||||
)
|
||||
manual_refund = forms.BooleanField(
|
||||
label=_('Create manual refund if the payment method does not support automatic refunds'),
|
||||
label=_('Create manual refund if the payment method odes not support automatic refunds'),
|
||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
|
||||
initial=True,
|
||||
required=False,
|
||||
|
||||
@@ -22,12 +22,6 @@ from pretix.helpers.money import change_decimal_field
|
||||
class SubEventForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs['event']
|
||||
instance = kwargs.get('instance')
|
||||
if instance and not instance.pk:
|
||||
kwargs['initial'].setdefault('name', self.event.name)
|
||||
kwargs['initial'].setdefault('location', self.event.location)
|
||||
kwargs['initial'].setdefault('geo_lat', self.event.geo_lat)
|
||||
kwargs['initial'].setdefault('geo_lon', self.event.geo_lon)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ class VoucherBulkForm(VoucherForm):
|
||||
data['bulk'] = True
|
||||
del data['codes']
|
||||
objs.append(obj)
|
||||
Voucher.objects.bulk_create(objs, batch_size=200)
|
||||
Voucher.objects.bulk_create(objs)
|
||||
objs = []
|
||||
for v in event.vouchers.filter(code__in=self.cleaned_data['codes']):
|
||||
# We need to query them again as bulk_create does not fill in .pk values on databases
|
||||
|
||||
@@ -357,8 +357,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'account.'),
|
||||
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
|
||||
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
|
||||
'pretix.control.auth.user.forgot_password.denied.repeated': _('A repeated password reset has been denied, as '
|
||||
'the last request was less than 24 hours ago.'),
|
||||
'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
|
||||
'pretix.voucher.added': _('The voucher has been created.'),
|
||||
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
|
||||
|
||||
@@ -6,13 +6,6 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
def current_url(request):
|
||||
if len(request.GET):
|
||||
return request.path + '?' + request.GET.urlencode()
|
||||
else:
|
||||
return request.path
|
||||
|
||||
|
||||
def event_permission_required(permission):
|
||||
"""
|
||||
This view decorator rejects all requests with a 403 response which are not from
|
||||
@@ -101,7 +94,7 @@ def administrator_permission_required():
|
||||
raise PermissionDenied()
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
if request.user.is_staff:
|
||||
return redirect(reverse('control:user.sudo') + '?next=' + quote(current_url(request)))
|
||||
return redirect(reverse('control:user.sudo') + '?next=' + quote(request.path))
|
||||
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||
return function(request, *args, **kw)
|
||||
return wrapper
|
||||
|
||||
@@ -291,7 +291,7 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
||||
"""
|
||||
|
||||
subevent_forms = EventPluginSignal(
|
||||
providing_args=['request', 'subevent', 'copy_from']
|
||||
providing_args=['request', 'subevent']
|
||||
)
|
||||
"""
|
||||
This signal allows you to return additional forms that should be rendered on the subevent creation
|
||||
@@ -301,8 +301,7 @@ as part of the standard validation and rendering cycle and rendered using defaul
|
||||
styles. It is advisable to set a prefix for your form to avoid clashes with other plugins.
|
||||
|
||||
``subevent`` can be ``None`` during creation. Before ``save()`` is called, a ``subevent`` property of
|
||||
your form instance will automatically being set to the subevent that has just been created. During
|
||||
creation, ``copy_from`` can be a subevent that is being copied from.
|
||||
your form instance will automatically being set to the subevent that has just been created.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
|
||||
{% if request.user.is_staff and not staff_session %}
|
||||
<li>
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-link" id="button-sudo">
|
||||
<i class="fa fa-id-card"></i> {% trans "Admin mode" %}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
{% bootstrap_field form.cancel_allow_user_paid_adjust_fees layout="control" %}
|
||||
<div data-display-dependency="#id_cancel_allow_user_paid_adjust_fees">
|
||||
{% bootstrap_field form.cancel_allow_user_paid_adjust_fees_explanation layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_adjust_fees_step layout="control" %}
|
||||
</div>
|
||||
{% bootstrap_field form.cancel_allow_user_paid_refund_as_giftcard layout="control" %}
|
||||
{% if not gets_notification %}
|
||||
|
||||
@@ -12,18 +12,6 @@
|
||||
<p>
|
||||
{% trans "Your shop is currently live. If you take it down, it will only be visible to you and your team." %}
|
||||
</p>
|
||||
{% if issues|length > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
{% trans "Your shop is already live, however the following issues would normally prevent your shop to go live:" %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
<li>{{ issue|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="text-right flip">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="live" value="false">
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
{% if form.instance.id %}
|
||||
<br><small class="text-muted">#{{ form.instance.id }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Product name" %}</th>
|
||||
<th></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
@@ -50,18 +50,7 @@
|
||||
<a href="
|
||||
{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}">{{ i }}</a>
|
||||
{% if not i.active %}</strike>{% endif %}
|
||||
</strong>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
#{{ i.pk }}
|
||||
{% for k, c in sales_channels.items %}
|
||||
{% if k in i.sales_channels %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{% if i.available_from or i.available_until %}
|
||||
@@ -80,8 +69,6 @@
|
||||
<td>
|
||||
{% if i.admission %}
|
||||
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket" %}"></span>
|
||||
{% elif i.issue_giftcard %}
|
||||
<span class="fa fa-gift fa-fw text-muted" data-toggle="tooltip" title="{% trans "Gift card" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@@ -93,9 +80,6 @@
|
||||
{% if i.category.is_addon %}
|
||||
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
|
||||
title="{% trans "Only available as an add-on product" %}"></span>
|
||||
{% elif i.require_bundling %}
|
||||
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
|
||||
title="{% trans "Only available as part of a bundle" %}"></span>
|
||||
{% elif i.hide_without_voucher %}
|
||||
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
|
||||
title="{% trans "Only visible with a voucher" %}"></span>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
{% load rich_text %}
|
||||
{% load safelink %}
|
||||
{% load eventsignal %}
|
||||
{% load l10n %}
|
||||
{% load phone_format %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
@@ -98,10 +97,7 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="start-action" value="do_nothing">
|
||||
<input type="hidden" name="start-mode" value="partial">
|
||||
{% localize off %}
|
||||
<input type="hidden" name="start-partial_amount" value="{{ overpaid|floatformat:2 }}">
|
||||
{% endlocalize %}
|
||||
<input type="hidden" name="comment" value="{% trans "Refund for overpayment" %}">
|
||||
<input type="hidden" name="start-partial_amount" value="{{ overpaid }}">
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed with amount=overpaid|money:request.event.currency %}
|
||||
This order is currently overpaid by {{ amount }}.
|
||||
@@ -763,19 +759,11 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if r.html_info or staff_session or r.comment %}
|
||||
{% if r.html_info %}
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="7">
|
||||
{% if r.comment %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Comment" %}</dt>
|
||||
<dd>{{ r.comment }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% if r.html_info %}
|
||||
{{ r.html_info|safe }}
|
||||
{% endif %}
|
||||
{{ r.html_info|safe }}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
<a href="" class="btn btn-default btn-xs" data-expandrefund
|
||||
@@ -787,6 +775,17 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif staff_session %}
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="7">
|
||||
<a href="" class="btn btn-default btn-xs" data-expandrefund
|
||||
data-id="{{ r.pk }}">
|
||||
<span class="fa-eye fa fa-fw"></span>
|
||||
{% trans "Inspect" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -17,41 +17,38 @@
|
||||
</h1>
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
<fieldset class="form-inline form-refund-choose">
|
||||
<fieldset class="form-inline form-order-change">
|
||||
<legend>{% trans "How should the refund be sent?" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Any payments that you selected for automatical refunds will be immediately communicate the refund
|
||||
request to the respective payment provider. Manual refunds will be created as pending refunds, you
|
||||
can then later mark them as done once you actually transferred the money back to the customer.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<h4>{% trans "Refund to original payment method" %}</h4>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Payment" %}</th>
|
||||
<th>{% trans "Payment details" %}</th>
|
||||
<th>#</th>
|
||||
<th>{% trans "Payment confirmation date" %}</th>
|
||||
<th>{% trans "Payment method" %}</th>
|
||||
<th>{% trans "Amount not refunded" %}</th>
|
||||
<th class="text-right flip refund-amount">{% trans "Refund amount" %}</th>
|
||||
<th>{% trans "Refund" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in payments %}
|
||||
<tr>
|
||||
<td>{{ p.full_id }}<br/>{{ p.payment_date|date:"SHORT_DATETIME_FORMAT" }}<br/>{{ p.payment_provider.verbose_name }}</td>
|
||||
<td class="payment-details">{{ p.html_info|default_if_none:""|safe }}</td>
|
||||
<td>{{ p.full_id }}</td>
|
||||
<td>{{ p.payment_date|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{{ p.payment_provider.verbose_name }}
|
||||
</td>
|
||||
<td>{{ p.available_amount|money:request.event.currency }}</td>
|
||||
<td class="text-right flip refund-amount">
|
||||
<td>
|
||||
{% if p.partial_refund_possible %}
|
||||
{% trans "Automatically refund" context "amount_label" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="refund-{{ p.pk }}"
|
||||
{% if p.propose_refund %}
|
||||
value="{{ p.propose_refund|floatformat:2 }}"
|
||||
value="{{ p.propose_refund|floatformat:2 }}"
|
||||
{% else %}
|
||||
placeholder="{{ p.propose_refund|floatformat:2 }}"
|
||||
placeholder="{{ p.propose_refund|floatformat:2 }}"
|
||||
{% endif %}
|
||||
title="" class="form-control">
|
||||
<span class="input-group-addon">
|
||||
@@ -63,80 +60,45 @@
|
||||
<input type="checkbox" name="refund-{{ p.pk }}"
|
||||
value="{{ p.amount|floatformat:2 }}"
|
||||
{% if p.propose_refund == p.amount %}checked{% endif %}>
|
||||
{% trans "Full amount" %} ({{ p.amount|money:request.event.currency }})
|
||||
{% trans "Automatically refund full amount" %}
|
||||
</label>
|
||||
{% else %}
|
||||
<em class="text-muted">{% trans "This payment method does not support automatic refunds." %}</em>
|
||||
<em>{% trans "This payment method does not support automatic refunds." %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4>{% trans "Refund to a different payment method" %}</h4>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Payment method" %}</th>
|
||||
<th>{% trans "Recipient / options" %}</th>
|
||||
<th class="text-right flip refund-amount">{% trans "Refund amount" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for prov, form in new_refunds %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
{{ prov.verbose_name }}
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ form|safe }}
|
||||
</td>
|
||||
<td class="text-right flip refund-amount">
|
||||
<div class="input-group">
|
||||
<input type="text" name="newrefund-{{ prov }}"
|
||||
placeholder="{{ 0|floatformat:2 }}"
|
||||
title="" class="form-control">
|
||||
<span class="input-group-addon">
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><strong>{% trans "Transfer to other order" %}</strong></td>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="text" name="order-offsetting" placeholder="{% trans "Order code" %}"
|
||||
value="" title="" class="form-control">
|
||||
</td>
|
||||
<td class="text-right flip refund-amount">
|
||||
{% trans "Transfer" context "amount_label" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="refund-offsetting"
|
||||
title="" class="form-control" placeholder="{{ 0|floatformat:2 }}">
|
||||
title="" class="form-control" placeholder="{{ 0|floatformat:2 }}">
|
||||
<span class="input-group-addon">
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div>
|
||||
{% trans "to" context "order_label" %}
|
||||
<input type="text" name="order-offsetting"
|
||||
value="" title="" class="form-control">
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>
|
||||
<strong>{% trans "Create a new gift card" %}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<div class="text-muted">
|
||||
{% trans "The gift card can be used to buy tickets for all events of this organizer." %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right flip refund-amount">
|
||||
<div class="input-group">
|
||||
<input type="text" name="refund-new-giftcard"
|
||||
title="" class="form-control"
|
||||
title="" class="form-control"
|
||||
{% if giftcard_proposal %}
|
||||
value="{{ giftcard_proposal|floatformat:2 }}"
|
||||
{% else %}
|
||||
@@ -147,53 +109,59 @@
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% trans "Manual refund" %}</strong></td>
|
||||
<td>
|
||||
<label class="radio no-bold">
|
||||
<input type="radio" name="manual_state" value="created" checked>
|
||||
{% trans "Keep transfer as to do" %}
|
||||
</label><br>
|
||||
<label class="radio no-bold">
|
||||
<input type="radio" name="manual_state" value="done">
|
||||
{% trans "Mark refund as done" %}
|
||||
</label>
|
||||
<div class="text-muted">
|
||||
{% trans "The gift card can be used to buy tickets for all events of this organizer." %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-right flip refund-amount">
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><strong>{% trans "Manual refund" %}</strong></td>
|
||||
<td></td>
|
||||
<td>
|
||||
{% trans "Manually refund" context "amount_label" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="refund-manual"
|
||||
{% if remainder %}
|
||||
value="{{ remainder|floatformat:2 }}"
|
||||
value="{{ remainder|floatformat:2 }}"
|
||||
{% else %}
|
||||
placeholder="{{ remainder|floatformat:2 }}"
|
||||
placeholder="{{ remainder|floatformat:2 }}"
|
||||
{% endif %}
|
||||
title="" class="form-control">
|
||||
<span class="input-group-addon">
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div>
|
||||
<label class="radio">
|
||||
<input type="radio" name="manual_state" value="created" checked>
|
||||
{% trans "Keep transfer as to do" %}
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="manual_state" value="done">
|
||||
{% trans "Mark refund as done" %}
|
||||
</label>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Any payments that you selected for automatical refunds will be immediately communicate the refund
|
||||
request to the respective payment provider. Manual refunds will be created as pending refunds, you
|
||||
can then later mark them as done once you actually transferred the money back to the customer.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p> </p>
|
||||
|
||||
<input type="hidden" name="start-action" value="{{ start_form.cleaned_data.action }}">
|
||||
<input type="hidden" name="start-mode" value="{{ start_form.cleaned_data.mode }}">
|
||||
<input type="hidden" name="start-partial_amount" value="{{ partial_amount }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="id_comment">{% trans "Refund reason" %}</label>
|
||||
<input type="text" name="comment" class="form-control" title="{% trans "May be shown to the end user or used e.g. as part of a payment reference." %}" id="id_comment"
|
||||
value="{{ comment|default:"" }}">
|
||||
<div class="help-block">{% trans "May be shown to the end user or used e.g. as part of a payment reference." %}</div>
|
||||
</div>
|
||||
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
|
||||
@@ -38,36 +38,12 @@
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
#
|
||||
<a href="?{% url_replace request 'ordering' '-order' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'order' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Payment provider" %}
|
||||
<a href="?{% url_replace request 'ordering' '-provider' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'provider' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Start date" %}
|
||||
<a href="?{% url_replace request 'ordering' '-created' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'created' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Source" %}
|
||||
<a href="?{% url_replace request 'ordering' '-source' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'source' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Status" %}
|
||||
<a href="?{% url_replace request 'ordering' '-state' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'state' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th class="text-right flip">
|
||||
{% trans "Amount" %}
|
||||
<a href="?{% url_replace request 'ordering' '-amount' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'amount' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</th>
|
||||
<th>#</th>
|
||||
<th>{% trans "Payment provider" %}</th>
|
||||
<th>{% trans "Start date" %}</th>
|
||||
<th>{% trans "Source" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th class="text-right flip">{% trans "Amount" %}</th>
|
||||
<th class="text-right flip">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -11,16 +11,12 @@
|
||||
method="post" class="form-horizontal" data-asynctask>
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% if download_on_shred %}
|
||||
<legend>{% trans "Step 1: Download data" %}</legend>
|
||||
{% else %}
|
||||
<legend>{% trans "(Optional) Step 1: Download data" %}</legend>
|
||||
{% endif %}
|
||||
<legend>{% trans "Step 1: Download data" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You are about to permanently delete data from the server, even though you might be required to
|
||||
keep
|
||||
some of this data on file. You can therefore download the following file and store it in a safe
|
||||
some of this data on file. You should therefore download the following file and store it in a safe
|
||||
place:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
@@ -31,7 +27,18 @@
|
||||
</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Step 2: Confirm deletion" %}</legend>
|
||||
<legend>{% trans "Step 2: Confirm download" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
In the downloaded file, there is a text file named "CONFIRM_CODE.txt" with a six-character code.
|
||||
Please enter this code here to confirm that you successfully downloaded the file.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<input type="text" class="form-control" name="confirm_code" required placeholder="{% trans "Confirmation code" %}">
|
||||
<br>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Step 3: Confirm deletion" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed with event=request.event.name slug=request.event.slug %}
|
||||
Please re-check that you are fully certain that you want to delete the selected categories of data from the event <strong>{{ event }}</strong>.
|
||||
@@ -39,21 +46,7 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<input type="text" class="form-control" name="slug" required placeholder="{% trans "Event short name" %}">
|
||||
<br>
|
||||
</fieldset>
|
||||
{% if download_on_shred %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Step 3: Confirm download" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
In the downloaded file, there is a text file named "CONFIRM_CODE.txt" with a six-character code.
|
||||
Please enter this code here to confirm that you successfully downloaded the file.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<input type="text" class="form-control" name="confirm_code" required placeholder="{% trans "Confirmation code" %}">
|
||||
<br>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<input type="hidden" name="file" value="{{ file.pk }}">
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -332,51 +332,9 @@
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<div class="panel panel-default hidden" id="subevent_add_many_slots">
|
||||
<div class="panel-body row">
|
||||
<div class="col-md-2 col-sm-12">
|
||||
<label for="subevent_add_many_slots_first">
|
||||
<strong>{% trans "Start of first slot" %}</strong>
|
||||
</label>
|
||||
<input class="form-control timepickerfield" id="subevent_add_many_slots_first" value="{{ time_begin_sample }}">
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-12">
|
||||
<label for="subevent_add_many_slots_end">
|
||||
<strong>{% trans "End of time slots" %}</strong>
|
||||
</label>
|
||||
<input class="form-control timepickerfield" id="subevent_add_many_slots_end" value="{{ time_end_sample }}">
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12">
|
||||
<label for="subevent_add_many_slots_length">
|
||||
<strong>{% trans "Length of slots" %}</strong>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="subevent_add_many_slots_length" value="15">
|
||||
<span class="input-group-addon">{% trans "minutes" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-12">
|
||||
<label for="subevent_add_many_slots_break">
|
||||
<strong>{% trans "Break between slots" %}</strong>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="subevent_add_many_slots_break" value="0">
|
||||
<span class="input-group-addon">{% trans "minutes" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-12">
|
||||
<label> </label>
|
||||
<button class="btn-block btn btn-primary" id="subevent_add_many_slots_go" type="button">
|
||||
<span class="fa fa-check"></span> {% trans "Create" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a single time slot" %}</button>
|
||||
<button type="button" class="btn btn-default" id="subevent_add_many_slots_start">
|
||||
<i class="fa fa-calendar"></i> {% trans "Add many time slots" %}</button>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new time slot" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control input-xs"
|
||||
id="voucher-bulk-codes-num"
|
||||
placeholder="{% trans "Number" context "number_of_things" %}">
|
||||
placeholder="{% trans "Number" %}">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" id="voucher-bulk-codes-generate"
|
||||
data-rng-url="{% url 'control:event.vouchers.rng' organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
|
||||
@@ -31,11 +31,6 @@
|
||||
here immediately. If you want, you can also send them out manually right now.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if not running %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Currently, no vouchers will be sent since your event is not live or is not selling tickets." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -23,18 +23,11 @@ class GeoCodeView(LoginRequiredMixin, View):
|
||||
}, status=200)
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
try:
|
||||
if gs.settings.opencagedata_apikey:
|
||||
res = self._use_opencage(q)
|
||||
elif gs.settings.mapquest_apikey:
|
||||
res = self._use_mapquest(q)
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'results': []
|
||||
}, status=200)
|
||||
except IOError:
|
||||
logger.exception("Geocoding failed")
|
||||
if gs.settings.opencagedata_apikey:
|
||||
res = self._use_opencage(q)
|
||||
if gs.settings.mapquest_apikey:
|
||||
res = self._use_mapquest(q)
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'results': []
|
||||
@@ -49,13 +42,21 @@ class GeoCodeView(LoginRequiredMixin, View):
|
||||
def _use_opencage(self, q):
|
||||
gs = GlobalSettingsObject()
|
||||
|
||||
r = requests.get(
|
||||
'https://api.opencagedata.com/geocode/v1/json?q={}&key={}'.format(
|
||||
quote(q), gs.settings.opencagedata_apikey
|
||||
try:
|
||||
r = requests.get(
|
||||
'https://api.opencagedata.com/geocode/v1/json?q={}&key={}'.format(
|
||||
quote(q), gs.settings.opencagedata_apikey
|
||||
)
|
||||
)
|
||||
)
|
||||
r.raise_for_status()
|
||||
d = r.json()
|
||||
r.raise_for_status()
|
||||
except IOError:
|
||||
logger.exception("Geocoding failed")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'results': []
|
||||
}, status=200)
|
||||
else:
|
||||
d = r.json()
|
||||
res = [
|
||||
{
|
||||
'formatted': r['formatted'],
|
||||
@@ -68,13 +69,21 @@ class GeoCodeView(LoginRequiredMixin, View):
|
||||
def _use_mapquest(self, q):
|
||||
gs = GlobalSettingsObject()
|
||||
|
||||
r = requests.get(
|
||||
'https://www.mapquestapi.com/geocoding/v1/address?location={}&key={}'.format(
|
||||
quote(q), gs.settings.mapquest_apikey
|
||||
try:
|
||||
r = requests.get(
|
||||
'https://www.mapquestapi.com/geocoding/v1/address?location={}&key={}'.format(
|
||||
quote(q), gs.settings.mapquest_apikey
|
||||
)
|
||||
)
|
||||
)
|
||||
r.raise_for_status()
|
||||
d = r.json()
|
||||
r.raise_for_status()
|
||||
except IOError:
|
||||
logger.exception("Geocoding failed")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'results': []
|
||||
}, status=200)
|
||||
else:
|
||||
d = r.json()
|
||||
res = [
|
||||
{
|
||||
'formatted': q,
|
||||
|
||||
@@ -46,7 +46,6 @@ from pretix.control.permissions import (
|
||||
from pretix.control.signals import item_forms, item_formsets
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...base.channels import get_all_sales_channels
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
@@ -67,11 +66,6 @@ class ItemList(ListView):
|
||||
'category__position', 'category', 'position'
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['sales_channels'] = get_all_sales_channels()
|
||||
return ctx
|
||||
|
||||
|
||||
def item_move(request, item, up=True):
|
||||
"""
|
||||
|
||||
@@ -5,12 +5,11 @@ import os
|
||||
import re
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal, DecimalException
|
||||
from urllib.parse import quote, urlencode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import vat_moss.id
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
@@ -760,7 +759,6 @@ class OrderRefundView(OrderView):
|
||||
|
||||
def choose_form(self):
|
||||
payments = list(self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED))
|
||||
comment = self.request.POST.get("comment") or self.request.GET.get("comment") or None
|
||||
if self.start_form.cleaned_data.get('mode') == 'full':
|
||||
full_refund = self.order.payment_refund_sum
|
||||
else:
|
||||
@@ -802,7 +800,6 @@ class OrderRefundView(OrderView):
|
||||
else OrderRefund.REFUND_STATE_CREATED
|
||||
),
|
||||
amount=manual_value,
|
||||
comment=comment,
|
||||
provider='manual'
|
||||
))
|
||||
|
||||
@@ -830,7 +827,6 @@ class OrderRefundView(OrderView):
|
||||
execution_date=now(),
|
||||
amount=giftcard_value,
|
||||
provider='giftcard',
|
||||
comment=comment,
|
||||
info=json.dumps({
|
||||
'gift_card': giftcard.pk
|
||||
})
|
||||
@@ -861,35 +857,11 @@ class OrderRefundView(OrderView):
|
||||
execution_date=now(),
|
||||
amount=offsetting_value,
|
||||
provider='offsetting',
|
||||
comment=comment,
|
||||
info=json.dumps({
|
||||
'orders': [order.code]
|
||||
})
|
||||
))
|
||||
|
||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
|
||||
prof_value = formats.sanitize_separators(prof_value)
|
||||
try:
|
||||
prof_value = Decimal(prof_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
continue
|
||||
if prof_value > Decimal('0.00'):
|
||||
try:
|
||||
refund = prov.new_refund_control_form_process(self.request, prof_value, self.order)
|
||||
except ValidationError as e:
|
||||
for err in e:
|
||||
messages.error(self.request, err)
|
||||
is_valid = False
|
||||
continue
|
||||
if refund:
|
||||
refund_selected += refund.amount
|
||||
refund.comment = comment
|
||||
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
|
||||
refunds.append(refund)
|
||||
|
||||
for p in payments:
|
||||
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
|
||||
value = formats.sanitize_separators(value)
|
||||
@@ -919,7 +891,6 @@ class OrderRefundView(OrderView):
|
||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=value,
|
||||
comment=comment,
|
||||
provider=p.provider
|
||||
))
|
||||
|
||||
@@ -931,7 +902,7 @@ class OrderRefundView(OrderView):
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
}, user=self.request.user)
|
||||
if r.provider != "manual":
|
||||
if r.payment or r.provider == "offsetting" or r.provider == "giftcard":
|
||||
try:
|
||||
r.payment_provider.execute_refund(r)
|
||||
except PaymentException as e:
|
||||
@@ -993,24 +964,10 @@ class OrderRefundView(OrderView):
|
||||
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
|
||||
'amount.'))
|
||||
|
||||
new_refunds = []
|
||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||
form = prov.new_refund_control_form_render(self.request, self.order)
|
||||
if form:
|
||||
new_refunds.append(
|
||||
(prov, form)
|
||||
)
|
||||
|
||||
for p in payments:
|
||||
if p.payment_provider:
|
||||
p.html_info = (p.payment_provider.payment_control_render_short(p) or "").strip()
|
||||
|
||||
return render(self.request, 'pretixcontrol/order/refund_choose.html', {
|
||||
'payments': payments,
|
||||
'new_refunds': new_refunds,
|
||||
'remainder': to_refund,
|
||||
'order': self.order,
|
||||
'comment': comment,
|
||||
'giftcard_proposal': giftcard_proposal,
|
||||
'partial_amount': (
|
||||
self.request.POST.get('start-partial_amount') if self.request.method == 'POST'
|
||||
@@ -1141,16 +1098,14 @@ class OrderTransition(OrderView):
|
||||
if self.order.pending_sum < 0:
|
||||
messages.success(self.request, _('The order has been canceled. You can now select how you want to '
|
||||
'transfer the money back to the user.'))
|
||||
with language(self.order.locale):
|
||||
return redirect(reverse('control:event.order.refunds.start', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'code': self.order.code
|
||||
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}&giftcard={}&comment={}'.format(
|
||||
round_decimal(self.order.pending_sum * -1),
|
||||
'true' if self.req and self.req.refund_as_giftcard else 'false',
|
||||
quote(gettext('Order canceled'))
|
||||
))
|
||||
return redirect(reverse('control:event.order.refunds.start', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'code': self.order.code
|
||||
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}&giftcard={}'.format(
|
||||
round_decimal(self.order.pending_sum * -1),
|
||||
'true' if self.req and self.req.refund_as_giftcard else 'false'
|
||||
))
|
||||
|
||||
messages.success(self.request, _('The order has been canceled.'))
|
||||
elif self.order.status == Order.STATUS_PENDING and to == 'e':
|
||||
@@ -1679,16 +1634,10 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
|
||||
self.invoice_form.save()
|
||||
self.order.log_action('pretix.event.order.modified', {
|
||||
'invoice_data': self.invoice_form.cleaned_data,
|
||||
'data': [
|
||||
dict(
|
||||
position=f.orderpos.pk,
|
||||
**{
|
||||
k: (f.cleaned_data.get(k).name if isinstance(f.cleaned_data.get(k),
|
||||
File) else f.cleaned_data.get(k))
|
||||
for k in f.changed_data
|
||||
}
|
||||
) for f in self.forms
|
||||
]
|
||||
'data': [{
|
||||
k: (f.cleaned_data.get(k).name if isinstance(f.cleaned_data.get(k), File) else f.cleaned_data.get(k))
|
||||
for k in f.changed_data
|
||||
} for f in self.forms]
|
||||
}, user=request.user)
|
||||
if self.invoice_form.has_changed():
|
||||
success_message = ('The invoice address has been updated. If you want to generate a new invoice, '
|
||||
@@ -2025,7 +1974,7 @@ class OrderGo(EventPermissionRequiredMixin, View):
|
||||
try:
|
||||
return Order.objects.get(code=code, event=self.request.event)
|
||||
except Order.DoesNotExist:
|
||||
return Order.objects.get(code=Order.normalize_code(code, is_fallback=True), event=self.request.event)
|
||||
return Order.objects.get(code=Order.normalize_code(code), event=self.request.event)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
code = request.GET.get("code", "").upper().strip()
|
||||
@@ -2095,7 +2044,7 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
return reverse('control:event.orders.export', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}) + '?identifier=' + self.exporter.identifier
|
||||
})
|
||||
|
||||
@cached_property
|
||||
def exporter(self):
|
||||
@@ -2106,22 +2055,14 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not self.exporter:
|
||||
messages.error(self.request, _('The selected exporter was not found.'))
|
||||
return redirect(reverse('control:event.orders.export', kwargs={
|
||||
return redirect('control:event.orders.export', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}))
|
||||
})
|
||||
|
||||
if not self.exporter.form.is_valid():
|
||||
messages.error(
|
||||
self.request,
|
||||
str(_('There was a problem processing your input:')) + ' ' + ', '.join(
|
||||
', '.join(line) for line in self.exporter.form.errors.values()
|
||||
)
|
||||
)
|
||||
return redirect(reverse('control:event.orders.export', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug
|
||||
}) + '?identifier=' + self.exporter.identifier)
|
||||
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.date = now()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import json
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
@@ -45,27 +43,8 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
|
||||
template_name = 'pretixcontrol/shredder/download.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
try:
|
||||
cf = CachedFile.objects.get(pk=kwargs['file'])
|
||||
except CachedFile.DoesNotExist:
|
||||
raise ShredError(_("The download file could no longer be found on the server, please try to start again."))
|
||||
|
||||
with ZipFile(cf.file.file, 'r') as zipfile:
|
||||
indexdata = json.loads(zipfile.read('index.json').decode())
|
||||
|
||||
if indexdata['organizer'] != kwargs['organizer'] or indexdata['event'] != kwargs['event']:
|
||||
raise ShredError(_("This file is from a different event."))
|
||||
|
||||
shredders = []
|
||||
for s in indexdata['shredders']:
|
||||
shredder = self.shredders.get(s)
|
||||
if not shredder:
|
||||
continue
|
||||
shredders.append(shredder)
|
||||
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['shredders'] = self.shredders
|
||||
ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders)
|
||||
ctx['file'] = get_object_or_404(CachedFile, pk=kwargs.get("file"))
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import copy
|
||||
from datetime import datetime, time, timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
|
||||
from django.contrib import messages
|
||||
@@ -11,7 +11,6 @@ from django.forms import inlineformset_factory
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
@@ -151,8 +150,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
@cached_property
|
||||
def plugin_forms(self):
|
||||
forms = []
|
||||
for rec, resp in subevent_forms.send(sender=self.request.event, subevent=self.object, request=self.request,
|
||||
copy_from=self.copy_from):
|
||||
for rec, resp in subevent_forms.send(sender=self.request.event, subevent=self.object, request=self.request):
|
||||
if isinstance(resp, (list, tuple)):
|
||||
forms.extend(resp)
|
||||
else:
|
||||
@@ -324,7 +322,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
|
||||
@cached_property
|
||||
def copy_from(self):
|
||||
if self.request.GET.get("copy_from") and (not getattr(self, 'object', None) or not self.object.pk):
|
||||
if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
|
||||
try:
|
||||
return self.request.event.subevents.get(pk=self.request.GET.get("copy_from"))
|
||||
except SubEvent.DoesNotExist:
|
||||
@@ -624,10 +622,6 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['rrule_formset'] = self.rrule_formset
|
||||
ctx['time_formset'] = self.time_formset
|
||||
|
||||
tf = get_format('TIME_INPUT_FORMATS')[0]
|
||||
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
|
||||
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -327,7 +327,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
v.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user, save=False)
|
||||
)
|
||||
voucherids.append(v.pk)
|
||||
LogEntry.objects.bulk_create(log_entries, batch_size=200)
|
||||
LogEntry.objects.bulk_create(log_entries)
|
||||
|
||||
if form.cleaned_data['send']:
|
||||
vouchers_send.apply_async(kwargs={
|
||||
|
||||
@@ -160,35 +160,20 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
quota_cache = {}
|
||||
any_avail = False
|
||||
for wle in ctx[self.context_object_name]:
|
||||
if (wle.item, wle.variation, wle.subevent) in itemvar_cache:
|
||||
wle.availability = itemvar_cache.get((wle.item, wle.variation, wle.subevent))
|
||||
if (wle.item, wle.variation) in itemvar_cache:
|
||||
wle.availability = itemvar_cache.get((wle.item, wle.variation))
|
||||
else:
|
||||
ev = (wle.subevent or self.request.event)
|
||||
disabled = (
|
||||
not ev.presale_is_running or
|
||||
(wle.subevent and not wle.subevent.active) or
|
||||
not wle.item.is_available()
|
||||
wle.availability = (
|
||||
wle.variation.check_quotas(count_waitinglist=False, subevent=wle.subevent, _cache=quota_cache)
|
||||
if wle.variation
|
||||
else wle.item.check_quotas(count_waitinglist=False, subevent=wle.subevent, _cache=quota_cache)
|
||||
)
|
||||
if disabled:
|
||||
wle.availability = (0, "forbidden")
|
||||
else:
|
||||
wle.availability = (
|
||||
wle.variation.check_quotas(count_waitinglist=False, subevent=wle.subevent, _cache=quota_cache)
|
||||
if wle.variation
|
||||
else wle.item.check_quotas(count_waitinglist=False, subevent=wle.subevent, _cache=quota_cache)
|
||||
)
|
||||
itemvar_cache[(wle.item, wle.variation, wle.subevent)] = wle.availability
|
||||
itemvar_cache[(wle.item, wle.variation)] = wle.availability
|
||||
if wle.availability[0] == 100:
|
||||
any_avail = True
|
||||
|
||||
ctx['any_avail'] = any_avail
|
||||
ctx['estimate'] = self.get_sales_estimate()
|
||||
|
||||
ctx['running'] = (
|
||||
self.request.event.live
|
||||
and (self.request.event.has_subevents or self.request.event.presale_is_running)
|
||||
)
|
||||
|
||||
return ctx
|
||||
|
||||
def get_sales_estimate(self):
|
||||
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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/"
|
||||
@@ -302,15 +302,15 @@ msgstr "الكل"
|
||||
msgid "None"
|
||||
msgstr "لا شيء"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "استخدام اسم مختلف داخليا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "انقر لقريب"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -385,11 +385,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -281,15 +281,15 @@ msgstr ""
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -350,11 +350,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+0000\n"
|
||||
"PO-Revision-Date: 2020-12-14 10:00+0000\n"
|
||||
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -281,15 +281,15 @@ msgstr ""
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -352,11 +352,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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/"
|
||||
@@ -306,15 +306,15 @@ msgstr "Alle"
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Klik for at lukke"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du har ændringer, der ikke er gemt!"
|
||||
|
||||
@@ -385,11 +385,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr "Tidszone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Your local time:"
|
||||
msgstr "Din lokaltid:"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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/"
|
||||
@@ -301,15 +301,15 @@ msgstr "Alle"
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Sie haben ungespeicherte Änderungen!"
|
||||
|
||||
@@ -372,11 +372,11 @@ 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:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ chardet
|
||||
charge
|
||||
Checkout
|
||||
Chrome
|
||||
Choice
|
||||
CONFIRM
|
||||
Connect
|
||||
Cronjob
|
||||
@@ -77,7 +76,6 @@ Erstattungsmethode
|
||||
Erstattungsoptionen
|
||||
Erstattungsstatus
|
||||
Erstattungsweg
|
||||
erstmalig
|
||||
Erweiterungs
|
||||
etc
|
||||
Event
|
||||
@@ -101,8 +99,6 @@ Gutscheineinlöser
|
||||
herunterscrollen
|
||||
hochlädst
|
||||
HTTPS
|
||||
IBAN
|
||||
IBANs
|
||||
iCal
|
||||
ics
|
||||
ID
|
||||
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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/"
|
||||
@@ -300,15 +300,15 @@ msgstr "Alle"
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du hast ungespeicherte Änderungen!"
|
||||
|
||||
@@ -371,11 +371,11 @@ 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:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ chardet
|
||||
charge
|
||||
Checkout
|
||||
Chrome
|
||||
Choice
|
||||
CONFIRM
|
||||
Connect
|
||||
Cronjob
|
||||
@@ -77,7 +76,6 @@ Erstattungsmethode
|
||||
Erstattungsoptionen
|
||||
Erstattungsstatus
|
||||
Erstattungsweg
|
||||
erstmalig
|
||||
Erweiterungs
|
||||
etc
|
||||
Event
|
||||
@@ -101,8 +99,6 @@ Gutscheineinlöser
|
||||
herunterscrollen
|
||||
hochlädst
|
||||
HTTPS
|
||||
IBAN
|
||||
IBANs
|
||||
iCal
|
||||
ics
|
||||
ID
|
||||
@@ -190,9 +186,9 @@ Saalplan
|
||||
SAQ
|
||||
SCA
|
||||
Scan
|
||||
Scans
|
||||
Scanergebnis
|
||||
Scanning
|
||||
Scans
|
||||
schiefgeht
|
||||
sechsstelligen
|
||||
Secret
|
||||
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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"
|
||||
@@ -280,15 +280,15 @@ msgstr ""
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -349,11 +349,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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/"
|
||||
@@ -320,15 +320,15 @@ msgstr "Όλα"
|
||||
msgid "None"
|
||||
msgstr "Κανένας"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Κάντε κλικ για να κλείσετε"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -395,11 +395,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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-"
|
||||
@@ -315,15 +315,15 @@ msgstr "Todos"
|
||||
msgid "None"
|
||||
msgstr "Ninguno"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Usar un nombre diferente internamente"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Click para cerrar"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "¡Tienes cambios sin guardar!"
|
||||
|
||||
@@ -392,11 +392,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-27 17:45+0000\n"
|
||||
"PO-Revision-Date: 2021-01-20 16:10+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+0000\n"
|
||||
"PO-Revision-Date: 2020-10-17 20:00+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/fi/>\n"
|
||||
@@ -49,7 +49,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
|
||||
msgid "Total"
|
||||
msgstr "Summa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
|
||||
@@ -283,15 +283,15 @@ msgstr "Kaikki"
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Käytä toista nimeä sisäisesti"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Sulje klikkaamalla"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Sinulla on tallentamattomia muutoksia!"
|
||||
|
||||
@@ -352,11 +352,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr "Aikavyöhyke:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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/"
|
||||
@@ -313,15 +313,15 @@ msgstr "Tous"
|
||||
msgid "None"
|
||||
msgstr "Aucun"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Utiliser un nom différent en interne"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Cliquez pour fermer"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Vous avez des modifications non sauvegardées !"
|
||||
|
||||
@@ -388,11 +388,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
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: 2021-01-27 17:45+0000\n"
|
||||
"POT-Creation-Date: 2020-12-22 11:06+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-"
|
||||
@@ -308,15 +308,15 @@ msgstr "Összes"
|
||||
msgid "None"
|
||||
msgstr "Semmi"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:710
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:709
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Használj másik nevet"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:767
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:766
|
||||
msgid "Click to close"
|
||||
msgstr "Bezárásért kattints"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:782
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:781
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Mentetlen változtatások!"
|
||||
|
||||
@@ -383,11 +383,11 @@ msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:432
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:450
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:445
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:441
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:437
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user