Compare commits

..

3 Commits

Author SHA1 Message Date
Raphael Michel
fe5a2286a4 Update cache handling 2021-01-01 20:13:09 +01:00
Raphael Michel
3d66bfee7f Update checkout.py 2021-01-01 20:13:09 +01:00
Raphael Michel
0b495a4070 Validate email addresses fo valid DNS names 2021-01-01 20:13:09 +01:00
190 changed files with 58755 additions and 67200 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
__version__ = "3.15.0"
__version__ = "3.15.0.dev0"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
&middot; <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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>&nbsp;</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"

View File

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

View File

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

View File

@@ -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>&nbsp;</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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