mirror of
https://github.com/pretix/pretix.git
synced 2026-10-03 19:34:41 +00:00
Compare commits
98
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5a163e4ea | ||
|
|
ca03a2556d | ||
|
|
2381af4267 | ||
|
|
9fd570a53d | ||
|
|
072c17c91a | ||
|
|
003bfb707d | ||
|
|
faf8a6b1d2 | ||
|
|
a4d10d9550 | ||
|
|
baf3cf04ec | ||
|
|
fa2009ed03 | ||
|
|
cc082e2001 | ||
|
|
930e153db4 | ||
|
|
560a369e65 | ||
|
|
d47c435489 | ||
|
|
32e0e31b15 | ||
|
|
ea819530f9 | ||
|
|
09aef94376 | ||
|
|
39fbd25fbb | ||
|
|
214ea5fcd3 | ||
|
|
2de032c2be | ||
|
|
2574b31afd | ||
|
|
5f2264daeb | ||
|
|
4295b02406 | ||
|
|
49247062bc | ||
|
|
f3db461a2a | ||
|
|
83e607b88d | ||
|
|
9609722a98 | ||
|
|
4372aa6725 | ||
|
|
71488f0a02 | ||
|
|
52a4381277 | ||
|
|
bf066909a7 | ||
|
|
1399d37827 | ||
|
|
0c0842c9db | ||
|
|
6e647b41e4 | ||
|
|
7cff4321e0 | ||
|
|
fe5d095ae7 | ||
|
|
ebc31c901a | ||
|
|
2660dfe1bc | ||
|
|
08c376a459 | ||
|
|
6901ed8b26 | ||
|
|
62f1cbadaf | ||
|
|
1386b40e6f | ||
|
|
bbcd1b86b7 | ||
|
|
6894070239 | ||
|
|
7470a7198b | ||
|
|
ddda634ef3 | ||
|
|
3f7064f12b | ||
|
|
6386c99175 | ||
|
|
eb29101d09 | ||
|
|
d007fb9566 | ||
|
|
93cbbea193 | ||
|
|
eacce6d1f8 | ||
|
|
cdd7412fbb | ||
|
|
a6d462c14e | ||
|
|
d5d928cdce | ||
|
|
e0a8b81290 | ||
|
|
6b0419e488 | ||
|
|
48104e0c9f | ||
|
|
7eefb18b67 | ||
|
|
f9022e5a5e | ||
|
|
08c5963d7b | ||
|
|
e8274527cb | ||
|
|
f77aad0eeb | ||
|
|
d5b0059c34 | ||
|
|
47a170ab62 | ||
|
|
93aebffe2c | ||
|
|
7795c711be | ||
|
|
1eb685d4ee | ||
|
|
b8c523cfac | ||
|
|
fae9fff98e | ||
|
|
92cfbd54b9 | ||
|
|
18203e0804 | ||
|
|
52ed8eeb50 | ||
|
|
1e4888bad0 | ||
|
|
c3056a8aae | ||
|
|
4a4cff3c41 | ||
|
|
ffca102a8a | ||
|
|
78f8830f90 | ||
|
|
eb594266a7 | ||
|
|
76e6803eac | ||
|
|
e19908571c | ||
|
|
779ab360df | ||
|
|
723c63008d | ||
|
|
15c194c0a3 | ||
|
|
7f847c3dd2 | ||
|
|
430c6dd269 | ||
|
|
6411648457 | ||
|
|
005b3864b1 | ||
|
|
1ee1c604cf | ||
|
|
e425105dbf | ||
|
|
738b375042 | ||
|
|
d09572d999 | ||
|
|
634754aacb | ||
|
|
14c3baa2aa | ||
|
|
dc1b62fc56 | ||
|
|
ea2c81e6dc | ||
|
|
2bd5e90a86 | ||
|
|
8095134400 |
@@ -351,7 +351,8 @@ Endpoints
|
|||||||
|
|
||||||
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
|
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
|
||||||
``incomplete``, ``already_redeemed``, ``blocked``, ``invalid_time``, or ``error``. Required.
|
``incomplete``, ``already_redeemed``, ``blocked``, ``invalid_time``, or ``error``. Required.
|
||||||
:<json raw_barcode: The raw barcode you scanned. Required.
|
:<json raw_barcode: The raw barcode or identifier you scanned. Required.
|
||||||
|
:<json raw_source_type: The type of medium you scanned, defaults to ``barcode``. Optional.
|
||||||
:<json datetime: Date and time of the scan. Optional.
|
:<json datetime: Date and time of the scan. Optional.
|
||||||
:<json type: Type of scan, defaults to ``"entry"``.
|
:<json type: Type of scan, defaults to ``"entry"``.
|
||||||
:<json position: Internal ID of an order position you matched. Optional.
|
:<json position: Internal ID of an order position you matched. Optional.
|
||||||
|
|||||||
@@ -864,6 +864,9 @@ Generating new secrets
|
|||||||
|
|
||||||
Triggers generation of new ``secret`` and ``web_secret`` attributes for both the order and all order positions.
|
Triggers generation of new ``secret`` and ``web_secret`` attributes for both the order and all order positions.
|
||||||
|
|
||||||
|
Ticket secrets of order positions that have been used to issue a gift card can not
|
||||||
|
be changed. Only the link (``web_secret``) will be changed in this case.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -895,6 +898,9 @@ Generating new secrets
|
|||||||
|
|
||||||
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
||||||
|
|
||||||
|
Ticket secrets of order positions that have been used to issue a gift card can not
|
||||||
|
be changed. Only the link (``web_secret``) will be changed in this case.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Working with the code
|
|||||||
---------------------
|
---------------------
|
||||||
If you do not have a recent installation of ``nodejs``, install it now::
|
If you do not have a recent installation of ``nodejs``, install it now::
|
||||||
|
|
||||||
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
|
curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
||||||
sudo apt install nodejs
|
sudo apt install nodejs
|
||||||
|
|
||||||
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
||||||
|
|||||||
+5
-5
@@ -40,11 +40,11 @@ dependencies = [
|
|||||||
"Django[argon2]==5.2.*",
|
"Django[argon2]==5.2.*",
|
||||||
"django-bootstrap3==26.1",
|
"django-bootstrap3==26.1",
|
||||||
"django-compressor==4.6.0",
|
"django-compressor==4.6.0",
|
||||||
"django-countries==8.2.*",
|
"django-countries==9.0.*",
|
||||||
"django-filter==25.1",
|
"django-filter==26.1",
|
||||||
"django-formset-js-improved==0.5.0.5",
|
"django-formset-js-improved==0.5.0.5",
|
||||||
"django-formtools==2.6.1",
|
"django-formtools==2.7",
|
||||||
"django-hierarkey==2.0.*,>=2.0.1",
|
"django-hierarkey==2.0.*,>=2.0.2",
|
||||||
"django-hijack==3.7.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.11.*",
|
"django-i18nfield==1.11.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
@@ -94,7 +94,7 @@ dependencies = [
|
|||||||
"redis==7.4.*",
|
"redis==7.4.*",
|
||||||
"reportlab==5.0.*",
|
"reportlab==5.0.*",
|
||||||
"requests==2.34.*",
|
"requests==2.34.*",
|
||||||
"sentry-sdk==2.64.*",
|
"sentry-sdk==2.66.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
|
('POST', 'api-v1:checkinrpc.annull'),
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
('GET', 'api-v1:reusablemedium-list'),
|
('GET', 'api-v1:reusablemedium-list'),
|
||||||
('POST', 'api-v1:reusablemedium-lookup'),
|
('POST', 'api-v1:reusablemedium-lookup'),
|
||||||
@@ -146,6 +147,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
|
('POST', 'api-v1:checkinrpc.annull'),
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -182,6 +184,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
|
('POST', 'api-v1:checkinrpc.annull'),
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
('GET', 'api-v1:checkinrpc.search'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from django.db.models import prefetch_related_objects
|
from django.db.models import prefetch_related_objects
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@@ -135,3 +136,30 @@ class SalesChannelMigrationMixin:
|
|||||||
else:
|
else:
|
||||||
value["sales_channels"] = value["limit_sales_channels"]
|
value["sales_channels"] = value["limit_sales_channels"]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class CompatDecimalField(serializers.DecimalField):
|
||||||
|
"""
|
||||||
|
Historically, pretix recorded tax rates as decimals with two places. Today, pretix supports tax rates with up to
|
||||||
|
four places. Since our API outputs decimals with the stored precision, this would have changed the API output from
|
||||||
|
"19.00" to "19.0000" without warning. While this is semantically the same thing, we need to assume some pretix API
|
||||||
|
users might run into trouble, either because they treat the value as a string and then map something
|
||||||
|
(e.g. ``if tax_rate == "19.00"``) or process it with a language where this is a significant difference. For example,
|
||||||
|
while in Python ``Decimal("19.00") == Decimal("19.0000")`` is true, in Java
|
||||||
|
``(new BigDecimal("19.00")).equals(new BigDecimal("19.0000"))`` is false and only
|
||||||
|
``(new BigDecimal("19.00")).compareTo(new BigDecimal("19.0000")) == 0`` is true.
|
||||||
|
|
||||||
|
Therefore, we stay backwards compatible by outputting two decimal places *as long as the trailing digits are zero-valued.
|
||||||
|
"""
|
||||||
|
|
||||||
|
regex = re.compile(r"^([0-9]+\.[0-9]{2})0+$")
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if self.localize:
|
||||||
|
raise ValueError("localization not supported")
|
||||||
|
value = super().to_representation(value)
|
||||||
|
if value and "." not in value:
|
||||||
|
return f"{value}.00"
|
||||||
|
if m := self.regex.match(value):
|
||||||
|
return m.group(1)
|
||||||
|
return value
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ from rest_framework.fields import ChoiceField, Field
|
|||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
from pretix.api.serializers import (
|
from pretix.api.serializers import (
|
||||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
CompatDecimalField, CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.fields import PluginsField
|
from pretix.api.serializers.fields import PluginsField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
@@ -681,6 +681,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
)
|
)
|
||||||
|
rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ from django.utils.functional import cached_property, lazy
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
from pretix.api.serializers import (
|
||||||
|
CompatDecimalField, SalesChannelMigrationMixin,
|
||||||
|
)
|
||||||
from pretix.api.serializers.event import MetaDataField
|
from pretix.api.serializers.event import MetaDataField
|
||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
@@ -276,10 +278,10 @@ class ItemAddOnSerializer(serializers.ModelSerializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ItemTaxRateField(serializers.Field):
|
class ItemTaxRateField(CompatDecimalField):
|
||||||
def to_representation(self, i):
|
def to_representation(self, i):
|
||||||
if i.tax_rule:
|
if i.tax_rule:
|
||||||
return str(Decimal(i.tax_rule.rate))
|
return super().to_representation(Decimal(i.tax_rule.rate))
|
||||||
else:
|
else:
|
||||||
return str(Decimal('0.00'))
|
return str(Decimal('0.00'))
|
||||||
|
|
||||||
@@ -289,7 +291,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
||||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
tax_rate = ItemTaxRateField(source='*', read_only=True, max_digits=7, decimal_places=4)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
'image/png', 'image/jpeg', 'image/gif'
|
'image/png', 'image/jpeg', 'image/gif'
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import CompatDecimalField, CompatibleJSONField
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
@@ -52,6 +52,7 @@ from pretix.api.signals import order_api_details, orderposition_api_details
|
|||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.invoicing.transmission import get_transmission_types
|
from pretix.base.invoicing.transmission import get_transmission_types
|
||||||
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
||||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||||
@@ -381,6 +382,7 @@ class PrintLogSerializer(serializers.ModelSerializer):
|
|||||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||||
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
||||||
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
||||||
|
raw_source_type = serializers.ChoiceField(choices=[(k, v) for k, v in MEDIA_TYPES.items()], default='barcode')
|
||||||
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none(), required=False, allow_null=True)
|
position = serializers.PrimaryKeyRelatedField(queryset=OrderPosition.all.none(), required=False, allow_null=True)
|
||||||
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
raw_item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
|
||||||
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
raw_variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
|
||||||
@@ -390,7 +392,7 @@ class FailedCheckinSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Checkin
|
model = Checkin
|
||||||
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
|
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
|
||||||
'raw_subevent', 'nonce', 'datetime', 'type', 'position')
|
'raw_subevent', 'raw_source_type', 'nonce', 'datetime', 'type', 'position')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -591,6 +593,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
country = CompatibleCountryField(source='*')
|
country = CompatibleCountryField(source='*')
|
||||||
attendee_name = serializers.CharField(required=False)
|
attendee_name = serializers.CharField(required=False)
|
||||||
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
|
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
|
||||||
|
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
list_serializer_class = OrderPositionListSerializer
|
list_serializer_class = OrderPositionListSerializer
|
||||||
@@ -747,6 +750,8 @@ class OrderPaymentDateField(serializers.DateField):
|
|||||||
|
|
||||||
|
|
||||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||||
|
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderFee
|
model = OrderFee
|
||||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
|
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
|
||||||
@@ -1911,6 +1916,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
|||||||
position = LinePositionField(read_only=True)
|
position = LinePositionField(read_only=True)
|
||||||
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
||||||
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
||||||
|
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
@@ -1994,6 +2000,7 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class TransactionSerializer(I18nAwareModelSerializer):
|
class TransactionSerializer(I18nAwareModelSerializer):
|
||||||
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
||||||
|
tax_rate = CompatDecimalField(max_digits=7, decimal_places=4)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Transaction
|
model = Transaction
|
||||||
|
|||||||
@@ -1658,6 +1658,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
count_waitinglist=False,
|
count_waitinglist=False,
|
||||||
force=request.data.get('force', False),
|
force=request.data.get('force', False),
|
||||||
send_mail=send_mail,
|
send_mail=send_mail,
|
||||||
|
ignore_date=request.data.get('force', False),
|
||||||
)
|
)
|
||||||
except Quota.QuotaExceededException:
|
except Quota.QuotaExceededException:
|
||||||
pass
|
pass
|
||||||
@@ -1693,7 +1694,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
count_waitinglist=False,
|
count_waitinglist=False,
|
||||||
send_mail=send_mail,
|
send_mail=send_mail,
|
||||||
force=force)
|
force=force,
|
||||||
|
ignore_date=force)
|
||||||
except Quota.QuotaExceededException as e:
|
except Quota.QuotaExceededException as e:
|
||||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except PaymentException as e:
|
except PaymentException as e:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import sys
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
@@ -35,21 +36,23 @@ def get_powered_by(request, safelink=True):
|
|||||||
d = gs.settings.license_check_input
|
d = gs.settings.license_check_input
|
||||||
if d.get('poweredby_name'):
|
if d.get('poweredby_name'):
|
||||||
if d.get('poweredby_url'):
|
if d.get('poweredby_url'):
|
||||||
msg = gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>').format(
|
msg = format_html(
|
||||||
|
gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>'),
|
||||||
name=d['poweredby_name'],
|
name=d['poweredby_name'],
|
||||||
a_name_attr='href="{}" target="_blank" rel="noopener"'.format(
|
a_name_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
||||||
sl(d['poweredby_url']) if safelink else d['poweredby_url'],
|
escape(sl(d['poweredby_url'])) if safelink else escape(d['poweredby_url']),
|
||||||
),
|
)),
|
||||||
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
a_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
||||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||||
)
|
))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = gettext('<a {a_attr}>powered by {name} based on pretix</a>').format(
|
msg = format_html(
|
||||||
|
gettext('<a {a_attr}>powered by {name} based on pretix</a>'),
|
||||||
name=d['poweredby_name'],
|
name=d['poweredby_name'],
|
||||||
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
a_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
||||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||||
)
|
))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = gettext('<a %(a_attr)s>ticketing powered by pretix</a>') % {
|
msg = gettext('<a %(a_attr)s>ticketing powered by pretix</a>') % {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import ipaddress
|
|
||||||
import logging
|
import logging
|
||||||
import smtplib
|
import smtplib
|
||||||
import socket
|
import socket
|
||||||
@@ -43,6 +42,7 @@ from pretix.base.templatetags.rich_text import (
|
|||||||
markdown_compile_email, truelink_callback,
|
markdown_compile_email, truelink_callback,
|
||||||
)
|
)
|
||||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||||
|
from pretix.helpers.ssrf import should_block_access
|
||||||
|
|
||||||
from pretix.base.services.placeholders import ( # noqa
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
get_available_placeholders, PlaceholderContext
|
get_available_placeholders, PlaceholderContext
|
||||||
@@ -57,8 +57,6 @@ logger = logging.getLogger('pretix.base.email')
|
|||||||
|
|
||||||
T = TypeVar("T", bound=EmailBackend)
|
T = TypeVar("T", bound=EmailBackend)
|
||||||
|
|
||||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -254,16 +252,9 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
|
|||||||
af, socktype, proto, canonname, sa = res
|
af, socktype, proto, canonname, sa = res
|
||||||
|
|
||||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||||
ip_addr = ipaddress.ip_address(sa[0])
|
is_private, msg = should_block_access(sa)
|
||||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
if is_private:
|
||||||
if ip_addr.is_multicast:
|
raise socket.error(msg)
|
||||||
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
|
||||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
|
||||||
raise socket.error(f"Request to local address {sa[0]} blocked")
|
|
||||||
if ip_addr.is_private:
|
|
||||||
raise socket.error(f"Request to private address {sa[0]} blocked")
|
|
||||||
if check_ip4 in _cgnat_net:
|
|
||||||
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
|
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -33,8 +33,6 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import ipaddress
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@@ -42,13 +40,12 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.metrics import pretix_failed_logins
|
from pretix.base.metrics import pretix_failed_logins
|
||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
from pretix.helpers.dicts import move_to_end
|
from pretix.helpers.dicts import move_to_end
|
||||||
from pretix.helpers.http import get_client_ip
|
from pretix.helpers.ratelimit import rate_limit
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -85,40 +82,20 @@ class LoginForm(forms.Form):
|
|||||||
else:
|
else:
|
||||||
move_to_end(self.fields, 'keep_logged_in')
|
move_to_end(self.fields, 'keep_logged_in')
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def ratelimit_key(self):
|
|
||||||
if not settings.HAS_REDIS:
|
|
||||||
return None
|
|
||||||
client_ip = get_client_ip(self.request)
|
|
||||||
if not client_ip:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
client_ip = ipaddress.ip_address(client_ip)
|
|
||||||
except ValueError:
|
|
||||||
# Web server not set up correctly
|
|
||||||
return None
|
|
||||||
if client_ip.is_private:
|
|
||||||
# This is the private IP of the server, web server not set up correctly
|
|
||||||
return None
|
|
||||||
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||||
if self.ratelimit_key:
|
rate_limit_kwargs = dict(include_ip_from_request=self.request, max_num=10, expire_time=300)
|
||||||
from django_redis import get_redis_connection
|
if rate_limit("login", **rate_limit_kwargs, increase=False):
|
||||||
rc = get_redis_connection("redis")
|
# Check rate limit without counting up, we increase below only on failed logins
|
||||||
cnt = rc.get(self.ratelimit_key)
|
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||||
if cnt and int(cnt) > 10:
|
logger.info("Backend login rejected due to rate limit.")
|
||||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||||
logger.info("Backend login rejected due to rate limit.")
|
|
||||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
|
||||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||||
if self.user_cache is None:
|
if self.user_cache is None:
|
||||||
if self.ratelimit_key:
|
|
||||||
rc.incr(self.ratelimit_key)
|
|
||||||
rc.expire(self.ratelimit_key, 300)
|
|
||||||
logger.info("Backend login invalid.")
|
logger.info("Backend login invalid.")
|
||||||
pretix_failed_logins.inc(1, reason="invalid")
|
pretix_failed_logins.inc(1, reason="invalid")
|
||||||
|
# Count towards rate limit (result is ignored, we are checking above)
|
||||||
|
rate_limit("login", **rate_limit_kwargs)
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['invalid_login'],
|
self.error_messages['invalid_login'],
|
||||||
code='invalid_login'
|
code='invalid_login'
|
||||||
|
|||||||
@@ -959,7 +959,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
label=label, required=required,
|
label=label, required=required,
|
||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
initial=_initial,
|
initial=_initial,
|
||||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
widget=TimePickerWidget(without_seconds=True),
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_DATETIME:
|
elif q.type == Question.TYPE_DATETIME:
|
||||||
if not help_text:
|
if not help_text:
|
||||||
@@ -1116,6 +1116,13 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
if q.dependency_question_id and not question_is_visible(q.dependency_question_id, q.dependency_values) and answer is not None:
|
if q.dependency_question_id and not question_is_visible(q.dependency_question_id, q.dependency_values) and answer is not None:
|
||||||
d['question_%d' % q.pk] = None
|
d['question_%d' % q.pk] = None
|
||||||
|
|
||||||
|
# Strip False answers to required yes/no questions even if all_optional is set, as our data model assumes that
|
||||||
|
# required yes/no questions can only be answered with yes
|
||||||
|
for q in question_cache.values():
|
||||||
|
if q.required and q.type == Question.TYPE_BOOLEAN:
|
||||||
|
if 'question_%d' % q.pk in d and d['question_%d' % q.pk] is False:
|
||||||
|
del d['question_%d' % q.pk]
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
password_validators_help_texts, validate_password,
|
password_validators_help_texts, validate_password,
|
||||||
@@ -46,6 +45,7 @@ from pytz import common_timezones
|
|||||||
from pretix.base.models import User
|
from pretix.base.models import User
|
||||||
from pretix.control.forms import SingleLanguageWidget
|
from pretix.control.forms import SingleLanguageWidget
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import format_map
|
||||||
|
from pretix.helpers.ratelimit import rate_limit
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
@@ -128,16 +128,11 @@ class UserPasswordChangeForm(forms.Form):
|
|||||||
def clean_old_pw(self):
|
def clean_old_pw(self):
|
||||||
old_pw = self.cleaned_data.get('old_pw')
|
old_pw = self.cleaned_data.get('old_pw')
|
||||||
|
|
||||||
if settings.HAS_REDIS:
|
if rate_limit("pwchange", self.user.pk, max_num=10, expire_time=300):
|
||||||
from django_redis import get_redis_connection
|
raise forms.ValidationError(
|
||||||
rc = get_redis_connection("redis")
|
self.error_messages['rate_limit'],
|
||||||
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
|
code='rate_limit',
|
||||||
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
|
)
|
||||||
if cnt > 10:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
self.error_messages['rate_limit'],
|
|
||||||
code='rate_limit',
|
|
||||||
)
|
|
||||||
|
|
||||||
if not check_password(old_pw, self.user.password):
|
if not check_password(old_pw, self.user.password):
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
@@ -175,19 +170,35 @@ class UserEmailChangeForm(forms.Form):
|
|||||||
error_messages = {
|
error_messages = {
|
||||||
'duplicate_identifier': _("There already is an account associated with this email address. "
|
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||||
"Please choose a different one."),
|
"Please choose a different one."),
|
||||||
|
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||||
}
|
}
|
||||||
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
|
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
|
||||||
new_email = forms.EmailField(label=_('New email address'))
|
new_email = forms.EmailField(label=_('New email address'))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user')
|
self.user = kwargs.pop('user')
|
||||||
|
self.request = kwargs.pop('request')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean_new_email(self):
|
def clean_new_email(self):
|
||||||
email = self.cleaned_data['new_email']
|
email = self.cleaned_data['new_email']
|
||||||
|
|
||||||
|
if rate_limit("emailchange_attempt", include_ip_from_request=self.request, max_num=5, expire_time=300):
|
||||||
|
# Rate limit lookup for conflicting email addresses to make enumeration harder
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['rate_limit'],
|
||||||
|
code='rate_limit',
|
||||||
|
)
|
||||||
|
|
||||||
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
|
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
self.error_messages['duplicate_identifier'],
|
self.error_messages['duplicate_identifier'],
|
||||||
code='duplicate_identifier',
|
code='duplicate_identifier',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if rate_limit("emailchange", self.user.pk, max_num=2, expire_time=300):
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['rate_limit'],
|
||||||
|
code='rate_limit',
|
||||||
|
)
|
||||||
return email
|
return email
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ from django.utils.timezone import get_current_timezone, now
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.helpers.format import PlainHtmlAlternativeString
|
from pretix.helpers.format import PlainHtmlAlternativeString
|
||||||
|
from pretix.helpers.i18n import (
|
||||||
|
get_format_without_seconds, get_javascript_format,
|
||||||
|
get_javascript_format_without_seconds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def replace_arabic_numbers(inp):
|
def replace_arabic_numbers(inp):
|
||||||
@@ -108,7 +112,7 @@ class DatePickerWidget(forms.DateInput):
|
|||||||
|
|
||||||
|
|
||||||
class TimePickerWidget(forms.TimeInput):
|
class TimePickerWidget(forms.TimeInput):
|
||||||
def __init__(self, attrs=None, time_format=None):
|
def __init__(self, attrs=None, time_format=None, without_seconds=False):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
if 'placeholder' in attrs:
|
if 'placeholder' in attrs:
|
||||||
del attrs['placeholder']
|
del attrs['placeholder']
|
||||||
@@ -117,8 +121,27 @@ class TimePickerWidget(forms.TimeInput):
|
|||||||
time_attrs['class'] += ' timepickerfield'
|
time_attrs['class'] += ' timepickerfield'
|
||||||
time_attrs['autocomplete'] = 'off'
|
time_attrs['autocomplete'] = 'off'
|
||||||
|
|
||||||
|
if time_format or without_seconds:
|
||||||
|
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
|
||||||
|
def time_format_attr():
|
||||||
|
if without_seconds:
|
||||||
|
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
||||||
|
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
||||||
|
|
||||||
|
time_attrs['data-format'] = lazy(time_format_attr, str)
|
||||||
|
|
||||||
|
def time_format_attr():
|
||||||
|
if without_seconds:
|
||||||
|
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
||||||
|
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
||||||
|
|
||||||
|
time_attrs['data-format'] = lazy(time_format_attr, str)
|
||||||
|
|
||||||
def placeholder():
|
def placeholder():
|
||||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
if without_seconds:
|
||||||
|
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
|
||||||
|
else:
|
||||||
|
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||||
return now().replace(
|
return now().replace(
|
||||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||||
).strftime(tf)
|
).strftime(tf)
|
||||||
@@ -182,7 +205,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
|
|||||||
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||||
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
|
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
|
||||||
|
|
||||||
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None):
|
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None, without_seconds=False):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
if 'placeholder' in attrs:
|
if 'placeholder' in attrs:
|
||||||
del attrs['placeholder']
|
del attrs['placeholder']
|
||||||
@@ -205,14 +228,36 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
|||||||
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
|
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
|
||||||
).isoformat()
|
).isoformat()
|
||||||
|
|
||||||
|
if date_format or time_format or without_seconds:
|
||||||
|
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
|
||||||
|
def date_format_attr():
|
||||||
|
if without_seconds:
|
||||||
|
return get_javascript_format_without_seconds(date_format or "DATE_INPUT_FORMATS")
|
||||||
|
return get_javascript_format(date_format or "DATE_INPUT_FORMATS")
|
||||||
|
|
||||||
|
date_attrs['data-format'] = lazy(date_format_attr, str)
|
||||||
|
|
||||||
|
def time_format_attr():
|
||||||
|
if without_seconds:
|
||||||
|
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
||||||
|
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
||||||
|
|
||||||
|
time_attrs['data-format'] = lazy(time_format_attr, str)
|
||||||
|
|
||||||
def date_placeholder():
|
def date_placeholder():
|
||||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
if without_seconds:
|
||||||
|
df = date_format or get_format_without_seconds('DATE_INPUT_FORMATS')
|
||||||
|
else:
|
||||||
|
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||||
return now().replace(
|
return now().replace(
|
||||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||||
).strftime(df)
|
).strftime(df)
|
||||||
|
|
||||||
def time_placeholder():
|
def time_placeholder():
|
||||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
if without_seconds:
|
||||||
|
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
|
||||||
|
else:
|
||||||
|
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||||
return now().replace(
|
return now().replace(
|
||||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||||
).strftime(tf)
|
).strftime(tf)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from django.core.cache import cache
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||||
|
from django_querytagger.tagging import with_tag
|
||||||
|
|
||||||
from pretix.helpers.periodic import SKIPPED
|
from pretix.helpers.periodic import SKIPPED
|
||||||
|
|
||||||
@@ -82,7 +83,8 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
# Check if the DB connection is still good, it might be closed if the previous task took too long.
|
# Check if the DB connection is still good, it might be closed if the previous task took too long.
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
r = receiver(signal=periodic_task, sender=self)
|
with with_tag(f"periodictask={name}"):
|
||||||
|
r = receiver(signal=periodic_task, sender=self)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if isinstance(err, KeyboardInterrupt):
|
if isinstance(err, KeyboardInterrupt):
|
||||||
raise err
|
raise err
|
||||||
|
|||||||
+112
-15
@@ -19,6 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
@@ -69,11 +71,44 @@ def get_supported_language(requested_language, allowed_languages, default_langua
|
|||||||
return language
|
return language
|
||||||
|
|
||||||
|
|
||||||
class LocaleMiddleware(MiddlewareMixin):
|
class BaseLocaleMiddleware(MiddlewareMixin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This middleware sets the correct locale and timezone
|
This is a reduced LocaleMiddleware that uses only information contained in the WSGI request data
|
||||||
for a request.
|
to figure out the language (cookie and browser settings). We need it to have a consistent language
|
||||||
|
for error pages that are generated from the middleware stack before we know e.g. which user is logged
|
||||||
|
in or which event is selected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_request(self, request: HttpRequest):
|
||||||
|
language = get_language_from_early_request(request)
|
||||||
|
translation.activate(language)
|
||||||
|
set_region(None)
|
||||||
|
request.LANGUAGE_CODE = language
|
||||||
|
timezone.deactivate()
|
||||||
|
|
||||||
|
def process_response(self, request: HttpRequest, response: HttpResponse):
|
||||||
|
language = translation.get_language()
|
||||||
|
patch_vary_headers(response, ('Accept-Language',))
|
||||||
|
if 'Content-Language' not in response:
|
||||||
|
response['Content-Language'] = language
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class LocaleMiddleware(MiddlewareMixin):
|
||||||
|
"""
|
||||||
|
This is the full LocaleMiddleware that uses all available information to figure out the correct
|
||||||
|
language for the request using all available sources, in this order of priority:
|
||||||
|
|
||||||
|
- Backend: User settings
|
||||||
|
- Language cookie
|
||||||
|
- Frontend: Customer account settings
|
||||||
|
- Browser settings
|
||||||
|
- Frontend: Event/Organizer settings
|
||||||
|
- System default
|
||||||
|
|
||||||
|
It needs to run late in the middleware stack to have all information available for these steps.
|
||||||
|
For some cases, it is even ran a second time since the event is sometimes only figured out after the
|
||||||
|
middleware stack (can happen for plugin views).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_request(self, request: HttpRequest):
|
def process_request(self, request: HttpRequest):
|
||||||
@@ -188,6 +223,24 @@ def get_default_language():
|
|||||||
return settings.LANGUAGE_CODE
|
return settings.LANGUAGE_CODE
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_from_early_request(request: HttpRequest) -> str:
|
||||||
|
"""
|
||||||
|
Analyzes the request to find what language the user wants the system to
|
||||||
|
show using only WSGI-available information. Only languages listed in
|
||||||
|
settings.LANGUAGES are taken into account. If the user requests a sublanguage
|
||||||
|
where we have a main language, we send out the main language.
|
||||||
|
"""
|
||||||
|
global _supported
|
||||||
|
if _supported is None:
|
||||||
|
_supported = OrderedDict(settings.LANGUAGES)
|
||||||
|
|
||||||
|
return (
|
||||||
|
get_language_from_cookie(request)
|
||||||
|
or get_language_from_browser(request)
|
||||||
|
or get_default_language()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_language_from_request(request: HttpRequest) -> str:
|
def get_language_from_request(request: HttpRequest) -> str:
|
||||||
"""
|
"""
|
||||||
Analyzes the request to find what language the user wants the system to
|
Analyzes the request to find what language the user wants the system to
|
||||||
@@ -202,7 +255,6 @@ def get_language_from_request(request: HttpRequest) -> str:
|
|||||||
if request.path.startswith(get_script_prefix() + 'control'):
|
if request.path.startswith(get_script_prefix() + 'control'):
|
||||||
return (
|
return (
|
||||||
get_language_from_user_settings(request)
|
get_language_from_user_settings(request)
|
||||||
or get_language_from_customer_settings(request)
|
|
||||||
or get_language_from_cookie(request)
|
or get_language_from_cookie(request)
|
||||||
or get_language_from_browser(request)
|
or get_language_from_browser(request)
|
||||||
or get_language_from_event(request)
|
or get_language_from_event(request)
|
||||||
@@ -262,10 +314,50 @@ def _merge_csp(a, b):
|
|||||||
for k, v in a.items():
|
for k, v in a.items():
|
||||||
if "'unsafe-inline'" in v:
|
if "'unsafe-inline'" in v:
|
||||||
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
|
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
|
||||||
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha-")]
|
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha256-")]
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_response_csp(response, csp_to_merge):
|
||||||
|
if "Content-Security-Policy" in response:
|
||||||
|
csp = _parse_csp(response["Content-Security-Policy"])
|
||||||
|
else:
|
||||||
|
csp = {}
|
||||||
|
|
||||||
|
_merge_csp(csp, csp_to_merge)
|
||||||
|
|
||||||
|
if csp:
|
||||||
|
response["Content-Security-Policy"] = _render_csp(csp)
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_response_csp_via_request(request, csp_to_merge):
|
||||||
|
_merge_csp(request._csp_to_merge, csp_to_merge)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_csp_hash(data):
|
||||||
|
hash_str = base64.b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode("ascii")
|
||||||
|
return f"'sha256-{hash_str}'"
|
||||||
|
|
||||||
|
|
||||||
class SecurityMiddleware(MiddlewareMixin):
|
class SecurityMiddleware(MiddlewareMixin):
|
||||||
|
SAFE_TYPES = (
|
||||||
|
# CSP policies are only used for:
|
||||||
|
# - HTML and SVG in top-level contexts
|
||||||
|
# - SVG or JS Workers delivered in embedded contexts
|
||||||
|
# See: https://www.w3.org/TR/CSP2/#which-policy-applies
|
||||||
|
# Therefore, we can save bandwidth on not including our (sometimes huge) policy
|
||||||
|
# on API responses or CSS. We do however include it with other types as a precaution
|
||||||
|
# (whitelist instead of blacklist) and we also do not whitelist JavaScript in
|
||||||
|
# we ever add service workers to not break the protection of this feature:
|
||||||
|
# https://www.w3.org/TR/CSP2/#sandboxing-and-workers
|
||||||
|
'application/json',
|
||||||
|
'text/css',
|
||||||
|
# We used to skip CSP for PDF since it was necessary for inline previews in Safari,
|
||||||
|
# but at the moment it does not seem to be an issue to just send it.
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
request._csp_to_merge = {}
|
||||||
|
|
||||||
def process_response(self, request, resp):
|
def process_response(self, request, resp):
|
||||||
if settings.DEBUG and resp.status_code >= 400:
|
if settings.DEBUG and resp.status_code >= 400:
|
||||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||||
@@ -277,13 +369,22 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
# https://github.com/pretix/pretix/issues/765
|
# https://github.com/pretix/pretix/issues/765
|
||||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||||
|
|
||||||
if not getattr(resp, '_csp_ignore', False):
|
if self._needs_csp(request, resp):
|
||||||
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
|
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
|
||||||
elif 'Content-Security-Policy' in resp:
|
elif 'Content-Security-Policy' in resp:
|
||||||
del resp['Content-Security-Policy']
|
del resp['Content-Security-Policy']
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def _needs_csp(self, request, resp):
|
||||||
|
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if getattr(resp, '_csp_ignore', False):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _build_csp(self, request, resp):
|
def _build_csp(self, request, resp):
|
||||||
url = resolve(request.path_info)
|
url = resolve(request.path_info)
|
||||||
|
|
||||||
@@ -293,7 +394,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
'object-src': ["'none'"],
|
'object-src': ["'none'"],
|
||||||
'frame-src': ['{static}'],
|
'frame-src': ['{static}'],
|
||||||
'style-src': ["{static}", "{media}"],
|
'style-src': ["{static}", "{media}"],
|
||||||
'connect-src': ["{dynamic}", "{media}"],
|
'connect-src': ["{static}", "{dynamic}", "{media}"],
|
||||||
'img-src': ["{static}", "{media}", "data:"],
|
'img-src': ["{static}", "{media}", "data:"],
|
||||||
'font-src': ["{static}"],
|
'font-src': ["{static}"],
|
||||||
'media-src': ["{static}", "data:"],
|
'media-src': ["{static}", "data:"],
|
||||||
@@ -316,13 +417,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
h['style-src'] += ["'unsafe-inline'"]
|
h['style-src'] += ["'unsafe-inline'"]
|
||||||
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
||||||
|
|
||||||
if hasattr(request, 'csp_nonce'):
|
|
||||||
nonce = f"'nonce-{request.csp_nonce}'"
|
|
||||||
h['script-src'].append(nonce)
|
|
||||||
if not settings.VITE_DEV_MODE:
|
|
||||||
# can't have 'unsafe-inline' and nonce at the same time
|
|
||||||
h['style-src'].append(nonce)
|
|
||||||
|
|
||||||
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
||||||
if (
|
if (
|
||||||
url.url_name == "event.order.pay.change" or
|
url.url_name == "event.order.pay.change" or
|
||||||
@@ -335,6 +429,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
if settings.LOG_CSP:
|
if settings.LOG_CSP:
|
||||||
h['report-uri'] = ["/csp_report/"]
|
h['report-uri'] = ["/csp_report/"]
|
||||||
|
|
||||||
|
if request._csp_to_merge:
|
||||||
|
_merge_csp(h, request._csp_to_merge)
|
||||||
|
|
||||||
if 'Content-Security-Policy' in resp:
|
if 'Content-Security-Policy' in resp:
|
||||||
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-07-01 09:27
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_duplicate_secrets(apps, schema_editor):
|
||||||
|
# This will autofix all possible duplicate Order.code and OrderPosition.secret values,
|
||||||
|
# unless Order.code is already too long to append something. This would need to be fixed by
|
||||||
|
# sysadmins manually.
|
||||||
|
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
|
||||||
|
Order = apps.get_model("pretixbase", "Order")
|
||||||
|
|
||||||
|
qs = OrderPosition.all.values("secret", "order__event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
|
||||||
|
for row in qs:
|
||||||
|
affected = OrderPosition.all.filter(
|
||||||
|
**{k: v for k, v in row.items() if k != "c"}
|
||||||
|
).order_by("pk")
|
||||||
|
logger.error(f"Found {row['c']} tickets with with the same secret \"{row['secret']}\" in organizer {row['order__event__organizer_id']}, all except one will be changed")
|
||||||
|
for i, a in enumerate(affected):
|
||||||
|
if i > 0:
|
||||||
|
a.secret = a.secret + "__dupl__" + str(a.pk)
|
||||||
|
logger.info(
|
||||||
|
f"Ticket {a.pk} has new secret {a.secret}"
|
||||||
|
)
|
||||||
|
a.save(update_fields=["organizer_id", "secret"])
|
||||||
|
|
||||||
|
qs = Order.objects.values("code", "event__organizer_id").order_by().annotate(c=Count("*")).filter(c__gt=1)
|
||||||
|
for row in qs:
|
||||||
|
affected = Order.objects.filter(
|
||||||
|
**{k: v for k, v in row.items() if k != "c"}
|
||||||
|
).order_by("pk")
|
||||||
|
logger.error(f"Found {row['c']} orders with with the same code \"{row['code']}\" in organizer {row['event__organizer_id']}, all except one will be changed")
|
||||||
|
for i, a in enumerate(affected):
|
||||||
|
if i > 0:
|
||||||
|
if len(a.code) > 16 - len(str(a.pk)):
|
||||||
|
raise ValueError(f"Cannot auto-fix order with duplicate code {a.code}, order code is too long already")
|
||||||
|
a.code = a.code + str(a.pk).zfill(16 - len(a.code))
|
||||||
|
logger.info(
|
||||||
|
f"Order {a.pk} has new code {a.code}"
|
||||||
|
)
|
||||||
|
a.save(update_fields=["organizer_id", "code"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"pretixbase",
|
||||||
|
"0301_reusablemedium_remove_orderposition",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(clean_duplicate_secrets, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-07-01 09:27
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"pretixbase",
|
||||||
|
"0302_resolve_duplicate_codes_and_secrets",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunSQL(
|
||||||
|
"UPDATE pretixbase_order "
|
||||||
|
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_event e WHERE e.id = pretixbase_order.event_id) "
|
||||||
|
"WHERE pretixbase_order.organizer_id IS NULL;",
|
||||||
|
migrations.RunSQL.noop,
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
"UPDATE pretixbase_orderposition "
|
||||||
|
"SET organizer_id = (SELECT e.organizer_id FROM pretixbase_order o LEFT JOIN pretixbase_event e ON e.id = o.event_id WHERE o.id = pretixbase_orderposition.order_id) "
|
||||||
|
"WHERE pretixbase_orderposition.organizer_id IS NULL;",
|
||||||
|
migrations.RunSQL.noop,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="order",
|
||||||
|
name="organizer",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="orders",
|
||||||
|
to="pretixbase.organizer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="orderposition",
|
||||||
|
name="organizer",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="order_positions",
|
||||||
|
to="pretixbase.organizer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-04-15 20:10
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.helpers.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0303_alter_order_organizer_alter_orderposition_organizer'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cartposition',
|
||||||
|
name='tax_rate',
|
||||||
|
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invoiceline',
|
||||||
|
name='tax_rate',
|
||||||
|
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, default=Decimal('0'), max_digits=7),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='orderfee',
|
||||||
|
name='tax_rate',
|
||||||
|
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='orderposition',
|
||||||
|
name='tax_rate',
|
||||||
|
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='tax_rate',
|
||||||
|
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='taxrule',
|
||||||
|
name='rate',
|
||||||
|
field=pretix.helpers.models.NormalizedDecimalField(decimal_places=4, max_digits=7),
|
||||||
|
),
|
||||||
|
|
||||||
|
]
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-04-28 11:34
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db import IntegrityError, migrations, transaction
|
||||||
|
from django.db.models import Count, F
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_cross_organizer_eventmetavalues(apps, schema_editor):
|
||||||
|
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
|
||||||
|
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
|
||||||
|
|
||||||
|
cross_org_values = EventMetaValue.objects.filter(
|
||||||
|
event__organizer__pk__ne=F('property__organizer__pk')
|
||||||
|
).order_by('event__organizer__slug', 'event__slug')
|
||||||
|
for emv in cross_org_values:
|
||||||
|
logger.warning("%s", f"Fixing cross-organizer EventMetaValue: {emv.event.organizer.slug}/{emv.event.slug}")
|
||||||
|
logger.warning(" %s", f"{emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
||||||
|
try:
|
||||||
|
emv.property = emv.event.organizer.meta_properties.get(name=emv.property.name)
|
||||||
|
if EventMetaValue.objects.filter(event=emv.event, property=emv.property).exists():
|
||||||
|
correct = EventMetaValue.objects.get(event=emv.event, property=emv.property)
|
||||||
|
if correct.value != emv.value:
|
||||||
|
logger.warning(" %s", f"WARN: conflicting EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
|
||||||
|
else:
|
||||||
|
logger.warning(" %s", f"OK: same-value EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
|
||||||
|
logger.warning(" %s", f"keeping: {correct.property.name}({correct.property.id}@{correct.property.organizer.slug}) = {repr(correct.value)}")
|
||||||
|
emv.delete()
|
||||||
|
else:
|
||||||
|
logger.warning(" %s", f"OK: found existing EventMetaProperty in {emv.event.organizer.slug}, updating reference")
|
||||||
|
logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
||||||
|
emv.save(update_fields=["property"])
|
||||||
|
except EventMetaProperty.DoesNotExist:
|
||||||
|
meta_prop = emv.property
|
||||||
|
meta_prop.pk = None
|
||||||
|
meta_prop.organizer = emv.event.organizer
|
||||||
|
meta_prop.filter_public = False
|
||||||
|
meta_prop.save(force_insert=True)
|
||||||
|
logger.warning(" %s", f"WARN: found no matching EventMetaProperty, creating")
|
||||||
|
logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
||||||
|
emv.save(update_fields=["property"])
|
||||||
|
|
||||||
|
|
||||||
|
def make_eventmetaproperties_unique(apps, schema_editor):
|
||||||
|
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
|
||||||
|
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
|
||||||
|
|
||||||
|
duplicates = EventMetaProperty.objects.values('organizer', 'organizer__slug', 'name').annotate(count=Count('id')).filter(count__gt=1)
|
||||||
|
for dup in duplicates:
|
||||||
|
logger.warning("%s", f"Fixup duplicate property {dup['organizer__slug']} {dup['name']}")
|
||||||
|
props = list(EventMetaProperty.objects.filter(organizer=dup['organizer'], name=dup['name']))
|
||||||
|
|
||||||
|
target = props[0]
|
||||||
|
invalid = props[1:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
affected = EventMetaValue.objects.filter(
|
||||||
|
event__organizer=dup['organizer'], property__in=invalid
|
||||||
|
).update(
|
||||||
|
property=target
|
||||||
|
)
|
||||||
|
logger.warning("%s", f" Switching {affected} value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
||||||
|
|
||||||
|
except IntegrityError as e:
|
||||||
|
logger.warning("%s", f" Failed to switch all value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
||||||
|
logger.warning("%s", f" {e}")
|
||||||
|
for prop in invalid:
|
||||||
|
newname = f'{prop.name}_DUPLICATE_{prop.id}'
|
||||||
|
logger.warning("%s", f" Renaming {prop.name}({prop.id}@{prop.organizer.slug}) to {newname}({prop.id}@{prop.organizer.slug})")
|
||||||
|
prop.name = newname
|
||||||
|
prop.filter_public = False
|
||||||
|
prop.save()
|
||||||
|
|
||||||
|
else:
|
||||||
|
for prop in invalid:
|
||||||
|
logger.warning("%s", f" Deleting {prop.name}({prop.id}@{prop.organizer.slug})")
|
||||||
|
prop.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0304_tax_rate_decimals"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fix_cross_organizer_eventmetavalues, migrations.RunPython.noop),
|
||||||
|
migrations.RunPython(make_eventmetaproperties_unique, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-04-28 11:34
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0305_fixup_eventmetaproperties"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="eventmetaproperty",
|
||||||
|
unique_together={("organizer", "name")},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -179,6 +179,12 @@ class EventMixin:
|
|||||||
self.date_to.astimezone(tz), ("D" if short else "l")
|
self.date_to.astimezone(tz), ("D" if short else "l")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_same_day(self):
|
||||||
|
if not self.date_to:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self.date_from.astimezone(self.timezone).date() == self.date_to.astimezone(self.timezone).date()
|
||||||
|
|
||||||
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str:
|
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a formatted string containing the start date and the end date
|
Returns a formatted string containing the start date and the end date
|
||||||
@@ -232,6 +238,9 @@ class EventMixin:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self):
|
def timezone(self):
|
||||||
|
# If we get rid of the shim, verify that
|
||||||
|
# https://github.com/py-vobject/vobject/issues/117#issuecomment-5045645314
|
||||||
|
# has been released and included
|
||||||
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -649,7 +658,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
is_remote = models.BooleanField(
|
is_remote = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_("This event is remote or partially remote."),
|
verbose_name=_("This event is remote or partially remote."),
|
||||||
help_text=_("This will be used to let users know if the event is in a different timezone and let’s us calculate users’ local times."),
|
help_text=_("This will be used to let users know if the event is in a different timezone, and to let us calculate the local time of a user."),
|
||||||
)
|
)
|
||||||
geo_lat = models.FloatField(
|
geo_lat = models.FloatField(
|
||||||
verbose_name=_("Latitude"),
|
verbose_name=_("Latitude"),
|
||||||
@@ -1403,15 +1412,12 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
for mp in self.organizer.meta_properties.all():
|
for mp in self.organizer.meta_properties.all():
|
||||||
if mp.required and not self.meta_data.get(mp.name):
|
if mp.required and not self.meta_data.get(mp.name):
|
||||||
issues.append(
|
issues.append(format_html(
|
||||||
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
|
'<a href="{href}{href_hash}">{text}</a>',
|
||||||
property=mp.name,
|
text=gettext('You need to fill the meta parameter "{property}".').format(property=mp.name),
|
||||||
a_attr='href="%s#id_prop-%d-value"' % (
|
href=reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
||||||
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
|
href_hash=f'#id_prop-{mp.pk}-value',
|
||||||
mp.pk
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
responses = event_live_issues.send(self)
|
responses = event_live_issues.send(self)
|
||||||
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
|
||||||
@@ -1854,6 +1860,7 @@ class EventMetaProperty(LoggedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("position", "name",)
|
ordering = ("position", "name",)
|
||||||
|
unique_together = ('organizer', 'name')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def choice_keys(self):
|
def choice_keys(self):
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from django_scopes import ScopedManager
|
|||||||
|
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
from pretix.helpers.countries import FastCountryField
|
from pretix.helpers.countries import FastCountryField
|
||||||
|
from pretix.helpers.models import NormalizedDecimalField
|
||||||
|
|
||||||
|
|
||||||
def invoice_filename(instance, filename: str) -> str:
|
def invoice_filename(instance, filename: str) -> str:
|
||||||
@@ -450,7 +451,7 @@ class InvoiceLine(models.Model):
|
|||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
|
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
|
||||||
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
||||||
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
tax_rate = NormalizedDecimalField(max_digits=7, decimal_places=4, default=Decimal('0'))
|
||||||
tax_name = models.CharField(max_length=190)
|
tax_name = models.CharField(max_length=190)
|
||||||
tax_code = models.CharField(max_length=190, null=True, blank=True)
|
tax_code = models.CharField(max_length=190, null=True, blank=True)
|
||||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
|||||||
@@ -885,26 +885,6 @@ class Item(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
|
||||||
now_dt = now_dt or time_machine_now()
|
|
||||||
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
|
||||||
if not self.active:
|
|
||||||
return 'active'
|
|
||||||
elif self.available_from and self.available_from > now_dt:
|
|
||||||
return 'available_from'
|
|
||||||
elif self.available_until and self.available_until < now_dt:
|
|
||||||
return 'available_until'
|
|
||||||
elif (self.require_voucher or self.hide_without_voucher) and not has_voucher:
|
|
||||||
return 'require_voucher'
|
|
||||||
elif subevent_item and subevent_item.available_from and subevent_item.available_from > now_dt:
|
|
||||||
return 'available_from'
|
|
||||||
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
|
|
||||||
return 'available_until'
|
|
||||||
elif self.hidden_if_item_available and self._dependency_available:
|
|
||||||
return 'hidden_if_item_available'
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||||
check_quotas = set(getattr(
|
check_quotas = set(getattr(
|
||||||
self, '_subevent_quotas', # Utilize cache in product list
|
self, '_subevent_quotas', # Utilize cache in product list
|
||||||
@@ -1413,22 +1393,6 @@ class ItemVariation(models.Model):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
|
||||||
now_dt = now_dt or time_machine_now()
|
|
||||||
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
|
||||||
if not self.active:
|
|
||||||
return 'active'
|
|
||||||
elif self.available_from and self.available_from > now_dt:
|
|
||||||
return 'available_from'
|
|
||||||
elif self.available_until and self.available_until < now_dt:
|
|
||||||
return 'available_until'
|
|
||||||
elif subevent_var and subevent_var.available_from and subevent_var.available_from > now_dt:
|
|
||||||
return 'available_from'
|
|
||||||
elif subevent_var and subevent_var.available_until and subevent_var.available_until < now_dt:
|
|
||||||
return 'available_until'
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meta_data(self):
|
def meta_data(self):
|
||||||
data = self.item.meta_data
|
data = self.item.meta_data
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ from pretix.base.timemachine import time_machine_now
|
|||||||
|
|
||||||
from ...helpers import OF_SELF
|
from ...helpers import OF_SELF
|
||||||
from ...helpers.countries import CachedCountries, FastCountryField
|
from ...helpers.countries import CachedCountries, FastCountryField
|
||||||
|
from ...helpers.models import NormalizedDecimalField
|
||||||
from ...helpers.names import build_name
|
from ...helpers.names import build_name
|
||||||
from ...testutils.middleware import debugflags_var
|
from ...testutils.middleware import debugflags_var
|
||||||
from ._transactions import (
|
from ._transactions import (
|
||||||
@@ -224,8 +225,6 @@ class Order(LockModel, LoggedModel):
|
|||||||
"Organizer",
|
"Organizer",
|
||||||
related_name="orders",
|
related_name="orders",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
)
|
)
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
Event,
|
Event,
|
||||||
@@ -329,7 +328,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
default="line",
|
default="line",
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Order")
|
verbose_name = _("Order")
|
||||||
@@ -2287,9 +2286,12 @@ class OrderRefund(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
|
def ActivePositionManager(**scope):
|
||||||
def get_queryset(self):
|
class InnerClass(ScopedManager(**scope).__class__):
|
||||||
return super().get_queryset().filter(canceled=False)
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(canceled=False)
|
||||||
|
|
||||||
|
return InnerClass()
|
||||||
|
|
||||||
|
|
||||||
class OrderFee(RoundingCorrectionMixin, models.Model):
|
class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||||
@@ -2356,8 +2358,8 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
|||||||
)
|
)
|
||||||
description = models.CharField(max_length=190, blank=True)
|
description = models.CharField(max_length=190, blank=True)
|
||||||
internal_type = models.CharField(max_length=255, blank=True)
|
internal_type = models.CharField(max_length=255, blank=True)
|
||||||
tax_rate = models.DecimalField(
|
tax_rate = NormalizedDecimalField(
|
||||||
max_digits=7, decimal_places=2,
|
max_digits=7, decimal_places=4,
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_rule = models.ForeignKey(
|
tax_rule = models.ForeignKey(
|
||||||
@@ -2379,7 +2381,7 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
|||||||
canceled = models.BooleanField(default=False)
|
canceled = models.BooleanField(default=False)
|
||||||
|
|
||||||
all = ScopedManager(organizer='order__event__organizer')
|
all = ScopedManager(organizer='order__event__organizer')
|
||||||
objects = ActivePositionManager()
|
objects = ActivePositionManager(organizer='order__event__organizer')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def net_value(self):
|
def net_value(self):
|
||||||
@@ -2541,8 +2543,6 @@ class OrderPosition(AbstractPosition):
|
|||||||
"Organizer",
|
"Organizer",
|
||||||
related_name="order_positions",
|
related_name="order_positions",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
)
|
)
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
Order,
|
Order,
|
||||||
@@ -2555,8 +2555,8 @@ class OrderPosition(AbstractPosition):
|
|||||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
tax_rate = models.DecimalField(
|
tax_rate = NormalizedDecimalField(
|
||||||
max_digits=7, decimal_places=2,
|
max_digits=7, decimal_places=4,
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_rule = models.ForeignKey(
|
tax_rule = models.ForeignKey(
|
||||||
@@ -2599,8 +2599,8 @@ class OrderPosition(AbstractPosition):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
all = ScopedManager(organizer='order__event__organizer')
|
all = ScopedManager(organizer='organizer')
|
||||||
objects = ActivePositionManager()
|
objects = ActivePositionManager(organizer='organizer')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -3074,8 +3074,8 @@ class Transaction(models.Model):
|
|||||||
price_includes_rounding_correction = models.DecimalField(
|
price_includes_rounding_correction = models.DecimalField(
|
||||||
max_digits=13, decimal_places=2, default=Decimal("0.00")
|
max_digits=13, decimal_places=2, default=Decimal("0.00")
|
||||||
)
|
)
|
||||||
tax_rate = models.DecimalField(
|
tax_rate = NormalizedDecimalField(
|
||||||
max_digits=7, decimal_places=2,
|
max_digits=7, decimal_places=4,
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_rule = models.ForeignKey(
|
tax_rule = models.ForeignKey(
|
||||||
@@ -3190,8 +3190,8 @@ class CartPosition(AbstractPosition):
|
|||||||
verbose_name=_("Limit for extending expiration date"),
|
verbose_name=_("Limit for extending expiration date"),
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
tax_rate = models.DecimalField(
|
tax_rate = NormalizedDecimalField(
|
||||||
max_digits=7, decimal_places=2, default=Decimal('0.00'),
|
max_digits=7, decimal_places=4, default=Decimal('0'),
|
||||||
verbose_name=_('Tax rate')
|
verbose_name=_('Tax rate')
|
||||||
)
|
)
|
||||||
tax_code = models.CharField(
|
tax_code = models.CharField(
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from pretix.base.decimal import round_decimal
|
|||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.countries import FastCountryField
|
from pretix.helpers.countries import FastCountryField
|
||||||
|
from pretix.helpers.models import NormalizedDecimalField
|
||||||
|
|
||||||
|
|
||||||
class TaxedPrice:
|
class TaxedPrice:
|
||||||
@@ -335,9 +336,9 @@ class TaxRule(LoggedModel):
|
|||||||
max_length=190,
|
max_length=190,
|
||||||
choices=TAX_CODE_LISTS,
|
choices=TAX_CODE_LISTS,
|
||||||
)
|
)
|
||||||
rate = models.DecimalField(
|
rate = NormalizedDecimalField(
|
||||||
max_digits=10,
|
max_digits=7,
|
||||||
decimal_places=2,
|
decimal_places=4,
|
||||||
validators=[
|
validators=[
|
||||||
MaxValueValidator(
|
MaxValueValidator(
|
||||||
limit_value=Decimal("100.00"),
|
limit_value=Decimal("100.00"),
|
||||||
|
|||||||
@@ -32,8 +32,10 @@
|
|||||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
import datetime
|
||||||
|
from dataclasses import dataclass
|
||||||
from decimal import ROUND_HALF_UP, Decimal
|
from decimal import ROUND_HALF_UP, Decimal
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -421,27 +423,33 @@ class Voucher(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_quota_get_ignored(old_instance):
|
def get_affected_quotas(quota, item, variation, subevent):
|
||||||
quotas = set()
|
if quota:
|
||||||
was_valid = old_instance and (
|
return {quota}
|
||||||
old_instance.valid_until is None or old_instance.valid_until >= now()
|
elif item and variation:
|
||||||
)
|
return set(variation.quotas.filter(subevent=subevent))
|
||||||
if old_instance and old_instance.block_quota and was_valid:
|
elif item and not item.has_variations:
|
||||||
if old_instance.quota:
|
return set(item.quotas.filter(subevent=subevent))
|
||||||
quotas.add(old_instance.quota)
|
elif item and item.has_variations:
|
||||||
elif old_instance.variation:
|
return set(
|
||||||
quotas |= set(old_instance.variation.quotas.filter(subevent=old_instance.subevent))
|
Quota.objects.filter(
|
||||||
elif old_instance.item:
|
pk__in=Quota.variations.through.objects.filter(
|
||||||
if old_instance.item.has_variations:
|
itemvariation__item=item,
|
||||||
quotas |= set(
|
quota__subevent=subevent,
|
||||||
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
|
).values('quota_id')
|
||||||
itemvariation__item=old_instance.item,
|
)
|
||||||
quota__subevent=old_instance.subevent,
|
)
|
||||||
).values('quota_id'))
|
else:
|
||||||
)
|
return set()
|
||||||
else:
|
|
||||||
quotas |= set(old_instance.item.quotas.filter(subevent=old_instance.subevent))
|
@staticmethod
|
||||||
return quotas
|
def clean_quota_get_ignored(voucher_data: Union["VoucherBulkData", "Voucher"]):
|
||||||
|
if voucher_data:
|
||||||
|
valid = voucher_data.valid_until is None or voucher_data.valid_until >= now()
|
||||||
|
if valid and voucher_data.block_quota and voucher_data.max_usages > voucher_data.redeemed:
|
||||||
|
return Voucher.get_affected_quotas(voucher_data.quota, voucher_data.item, voucher_data.variation, voucher_data.subevent)
|
||||||
|
|
||||||
|
return set()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
||||||
@@ -453,22 +461,8 @@ class Voucher(LoggedModel):
|
|||||||
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
||||||
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||||
|
|
||||||
if quota:
|
new_quotas = Voucher.get_affected_quotas(quota, item, variation, data.get('subevent'))
|
||||||
new_quotas = {quota}
|
if not new_quotas:
|
||||||
elif item and variation:
|
|
||||||
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
|
||||||
elif item and not item.has_variations:
|
|
||||||
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
|
||||||
elif item and item.has_variations:
|
|
||||||
new_quotas = set(
|
|
||||||
Quota.objects.filter(
|
|
||||||
pk__in=Quota.variations.through.objects.filter(
|
|
||||||
itemvariation__item=item,
|
|
||||||
quota__subevent=data.get('subevent'),
|
|
||||||
).values('quota_id')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||||
'tickets.'))
|
'tickets.'))
|
||||||
|
|
||||||
@@ -644,3 +638,16 @@ class Voucher(LoggedModel):
|
|||||||
]
|
]
|
||||||
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
||||||
return ops
|
return ops
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VoucherBulkData:
|
||||||
|
item: object
|
||||||
|
variation: object
|
||||||
|
quota: object
|
||||||
|
block_quota: bool
|
||||||
|
valid_until: datetime.datetime
|
||||||
|
subevent: object
|
||||||
|
redeemed: int
|
||||||
|
max_usages: int
|
||||||
|
allow_ignore_quota: bool
|
||||||
|
|||||||
@@ -936,7 +936,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
Will be called if the *event administrator* views the details of a payment.
|
Will be called if the *event administrator* views the details of a payment.
|
||||||
|
|
||||||
It should return HTML code containing information regarding the current payment
|
It should return a SafeString containing HTML code, with information regarding the current payment
|
||||||
status and, if applicable, next steps.
|
status and, if applicable, next steps.
|
||||||
|
|
||||||
The default implementation returns an empty string.
|
The default implementation returns an empty string.
|
||||||
@@ -961,7 +961,7 @@ class BasePaymentProvider:
|
|||||||
"""
|
"""
|
||||||
Will be called if the *event administrator* views the details of a refund.
|
Will be called if the *event administrator* views the details of a refund.
|
||||||
|
|
||||||
It should return HTML code containing information regarding the current refund
|
It should return a SafeString containing HTML code, with information regarding the current refund
|
||||||
status and, if applicable, next steps.
|
status and, if applicable, next steps.
|
||||||
|
|
||||||
The default implementation returns an empty string.
|
The default implementation returns an empty string.
|
||||||
@@ -1706,6 +1706,58 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHistoricalPaymentProvider(BasePaymentProvider):
|
||||||
|
"""
|
||||||
|
Base class for payment providers that no longer exist but can't be deleted to make sure historical
|
||||||
|
payments are shown correctly.
|
||||||
|
|
||||||
|
Subclasses are recommended to only implement:
|
||||||
|
- identifier
|
||||||
|
- verbose_name
|
||||||
|
- public_name
|
||||||
|
- payment_control_render
|
||||||
|
- payment_control_render_short
|
||||||
|
- refund_control_render
|
||||||
|
- refund_control_render_short
|
||||||
|
- render_invoice_text
|
||||||
|
- render_invoice_stamp
|
||||||
|
- api_payment_details
|
||||||
|
- api_refund_details
|
||||||
|
- shred_payment_info
|
||||||
|
- matching_id
|
||||||
|
- refund_matching_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def settings_form_fields(self) -> dict:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def payment_is_valid_session(self, request: HttpRequest, payment: OrderPayment):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||||
|
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
|
||||||
|
|
||||||
|
def execute_refund(self, refund: OrderRefund):
|
||||||
|
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
@receiver(register_payment_providers, dispatch_uid="payment_free")
|
||||||
def register_payment_provider(sender, **kwargs):
|
def register_payment_provider(sender, **kwargs):
|
||||||
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
|
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
|
||||||
|
|||||||
@@ -245,6 +245,9 @@ def recv_classic(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
|
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
|
||||||
|
if position.pk and position.issued_gift_cards.exists():
|
||||||
|
return
|
||||||
|
|
||||||
gen = event.ticket_secret_generator
|
gen = event.ticket_secret_generator
|
||||||
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
|
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
|
||||||
force_invalidate = True
|
force_invalidate = True
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from typing import List
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
|
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
|
||||||
from pretix.presale.views.event import get_grouped_items
|
from pretix.presale.productlist import prepare_item_list_for_shop
|
||||||
|
|
||||||
|
|
||||||
class DummyCategory:
|
class DummyCategory:
|
||||||
@@ -162,7 +162,7 @@ class CrossSellingService:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def _prepare_items(self, subevent, items_qs, discount_info):
|
def _prepare_items(self, subevent, items_qs, discount_info):
|
||||||
items, _btn = get_grouped_items(
|
items, _btn = prepare_item_list_for_shop(
|
||||||
self.event,
|
self.event,
|
||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
voucher=None,
|
voucher=None,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import transaction
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -96,6 +97,8 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
|||||||
:param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
|
:param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
|
||||||
where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
|
where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
|
||||||
"""
|
"""
|
||||||
|
if lock and not transaction.get_connection().in_atomic_block:
|
||||||
|
raise Exception('validate_memberships_in_order(lock=True) should only be called in atomic transaction!')
|
||||||
tz = event.timezone
|
tz = event.timezone
|
||||||
applicable_positions = [
|
applicable_positions = [
|
||||||
p for p in positions
|
p for p in positions
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ from pretix.celery_app import app
|
|||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
from pretix.helpers.periodic import minimum_interval
|
from pretix.helpers.periodic import minimum_interval
|
||||||
|
from pretix.presale.productlist import prepare_item_list_for_shop
|
||||||
from pretix.testutils.middleware import debugflags_var
|
from pretix.testutils.middleware import debugflags_var
|
||||||
|
|
||||||
|
|
||||||
@@ -790,6 +791,12 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
[op.seat for op in sorted_positions if op.seat],
|
[op.seat for op in sorted_positions if op.seat],
|
||||||
shared_lock_objects=[event]
|
shared_lock_objects=[event]
|
||||||
)
|
)
|
||||||
|
elif any(cp.voucher and cp.voucher.budget for cp in sorted_positions):
|
||||||
|
# Voucher budgets are not guaranteed by the cart manager
|
||||||
|
lock_objects(
|
||||||
|
[op.voucher for op in sorted_positions if op.voucher and op.voucher.budget],
|
||||||
|
shared_lock_objects=[event]
|
||||||
|
)
|
||||||
|
|
||||||
q_avail = Counter()
|
q_avail = Counter()
|
||||||
v_avail = Counter()
|
v_avail = Counter()
|
||||||
@@ -1599,6 +1606,7 @@ class OrderChangeManager:
|
|||||||
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
||||||
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
||||||
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
||||||
|
'gift_card_secret': gettext_lazy('You cannot change the ticket secret of a position that has been used to issue a gift card.'),
|
||||||
'max_items_per_product': ngettext_lazy(
|
'max_items_per_product': ngettext_lazy(
|
||||||
"You cannot select more than %(max)s item of the product %(product)s.",
|
"You cannot select more than %(max)s item of the product %(product)s.",
|
||||||
"You cannot select more than %(max)s items of the product %(product)s.",
|
"You cannot select more than %(max)s items of the product %(product)s.",
|
||||||
@@ -1756,6 +1764,9 @@ class OrderChangeManager:
|
|||||||
self._operations.append(self.RegenerateSecretOperation(position))
|
self._operations.append(self.RegenerateSecretOperation(position))
|
||||||
|
|
||||||
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
|
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
|
||||||
|
if position.issued_gift_cards.exists():
|
||||||
|
raise OrderError(self.error_messages['gift_card_secret'])
|
||||||
|
|
||||||
self._operations.append(self.ChangeSecretOperation(position, new_secret))
|
self._operations.append(self.ChangeSecretOperation(position, new_secret))
|
||||||
|
|
||||||
def change_valid_from(self, position: OrderPosition, new_value: datetime):
|
def change_valid_from(self, position: OrderPosition, new_value: datetime):
|
||||||
@@ -1943,13 +1954,18 @@ class OrderChangeManager:
|
|||||||
|
|
||||||
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
|
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
|
||||||
``"count"``, and ``"price"``.
|
``"count"``, and ``"price"``.
|
||||||
:param limit_main_positions: By default, the method works on all methods of the order. If you set this to a
|
:param limit_main_positions: By default, the method works on all positions of the order. If you set this to a
|
||||||
queryset or a list of positions, all other positions and their add-ons will be kept
|
queryset or a list of positions, all other positions and their add-ons will be kept
|
||||||
untouched.
|
untouched.
|
||||||
"""
|
"""
|
||||||
if self._operations:
|
if self._operations:
|
||||||
raise ValueError("Setting addons should be the first/only operation")
|
raise ValueError("Setting addons should be the first/only operation")
|
||||||
|
|
||||||
|
def _allowed_on_order_sales_channel(item_or_var, order):
|
||||||
|
return item_or_var.all_sales_channels or (
|
||||||
|
order.sales_channel.identifier in (s.identifier for s in item_or_var.limit_sales_channels.all())
|
||||||
|
)
|
||||||
|
|
||||||
# Prepare containers for min/max check of products
|
# Prepare containers for min/max check of products
|
||||||
item_counts = Counter()
|
item_counts = Counter()
|
||||||
for p in self.order.positions.all():
|
for p in self.order.positions.all():
|
||||||
@@ -2043,13 +2059,11 @@ class OrderChangeManager:
|
|||||||
if not item.is_available() or (variation and not variation.is_available()):
|
if not item.is_available() or (variation and not variation.is_available()):
|
||||||
raise OrderError(error_messages['unavailable'])
|
raise OrderError(error_messages['unavailable'])
|
||||||
|
|
||||||
if not item.all_sales_channels:
|
if not _allowed_on_order_sales_channel(item, self.order):
|
||||||
if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
|
raise OrderError(error_messages['unavailable'])
|
||||||
raise OrderError(error_messages['unavailable'])
|
|
||||||
|
|
||||||
if variation and not variation.all_sales_channels:
|
if variation and not _allowed_on_order_sales_channel(variation, self.order):
|
||||||
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
|
raise OrderError(error_messages['unavailable'])
|
||||||
raise OrderError(error_messages['unavailable'])
|
|
||||||
|
|
||||||
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
|
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
|
||||||
raise OrderError(error_messages['not_for_sale'])
|
raise OrderError(error_messages['not_for_sale'])
|
||||||
@@ -2097,6 +2111,36 @@ class OrderChangeManager:
|
|||||||
)
|
)
|
||||||
item_counts[item] += 1
|
item_counts[item] += 1
|
||||||
|
|
||||||
|
def _addon_is_available(a):
|
||||||
|
# If an item is no longer available due to time, it should usually also be no longer
|
||||||
|
# user-removable, because e.g. the stock has already been ordered.
|
||||||
|
# We always set voucher=None because that's what's done when generating the form in
|
||||||
|
# OrderChangeMixin (vouchers for addons are not supported).
|
||||||
|
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||||
|
# be part of the input.
|
||||||
|
if not _allowed_on_order_sales_channel(a.item, self.order) or (
|
||||||
|
a.variation and not _allowed_on_order_sales_channel(a.variation, self.order)
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
items, _ = prepare_item_list_for_shop(
|
||||||
|
self.order.event,
|
||||||
|
channel=self.order.sales_channel,
|
||||||
|
subevent=a.subevent,
|
||||||
|
voucher=None,
|
||||||
|
base_qs=Item.objects.filter(pk=a.item.pk),
|
||||||
|
allow_addons=True
|
||||||
|
)
|
||||||
|
if (not items) or items[0].current_unavailability_reason:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if a.variation:
|
||||||
|
variations = [var for var in items[0].available_variations if var.pk == a.variation.pk]
|
||||||
|
if (not variations) or variations[0].current_unavailability_reason:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
# Detect removed add-ons and create RemoveOperations
|
# Detect removed add-ons and create RemoveOperations
|
||||||
for cp, al in list(current_addons.items()):
|
for cp, al in list(current_addons.items()):
|
||||||
for k, v in al.items():
|
for k, v in al.items():
|
||||||
@@ -2106,22 +2150,7 @@ class OrderChangeManager:
|
|||||||
for a in current_addons[cp][k][:current_num - input_num]:
|
for a in current_addons[cp][k][:current_num - input_num]:
|
||||||
if a.canceled:
|
if a.canceled:
|
||||||
continue
|
continue
|
||||||
is_unavailable = (
|
if not _addon_is_available(a):
|
||||||
# If an item is no longer available due to time, it should usually also be no longer
|
|
||||||
# user-removable, because e.g. the stock has already been ordered.
|
|
||||||
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
|
|
||||||
# not mean it should be unremovable for others.
|
|
||||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
|
||||||
# be part of the input.
|
|
||||||
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
|
||||||
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
|
|
||||||
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
|
||||||
or (
|
|
||||||
not a.item.all_sales_channels and
|
|
||||||
not a.item.limit_sales_channels.contains(self.order.sales_channel)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if is_unavailable:
|
|
||||||
# "Re-select" add-on
|
# "Re-select" add-on
|
||||||
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
|
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -535,8 +535,9 @@ EventPluginRegistry = PluginAwareRegistry # for backwards compatibility
|
|||||||
event_live_issues = EventPluginSignal()
|
event_live_issues = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to determine whether an event can be taken live. If you want to
|
This signal is sent out to determine whether an event can be taken live. If you want to
|
||||||
prevent the event from going live, return a string that will be displayed to the user
|
prevent the event from going live, return an error message to display to the user (either
|
||||||
as the error message. If you don't, your receiver should return ``None``.
|
as a SafeString containing HTML, or a string that will be HTML-escaped). If you don't,
|
||||||
|
your receiver should return ``None``.
|
||||||
|
|
||||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
@@ -44,7 +45,7 @@ def eventsignal(event: Event, signame: str, **kwargs):
|
|||||||
_html = []
|
_html = []
|
||||||
for receiver, response in signal.send(event, **kwargs):
|
for receiver, response in signal.send(event, **kwargs):
|
||||||
if response:
|
if response:
|
||||||
_html.append(response)
|
_html.append(conditional_escape(response))
|
||||||
return mark_safe("".join(_html))
|
return mark_safe("".join(_html))
|
||||||
|
|
||||||
|
|
||||||
@@ -63,5 +64,5 @@ def signal(signame: str, request, **kwargs):
|
|||||||
_html = []
|
_html = []
|
||||||
for receiver, response in signal.send(request, **kwargs):
|
for receiver, response in signal.send(request, **kwargs):
|
||||||
if response:
|
if response:
|
||||||
_html.append(response)
|
_html.append(conditional_escape(response))
|
||||||
return mark_safe("".join(_html))
|
return mark_safe("".join(_html))
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ from babel.numbers import format_currency
|
|||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.defaultfilters import floatformat
|
from django.template.defaultfilters import floatformat
|
||||||
|
from django.utils import formats
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from pretix.base.i18n import get_babel_locale
|
from pretix.base.i18n import get_babel_locale
|
||||||
|
|
||||||
@@ -82,3 +84,19 @@ def money_numberfield_filter(value: Decimal, arg=''):
|
|||||||
|
|
||||||
places = settings.CURRENCY_PLACES.get(arg, 2)
|
places = settings.CURRENCY_PLACES.get(arg, 2)
|
||||||
return str(value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP))
|
return str(value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP))
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(is_safe=True)
|
||||||
|
def tax_rate_format(number):
|
||||||
|
"""
|
||||||
|
Display a Decimal to its significant decimal places, used for tax rates.
|
||||||
|
"""
|
||||||
|
assert isinstance(number, Decimal)
|
||||||
|
return mark_safe(
|
||||||
|
formats.number_format(
|
||||||
|
number.normalize(),
|
||||||
|
-number.as_tuple().exponent,
|
||||||
|
use_l10n=True,
|
||||||
|
force_grouping=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,12 +19,10 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import secrets
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
@@ -33,6 +31,10 @@ from django import template
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from pretix.base.middleware import (
|
||||||
|
add_to_response_csp_via_request, calculate_csp_hash,
|
||||||
|
)
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
_MANIFEST = {}
|
_MANIFEST = {}
|
||||||
@@ -234,10 +236,16 @@ def vite_importmap(context):
|
|||||||
if not imports:
|
if not imports:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Generate a nonce and store it on the request so the CSP middleware can allow it
|
json_string = json.dumps({"imports": imports})
|
||||||
nonce = secrets.token_urlsafe(16)
|
|
||||||
|
# Calculate hash and add it to the CSP info in the request, so that will be merged into the
|
||||||
|
# response header later on by the middleware
|
||||||
request = context.get('request')
|
request = context.get('request')
|
||||||
if request:
|
if request:
|
||||||
request.csp_nonce = nonce
|
csp_hash = calculate_csp_hash(json_string)
|
||||||
|
add_to_response_csp_via_request(request, {
|
||||||
|
'style-src': [csp_hash],
|
||||||
|
'script-src': [csp_hash],
|
||||||
|
})
|
||||||
|
|
||||||
return f'<script type="importmap" nonce="{nonce}">{json.dumps({"imports": imports})}</script>'
|
return f'<script type="importmap">{json_string}</script>'
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ from django.template import TemplateDoesNotExist, loader
|
|||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.decorators.csrf import requires_csrf_token
|
|
||||||
from sentry_sdk import last_event_id
|
from sentry_sdk import last_event_id
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.middleware import get_language_from_request
|
from pretix.base.middleware import get_language_from_request
|
||||||
|
from pretix.multidomain.middlewares import requires_csrf_token
|
||||||
|
|
||||||
|
|
||||||
def csrf_failure(request, reason=""):
|
def csrf_failure(request, reason=""):
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def on_task_prerun(sender, task_id, task, **kwargs):
|
|||||||
from pretix.helpers.logs import local
|
from pretix.helpers.logs import local
|
||||||
|
|
||||||
local.request_id = task_id
|
local.request_id = task_id
|
||||||
if "X-Pretix-Trace" in task.request.headers:
|
if task.request.headers and "X-Pretix-Trace" in task.request.headers:
|
||||||
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
|
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
|
||||||
else:
|
else:
|
||||||
local.trace = []
|
local.trace = []
|
||||||
|
|||||||
@@ -197,10 +197,10 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
'presale_end': SplitDateTimeField,
|
'presale_end': SplitDateTimeField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'date_from': SplitDateTimePickerWidget(),
|
'date_from': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}),
|
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}, without_seconds=True),
|
||||||
'presale_start': SplitDateTimePickerWidget(),
|
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}),
|
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}, without_seconds=True),
|
||||||
'slug': SlugWidget,
|
'slug': SlugWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,11 +521,11 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
'limit_sales_channels': SafeModelMultipleChoiceField,
|
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'date_from': SplitDateTimePickerWidget(),
|
'date_from': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
|
||||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
|
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}, without_seconds=True),
|
||||||
'presale_start': SplitDateTimePickerWidget(),
|
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1905,12 +1905,6 @@ class QuickSetupForm(I18nForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_("We'll show this publicly to allow attendees to contact you.")
|
help_text=_("We'll show this publicly to allow attendees to contact you.")
|
||||||
)
|
)
|
||||||
contact_url = forms.URLField(
|
|
||||||
label=_("Contact URL"),
|
|
||||||
required=False,
|
|
||||||
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
|
|
||||||
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
|
|
||||||
)
|
|
||||||
total_quota = forms.IntegerField(
|
total_quota = forms.IntegerField(
|
||||||
label=_("Total capacity"),
|
label=_("Total capacity"),
|
||||||
min_value=0,
|
min_value=0,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from urllib.parse import urlencode
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, Max, Model, OrderBy, OuterRef, Q, QuerySet,
|
Count, Exists, F, Max, Model, OrderBy, OuterRef, Q, QuerySet,
|
||||||
)
|
)
|
||||||
@@ -59,7 +60,7 @@ from pretix.base.models import (
|
|||||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||||
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
|
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
|
||||||
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
|
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
|
||||||
Voucher,
|
User, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
from pretix.base.timeframes import (
|
from pretix.base.timeframes import (
|
||||||
@@ -770,7 +771,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_TIME:
|
elif q.type == Question.TYPE_TIME:
|
||||||
self.fields[fname] = forms.TimeField(
|
self.fields[fname] = forms.TimeField(
|
||||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
widget=TimePickerWidget(without_seconds=True),
|
||||||
help_text=_('Exact matches only'),
|
help_text=_('Exact matches only'),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
@@ -3007,3 +3008,91 @@ class OutgoingMailFilterForm(FilterForm):
|
|||||||
qs = qs.order_by("-created", "-pk")
|
qs = qs.order_by("-created", "-pk")
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class LogFilterForm(FilterForm):
|
||||||
|
source = forms.ChoiceField(
|
||||||
|
label=_('Source'),
|
||||||
|
choices=[
|
||||||
|
('', _('All sources')),
|
||||||
|
('team', _('Team actions')),
|
||||||
|
('customer', _('Customer actions')),
|
||||||
|
('device', _('Device actions')),
|
||||||
|
],
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
device = SafeModelChoiceField(
|
||||||
|
label=_('Device'),
|
||||||
|
empty_label=_('All devices'),
|
||||||
|
queryset=Device.objects.none(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
user_email = forms.EmailField(
|
||||||
|
label=_('User email'),
|
||||||
|
widget=forms.EmailInput(
|
||||||
|
attrs={"placeholder": _('All users')}
|
||||||
|
),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
action_type = forms.CharField(
|
||||||
|
widget=forms.HiddenInput,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
content_type = forms.ModelChoiceField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
widget=forms.HiddenInput,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
object = forms.IntegerField(
|
||||||
|
widget=forms.HiddenInput,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.organizer = kwargs.pop('organizer')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['device'].queryset = self.organizer.devices.all().order_by('device_id')
|
||||||
|
self.fields['device'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'generic',
|
||||||
|
'data-select2-url': reverse('control:organizer.devices.select2', kwargs={
|
||||||
|
'organizer': self.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': _('All devices'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['device'].widget.choices = self.fields['device'].choices
|
||||||
|
self.fields['device'].label = _('Device')
|
||||||
|
|
||||||
|
def filter_qs(self, qs):
|
||||||
|
fdata = self.cleaned_data
|
||||||
|
if fdata.get('source') == 'team':
|
||||||
|
qs = qs.filter(user__isnull=False)
|
||||||
|
elif fdata.get('source') == 'device':
|
||||||
|
qs = qs.filter(device__isnull=False)
|
||||||
|
elif fdata.get('source') == 'customer':
|
||||||
|
qs = qs.filter(user__isnull=True, device__isnull=True)
|
||||||
|
|
||||||
|
if fdata.get('device'):
|
||||||
|
qs = qs.filter(device_id=fdata['device'].pk)
|
||||||
|
|
||||||
|
if fdata.get('action_type'):
|
||||||
|
qs = qs.filter(action_type__in=fdata['action_type'].split(','))
|
||||||
|
|
||||||
|
if fdata.get('user_email'):
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=fdata['user_email'].lower())
|
||||||
|
qs = qs.filter(user=user)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# Do not actively leak info that this user does not exist. Yes, this will likely have a
|
||||||
|
# timing attack possibility, but with the magic that database query planners do,
|
||||||
|
# it's probably impossible to avoid that.
|
||||||
|
qs = qs.none()
|
||||||
|
|
||||||
|
if fdata.get('content_type'):
|
||||||
|
qs = qs.filter(content_type=fdata.get('content_type'))
|
||||||
|
|
||||||
|
if fdata.get('object'):
|
||||||
|
qs = qs.filter(object_id=fdata.get('object'))
|
||||||
|
return qs
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ class QuestionForm(I18nModelForm):
|
|||||||
'valid_string_length_max',
|
'valid_string_length_max',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'valid_datetime_min': SplitDateTimePickerWidget(),
|
'valid_datetime_min': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'valid_datetime_max': SplitDateTimePickerWidget(),
|
'valid_datetime_max': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'valid_date_min': DatePickerWidget(),
|
'valid_date_min': DatePickerWidget(),
|
||||||
'valid_date_max': DatePickerWidget(),
|
'valid_date_max': DatePickerWidget(),
|
||||||
'items': forms.CheckboxSelectMultiple(
|
'items': forms.CheckboxSelectMultiple(
|
||||||
@@ -1427,6 +1427,6 @@ class ItemProgramTimeForm(I18nModelForm):
|
|||||||
'end': forms.SplitDateTimeField,
|
'end': forms.SplitDateTimeField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'start': SplitDateTimePickerWidget(),
|
'start': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'end': SplitDateTimePickerWidget(),
|
'end': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
|
|||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||||
from pretix.control.forms.rrule import RRuleForm
|
from pretix.control.forms.rrule import RRuleForm
|
||||||
|
from pretix.helpers.i18n import get_javascript_format_without_seconds
|
||||||
from pretix.helpers.money import change_decimal_field
|
from pretix.helpers.money import change_decimal_field
|
||||||
|
|
||||||
|
|
||||||
@@ -80,11 +81,11 @@ class SubEventForm(I18nModelForm):
|
|||||||
'presale_end': SplitDateTimeField,
|
'presale_end': SplitDateTimeField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'date_from': SplitDateTimePickerWidget(),
|
'date_from': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
|
||||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
|
||||||
'presale_start': SplitDateTimePickerWidget(),
|
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
|
||||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ class SubEventBulkEditForm(I18nModelForm):
|
|||||||
self.fields[k + '_time'] = forms.TimeField(
|
self.fields[k + '_time'] = forms.TimeField(
|
||||||
label=self._meta.model._meta.get_field(k).verbose_name,
|
label=self._meta.model._meta.get_field(k).verbose_name,
|
||||||
help_text=self._meta.model._meta.get_field(k).help_text,
|
help_text=self._meta.model._meta.get_field(k).help_text,
|
||||||
widget=TimePickerWidget(),
|
widget=TimePickerWidget(without_seconds=True),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -506,6 +507,12 @@ class TimeForm(forms.Form):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['time_from'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
|
||||||
|
self.fields['time_to'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
|
||||||
|
self.fields['time_admission'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
|
||||||
|
|
||||||
|
|
||||||
TimeFormSet = formset_factory(
|
TimeFormSet = formset_factory(
|
||||||
TimeForm,
|
TimeForm,
|
||||||
|
|||||||
@@ -32,16 +32,20 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
import csv
|
import csv
|
||||||
from collections import namedtuple
|
from collections import Counter, namedtuple
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import EmailValidator
|
from django.core.validators import EmailValidator
|
||||||
|
from django.db.models import Count, F, Max
|
||||||
from django.db.models.functions import Upper
|
from django.db.models.functions import Upper
|
||||||
|
from django.forms.utils import ErrorDict
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes.forms import SafeModelChoiceField
|
from django_scopes.forms import SafeModelChoiceField
|
||||||
|
|
||||||
from pretix.base.email import get_available_placeholders
|
from pretix.base.email import get_available_placeholders
|
||||||
@@ -50,7 +54,10 @@ from pretix.base.forms import (
|
|||||||
)
|
)
|
||||||
from pretix.base.forms.widgets import format_placeholders_help_text
|
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Item, Voucher
|
from pretix.base.models import Item, ItemVariation, Quota, SubEvent, Voucher
|
||||||
|
from pretix.base.models.vouchers import VoucherBulkData
|
||||||
|
from pretix.base.services.locking import lock_objects
|
||||||
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||||
from pretix.control.signals import voucher_form_validation
|
from pretix.control.signals import voucher_form_validation
|
||||||
@@ -105,20 +112,22 @@ class VoucherForm(I18nModelForm):
|
|||||||
except Item.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
if not self.event and self.instance:
|
||||||
|
self.event = self.instance.event
|
||||||
|
|
||||||
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
|
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
|
||||||
'event': instance.event.slug,
|
'event': self.event.slug,
|
||||||
'organizer': instance.event.organizer.slug,
|
'organizer': self.event.organizer.slug,
|
||||||
})
|
})
|
||||||
|
|
||||||
if instance.event.has_subevents:
|
if self.event.has_subevents:
|
||||||
self.fields['subevent'].queryset = instance.event.subevents.all()
|
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||||
self.fields['subevent'].widget = Select2(
|
self.fields['subevent'].widget = Select2(
|
||||||
attrs={
|
attrs={
|
||||||
'data-model-select2': 'event',
|
'data-model-select2': 'event',
|
||||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
'event': instance.event.slug,
|
'event': self.event.slug,
|
||||||
'organizer': instance.event.organizer.slug,
|
'organizer': self.event.organizer.slug,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -128,18 +137,19 @@ class VoucherForm(I18nModelForm):
|
|||||||
del self.fields['subevent']
|
del self.fields['subevent']
|
||||||
|
|
||||||
choices = []
|
choices = []
|
||||||
if 'itemvar' in initial or (self.data and 'itemvar' in self.data):
|
prefix = (self.prefix + '-') if self.prefix else ''
|
||||||
iv = self.data.get('itemvar') or initial.get('itemvar', '')
|
if 'itemvar' in initial or (self.data and prefix + 'itemvar' in self.data):
|
||||||
|
iv = self.data.get(prefix + 'itemvar', '') or initial.get('itemvar', '') or ''
|
||||||
if iv.startswith('q-'):
|
if iv.startswith('q-'):
|
||||||
q = self.instance.event.quotas.get(pk=iv[2:])
|
q = self.event.quotas.get(pk=iv[2:])
|
||||||
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
||||||
elif '-' in iv:
|
elif '-' in iv:
|
||||||
itemid, varid = iv.split('-')
|
itemid, varid = iv.split('-')
|
||||||
i = self.instance.event.items.get(pk=itemid)
|
i = self.event.items.get(pk=itemid)
|
||||||
v = i.variations.get(pk=varid)
|
v = i.variations.get(pk=varid)
|
||||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||||
elif iv:
|
elif iv:
|
||||||
i = self.instance.event.items.get(pk=iv)
|
i = self.event.items.get(pk=iv)
|
||||||
if i.variations.exists():
|
if i.variations.exists():
|
||||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i)))
|
choices.append((str(i.pk), _('{product} – Any variation').format(product=i)))
|
||||||
else:
|
else:
|
||||||
@@ -150,8 +160,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
attrs={
|
attrs={
|
||||||
'data-model-select2': 'generic',
|
'data-model-select2': 'generic',
|
||||||
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
|
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
|
||||||
'event': instance.event.slug,
|
'event': self.event.slug,
|
||||||
'organizer': instance.event.organizer.slug,
|
'organizer': self.event.organizer.slug,
|
||||||
}),
|
}),
|
||||||
'data-placeholder': _('All products')
|
'data-placeholder': _('All products')
|
||||||
}
|
}
|
||||||
@@ -159,7 +169,7 @@ class VoucherForm(I18nModelForm):
|
|||||||
self.fields['itemvar'].required = False
|
self.fields['itemvar'].required = False
|
||||||
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
|
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
|
||||||
|
|
||||||
if self.instance.event.seating_plan or self.instance.event.subevents.filter(seating_plan__isnull=False).exists():
|
if self.event.seating_plan or self.event.subevents.filter(seating_plan__isnull=False).exists():
|
||||||
self.fields['seat'] = forms.CharField(
|
self.fields['seat'] = forms.CharField(
|
||||||
label=_("Specific seat ID"),
|
label=_("Specific seat ID"),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
@@ -169,40 +179,45 @@ class VoucherForm(I18nModelForm):
|
|||||||
help_text=str(self.instance.seat) if self.instance.seat else '',
|
help_text=str(self.instance.seat) if self.instance.seat else '',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def parse_itemvar(self, data):
|
||||||
|
try:
|
||||||
|
itemid = quotaid = None
|
||||||
|
iv = data.get('itemvar', '')
|
||||||
|
if iv.startswith('q-'):
|
||||||
|
quotaid = iv[2:]
|
||||||
|
elif '-' in iv:
|
||||||
|
itemid, varid = iv.split('-')
|
||||||
|
elif iv:
|
||||||
|
itemid, varid = iv, None
|
||||||
|
else:
|
||||||
|
itemid, varid = None, None
|
||||||
|
|
||||||
|
if itemid:
|
||||||
|
item = self.event.items.get(pk=itemid)
|
||||||
|
if varid:
|
||||||
|
variation = item.variations.get(pk=varid)
|
||||||
|
else:
|
||||||
|
variation = None
|
||||||
|
quota = None
|
||||||
|
elif quotaid:
|
||||||
|
quota = self.event.quotas.get(pk=quotaid)
|
||||||
|
item = None
|
||||||
|
variation = None
|
||||||
|
else:
|
||||||
|
quota = None
|
||||||
|
item = None
|
||||||
|
variation = None
|
||||||
|
|
||||||
|
return (item, variation, quota)
|
||||||
|
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValidationError(_("Invalid product selected."))
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
if not self._errors:
|
if not self._errors:
|
||||||
try:
|
self.instance.item, self.instance.variation, self.instance.quota = self.parse_itemvar(self.data)
|
||||||
itemid = quotaid = None
|
|
||||||
iv = self.data.get('itemvar', '')
|
|
||||||
if iv.startswith('q-'):
|
|
||||||
quotaid = iv[2:]
|
|
||||||
elif '-' in iv:
|
|
||||||
itemid, varid = iv.split('-')
|
|
||||||
elif iv:
|
|
||||||
itemid, varid = iv, None
|
|
||||||
else:
|
|
||||||
itemid, varid = None, None
|
|
||||||
|
|
||||||
if itemid:
|
|
||||||
self.instance.item = self.instance.event.items.get(pk=itemid)
|
|
||||||
if varid:
|
|
||||||
self.instance.variation = self.instance.item.variations.get(pk=varid)
|
|
||||||
else:
|
|
||||||
self.instance.variation = None
|
|
||||||
self.instance.quota = None
|
|
||||||
elif quotaid:
|
|
||||||
self.instance.quota = self.instance.event.quotas.get(pk=quotaid)
|
|
||||||
self.instance.item = None
|
|
||||||
self.instance.variation = None
|
|
||||||
else:
|
|
||||||
self.instance.quota = None
|
|
||||||
self.instance.item = None
|
|
||||||
self.instance.variation = None
|
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise ValidationError(_("Invalid product selected."))
|
|
||||||
|
|
||||||
if 'codes' in data:
|
if 'codes' in data:
|
||||||
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a]
|
||||||
@@ -214,7 +229,7 @@ class VoucherForm(I18nModelForm):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Voucher.clean_item_properties(
|
Voucher.clean_item_properties(
|
||||||
data, self.instance.event,
|
data, self.event,
|
||||||
self.instance.quota, self.instance.item, self.instance.variation,
|
self.instance.quota, self.instance.item, self.instance.variation,
|
||||||
seats_given=data.get('seat') or data.get('seats'),
|
seats_given=data.get('seat') or data.get('seats'),
|
||||||
block_quota=data.get('block_quota')
|
block_quota=data.get('block_quota')
|
||||||
@@ -234,7 +249,7 @@ class VoucherForm(I18nModelForm):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Voucher.clean_subevent(
|
Voucher.clean_subevent(
|
||||||
data, self.instance.event
|
data, self.event
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise ValidationError({"subevent": e.message})
|
raise ValidationError({"subevent": e.message})
|
||||||
@@ -250,19 +265,19 @@ class VoucherForm(I18nModelForm):
|
|||||||
if check_quota:
|
if check_quota:
|
||||||
Voucher.clean_quota_check(
|
Voucher.clean_quota_check(
|
||||||
data, cnt, self.initial_instance_data,
|
data, cnt, self.initial_instance_data,
|
||||||
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
self.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||||
)
|
)
|
||||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
Voucher.clean_voucher_code(data, self.event, self.instance.pk)
|
||||||
if 'seat' in self.fields:
|
if 'seat' in self.fields:
|
||||||
if data.get('seat'):
|
if data.get('seat'):
|
||||||
self.instance.seat = Voucher.clean_seat_id(
|
self.instance.seat = Voucher.clean_seat_id(
|
||||||
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
data, self.instance.item, self.instance.quota, self.event, self.instance.pk
|
||||||
)
|
)
|
||||||
self.instance.item = self.instance.seat.product
|
self.instance.item = self.instance.seat.product
|
||||||
else:
|
else:
|
||||||
self.instance.seat = None
|
self.instance.seat = None
|
||||||
|
|
||||||
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
|
voucher_form_validation.send(sender=self.event, form=self, data=data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -270,6 +285,215 @@ class VoucherForm(I18nModelForm):
|
|||||||
return super().save(commit)
|
return super().save(commit)
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherBulkEditForm(VoucherForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.mixed_values = kwargs.pop('mixed_values')
|
||||||
|
self.queryset = kwargs.pop('queryset')
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
del self.fields["code"]
|
||||||
|
self.fields.pop("seat", None)
|
||||||
|
|
||||||
|
def is_bulk_checked(self, fieldname):
|
||||||
|
return self.prefix + fieldname in self.data.getlist('_bulk')
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# We skip the parent class because it's not suited for bulk editing and implement custom validation here.
|
||||||
|
# This does not validate *everything* we validate in VoucherForm. For example, we skip validation that one does
|
||||||
|
# not create a voucher for an add-on product or that the seat matches the product to save on complexity.
|
||||||
|
# This is a UX validation only anyway, since one could first create the voucher and then make the product an
|
||||||
|
# add-on product. However, we need to validate everything that we don't want violated in the database.
|
||||||
|
data = super(VoucherForm, self).clean()
|
||||||
|
|
||||||
|
if self.is_bulk_checked("itemvar"):
|
||||||
|
data["item"], data["variation"], data["quota"] = self.parse_itemvar(data)
|
||||||
|
|
||||||
|
if self.is_bulk_checked("max_usages") and "max_usages" in data:
|
||||||
|
max_redeemed = self.queryset.aggregate(m=Max("redeemed"))["m"]
|
||||||
|
if data["max_usages"] < max_redeemed:
|
||||||
|
raise ValidationError(_(
|
||||||
|
"You cannot reduce the maximum number of redemptions to %(max_usages)s, because at least one "
|
||||||
|
"of the selected vouchers has already been redeemed %(max_redeemed)s times."
|
||||||
|
) % {"max_usages": data["max_usages"], "max_redeemed": max_redeemed})
|
||||||
|
|
||||||
|
# Check diff on product and quota usage based on old groups of vouchers
|
||||||
|
if any(self.is_bulk_checked(k) for k in ("max_usages", "itemvar", "block_quota", "valid_until", "subevent")):
|
||||||
|
quota_diff = Counter()
|
||||||
|
|
||||||
|
current_vouchers = self.queryset.order_by().values(
|
||||||
|
"item", "variation", "quota", "block_quota", "valid_until", "subevent", "redeemed", "max_usages",
|
||||||
|
"allow_ignore_quota",
|
||||||
|
).annotate(c=Count("*"))
|
||||||
|
item_cache = {i.pk: i for i in Item.objects.filter(pk__in=[c["item"] for c in current_vouchers])}
|
||||||
|
var_cache = {v.pk: v for v in ItemVariation.objects.filter(pk__in=[c["variation"] for c in current_vouchers])}
|
||||||
|
quota_cache = {q.pk: q for q in Quota.objects.filter(pk__in=[c["quota"] for c in current_vouchers])}
|
||||||
|
subevent_cache = {s.pk: s for s in SubEvent.objects.filter(pk__in=[c["subevent"] for c in current_vouchers])}
|
||||||
|
|
||||||
|
for current in current_vouchers:
|
||||||
|
bulk_count = current.pop('c')
|
||||||
|
current = VoucherBulkData(**current)
|
||||||
|
# Get quotas that are currently used
|
||||||
|
if current.item:
|
||||||
|
current.item = item_cache[current.item]
|
||||||
|
if current.variation:
|
||||||
|
current.variation = var_cache[current.variation]
|
||||||
|
if current.quota:
|
||||||
|
current.quota = quota_cache[current.quota]
|
||||||
|
if current.subevent:
|
||||||
|
current.subevent = subevent_cache[current.subevent]
|
||||||
|
|
||||||
|
old_quotas = Voucher.clean_quota_get_ignored(current)
|
||||||
|
old_amount = max(current.max_usages - current.redeemed, 0) * bulk_count
|
||||||
|
|
||||||
|
# Predict state after change
|
||||||
|
after_change = copy.copy(current)
|
||||||
|
if self.is_bulk_checked("itemvar") and "itemvar" in data:
|
||||||
|
after_change.item = data["item"]
|
||||||
|
after_change.variation = data["variation"]
|
||||||
|
after_change.quota = data["quota"]
|
||||||
|
if self.is_bulk_checked("subevent") and "subevent" in data:
|
||||||
|
after_change.subevent = data["subevent"]
|
||||||
|
if self.is_bulk_checked("max_usages") and "max_usages" in data:
|
||||||
|
after_change.max_usages = data["max_usages"]
|
||||||
|
if self.is_bulk_checked("block_quota") and "block_quota" in data:
|
||||||
|
after_change.block_quota = data["block_quota"]
|
||||||
|
if self.is_bulk_checked("valid_until") and "valid_until" in data:
|
||||||
|
after_change.valid_until = data["valid_until"]
|
||||||
|
if self.is_bulk_checked("allow_ignore_quota") and "allow_ignore_quota" in data:
|
||||||
|
after_change.allow_ignore_quota = data["allow_ignore_quota"]
|
||||||
|
|
||||||
|
if after_change.quota and self.event.has_subevents and not after_change.subevent:
|
||||||
|
raise ValidationError(_("You cannot create a voucher that allows selection of a quota but has no date selected."))
|
||||||
|
|
||||||
|
if after_change.quota and after_change.subevent and after_change.quota.subevent_id != after_change.subevent.pk:
|
||||||
|
raise ValidationError(_("The selected quota does not match the selected subevent."))
|
||||||
|
|
||||||
|
if after_change.block_quota and self.event.has_subevents and not after_change.subevent:
|
||||||
|
raise ValidationError(
|
||||||
|
_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||||
|
|
||||||
|
if after_change.block_quota and not after_change.item and not after_change.quota:
|
||||||
|
raise ValidationError(
|
||||||
|
_('You need to select a specific product or quota if this voucher should reserve '
|
||||||
|
'tickets.')
|
||||||
|
)
|
||||||
|
|
||||||
|
if after_change.allow_ignore_quota:
|
||||||
|
# todo: is this the most useful way to do this?
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_quotas = Voucher.clean_quota_get_ignored(after_change)
|
||||||
|
new_amount = max(after_change.max_usages - after_change.redeemed, 0) * bulk_count
|
||||||
|
|
||||||
|
if new_quotas != old_quotas or new_amount != old_amount:
|
||||||
|
for q in old_quotas:
|
||||||
|
quota_diff[q] -= old_amount
|
||||||
|
for q in new_quotas:
|
||||||
|
quota_diff[q] += new_amount
|
||||||
|
|
||||||
|
if any(v > 0 for q, v in quota_diff.items()):
|
||||||
|
lock_objects([q for q, v in quota_diff.items() if q.size is not None and v > 0], shared_lock_objects=[self.event])
|
||||||
|
qa = QuotaAvailability(count_waitinglist=False)
|
||||||
|
qa.queue(*(q for q, v in quota_diff.items() if v > 0))
|
||||||
|
qa.compute()
|
||||||
|
|
||||||
|
if any(qa.results[q][0] != Quota.AVAILABILITY_OK or (qa.results[q][1] is not None and qa.results[q][1] < required)
|
||||||
|
for q, required in quota_diff.items() if required > 0):
|
||||||
|
raise ValidationError(_(
|
||||||
|
'There is no sufficient quota available to perform this change.'
|
||||||
|
))
|
||||||
|
|
||||||
|
has_seat = self.queryset.filter(seat__isnull=False).exists()
|
||||||
|
if has_seat:
|
||||||
|
if self.is_bulk_checked("max_usages"):
|
||||||
|
raise ValidationError(_(
|
||||||
|
'Changing the maximum number of usages in bulk is not supported if any of the selected vouchers '
|
||||||
|
'is assigned a seat.'
|
||||||
|
))
|
||||||
|
if self.is_bulk_checked("subevent"):
|
||||||
|
raise ValidationError(pgettext_lazy(
|
||||||
|
'subevent',
|
||||||
|
'Changing the date in bulk is not supported if any of the selected vouchers '
|
||||||
|
'is assigned a seat.'
|
||||||
|
))
|
||||||
|
if self.is_bulk_checked("itemvar") and data["quota"]:
|
||||||
|
raise ValidationError(_(
|
||||||
|
'Changing the product to a quota is not supported if any of the selected vouchers '
|
||||||
|
'is assigned a seat.'
|
||||||
|
))
|
||||||
|
|
||||||
|
if self.is_bulk_checked("valid_until"):
|
||||||
|
if data["valid_until"] is None or data["valid_until"] >= now():
|
||||||
|
currently_not_blocked_seats = self.queryset.filter(
|
||||||
|
seat__isnull=False,
|
||||||
|
max_usages__gt=F("redeemed"),
|
||||||
|
valid_until__lt=now(),
|
||||||
|
)
|
||||||
|
if self.event.has_subevents:
|
||||||
|
subevents = self.event.subevents.filter(pk__in=currently_not_blocked_seats.values_list("subevent"))
|
||||||
|
for se in subevents:
|
||||||
|
conflicts = currently_not_blocked_seats.filter(
|
||||||
|
subevent=se
|
||||||
|
).exclude(
|
||||||
|
seat_id__in=se.free_seats().values("pk")
|
||||||
|
)
|
||||||
|
if conflicts:
|
||||||
|
raise ValidationError(_(
|
||||||
|
'This change cannot be completed because not all assigned seats of the vouchers are '
|
||||||
|
'still available'
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
conflicts = currently_not_blocked_seats.exclude(
|
||||||
|
seat_id__in=self.event.free_seats().values("pk")
|
||||||
|
)
|
||||||
|
if conflicts:
|
||||||
|
raise ValidationError(_(
|
||||||
|
'This change cannot be completed because not all assigned seats of the vouchers are '
|
||||||
|
'still available'
|
||||||
|
))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
objs = list(self.queryset)
|
||||||
|
fields = set()
|
||||||
|
|
||||||
|
check_map = {
|
||||||
|
'price_mode': '__price',
|
||||||
|
'value': '__price',
|
||||||
|
}
|
||||||
|
for k in self.fields:
|
||||||
|
if not self.is_bulk_checked(check_map.get(k, k)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if k == 'itemvar':
|
||||||
|
fields.add("item")
|
||||||
|
fields.add("variation")
|
||||||
|
fields.add("quota")
|
||||||
|
else:
|
||||||
|
fields.add(k)
|
||||||
|
for obj in objs:
|
||||||
|
if k == 'itemvar':
|
||||||
|
obj.item = self.cleaned_data["item"]
|
||||||
|
obj.variation = self.cleaned_data["variation"]
|
||||||
|
obj.quota = self.cleaned_data["quota"]
|
||||||
|
else:
|
||||||
|
setattr(obj, k, self.cleaned_data[k])
|
||||||
|
|
||||||
|
fields = [f for f in fields if f != 'itemvars']
|
||||||
|
if fields:
|
||||||
|
Voucher.objects.bulk_update(objs, fields, 200)
|
||||||
|
|
||||||
|
def full_clean(self):
|
||||||
|
if len(self.data) == 0:
|
||||||
|
# form wasn't submitted
|
||||||
|
self._errors = ErrorDict()
|
||||||
|
return
|
||||||
|
super().full_clean()
|
||||||
|
|
||||||
|
def _post_clean(self):
|
||||||
|
pass # skip model-level clean
|
||||||
|
|
||||||
|
|
||||||
class VoucherBulkForm(VoucherForm):
|
class VoucherBulkForm(VoucherForm):
|
||||||
codes = forms.CharField(
|
codes = forms.CharField(
|
||||||
widget=forms.Textarea,
|
widget=forms.Textarea,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from urllib.parse import quote, urljoin, urlparse
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.contrib.auth.views import redirect_to_login
|
||||||
from django.http import Http404
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, resolve_url
|
from django.shortcuts import get_object_or_404, resolve_url
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import get_script_prefix, resolve, reverse
|
from django.urls import get_script_prefix, resolve, reverse
|
||||||
@@ -98,6 +98,8 @@ class PermissionMiddleware:
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def _login_redirect(self, request):
|
def _login_redirect(self, request):
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
|
|
||||||
# Taken from django/contrib/auth/decorators.py
|
# Taken from django/contrib/auth/decorators.py
|
||||||
path = request.build_absolute_uri()
|
path = request.build_absolute_uri()
|
||||||
# urlparse chokes on lazy objects in Python 3, force to str
|
# urlparse chokes on lazy objects in Python 3, force to str
|
||||||
@@ -110,10 +112,21 @@ class PermissionMiddleware:
|
|||||||
if ((not login_scheme or login_scheme == current_scheme) and
|
if ((not login_scheme or login_scheme == current_scheme) and
|
||||||
(not login_netloc or login_netloc == current_netloc)):
|
(not login_netloc or login_netloc == current_netloc)):
|
||||||
path = request.get_full_path()
|
path = request.get_full_path()
|
||||||
from django.contrib.auth.views import redirect_to_login
|
|
||||||
|
|
||||||
return redirect_to_login(
|
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
# It's not useful to return a 302 redirect on a XMLHttpRequest request, because
|
||||||
|
# the XMLHttpRequest is unable to detect redirects.
|
||||||
|
return HttpResponse(
|
||||||
|
"Authentication required",
|
||||||
|
status=401,
|
||||||
|
headers={
|
||||||
|
# Appending ?next= is handled by client, because it should be the top-level context url,
|
||||||
|
# not the URL called in the background
|
||||||
|
"X-Login-Url": resolved_login_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect_to_login(path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
url = resolve(request.path_info)
|
url = resolve(request.path_info)
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ from pretix.base.signals import (
|
|||||||
html_page_start = GlobalSignal()
|
html_page_start = GlobalSignal()
|
||||||
"""
|
"""
|
||||||
This signal allows you to put code in the beginning of the main page for every
|
This signal allows you to put code in the beginning of the main page for every
|
||||||
page in the backend. You are expected to return HTML.
|
page in the backend. You are expected to return a SafeString containing HTML, or
|
||||||
|
a string that will be HTML-escaped.
|
||||||
|
|
||||||
The ``sender`` keyword argument will contain the request.
|
The ``sender`` keyword argument will contain the request.
|
||||||
"""
|
"""
|
||||||
@@ -129,7 +130,7 @@ event_dashboard_top = EventPluginSignal()
|
|||||||
Arguments: 'request'
|
Arguments: 'request'
|
||||||
|
|
||||||
This signal is sent out to include custom HTML in the top part of the the event dashboard.
|
This signal is sent out to include custom HTML in the top part of the the event dashboard.
|
||||||
Receivers should return HTML.
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
An additional keyword argument ``subevent`` *can* contain a sub-event.
|
An additional keyword argument ``subevent`` *can* contain a sub-event.
|
||||||
@@ -140,7 +141,7 @@ event_dashboard_widgets = EventPluginSignal()
|
|||||||
This signal is sent out to include widgets in the event dashboard. Receivers
|
This signal is sent out to include widgets in the event dashboard. Receivers
|
||||||
should return a list of dictionaries, where each dictionary can have the keys:
|
should return a list of dictionaries, where each dictionary can have the keys:
|
||||||
|
|
||||||
* content (str, containing HTML)
|
* content (SafeString, containing HTML)
|
||||||
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
|
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
|
||||||
(quarter of a row). May be ignored on small displays, default is "small")
|
(quarter of a row). May be ignored on small displays, default is "small")
|
||||||
* priority (int, used for ordering, higher comes first, default is 1)
|
* priority (int, used for ordering, higher comes first, default is 1)
|
||||||
@@ -157,7 +158,7 @@ Arguments: 'user'
|
|||||||
This signal is sent out to include widgets in the personal user dashboard. Receivers
|
This signal is sent out to include widgets in the personal user dashboard. Receivers
|
||||||
should return a list of dictionaries, where each dictionary can have the keys:
|
should return a list of dictionaries, where each dictionary can have the keys:
|
||||||
|
|
||||||
* content (str, containing HTML)
|
* content (SafeString, containing HTML)
|
||||||
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
|
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
|
||||||
(quarter of a row). May be ignored on small displays, default is "small")
|
(quarter of a row). May be ignored on small displays, default is "small")
|
||||||
* priority (int, used for ordering, higher comes first, default is 1)
|
* priority (int, used for ordering, higher comes first, default is 1)
|
||||||
@@ -172,6 +173,7 @@ Arguments: 'form'
|
|||||||
|
|
||||||
This signal allows you to add additional HTML to the form that is used for modifying vouchers.
|
This signal allows you to add additional HTML to the form that is used for modifying vouchers.
|
||||||
You receive the form object in the ``form`` keyword argument.
|
You receive the form object in the ``form`` keyword argument.
|
||||||
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
@@ -209,6 +211,7 @@ Arguments: 'quota'
|
|||||||
|
|
||||||
This signal allows you to append HTML to a Quota's detail view. You receive the
|
This signal allows you to append HTML to a Quota's detail view. You receive the
|
||||||
quota as argument in the ``quota`` keyword argument.
|
quota as argument in the ``quota`` keyword argument.
|
||||||
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
@@ -219,6 +222,7 @@ Arguments: 'subevent'
|
|||||||
|
|
||||||
This signal allows you to append HTML to a SubEvent's detail view. You receive the
|
This signal allows you to append HTML to a SubEvent's detail view. You receive the
|
||||||
subevent as argument in the ``subevent`` keyword argument.
|
subevent as argument in the ``subevent`` keyword argument.
|
||||||
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
@@ -265,7 +269,8 @@ order_info = EventPluginSignal()
|
|||||||
"""
|
"""
|
||||||
Arguments: ``order``, ``request``
|
Arguments: ``order``, ``request``
|
||||||
|
|
||||||
This signal is sent out to display additional information on the order detail page
|
This signal is sent out to display additional information on the order detail page.
|
||||||
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
Additionally, the argument ``order`` and ``request`` are available.
|
Additionally, the argument ``order`` and ``request`` are available.
|
||||||
@@ -275,7 +280,8 @@ order_approve_info = EventPluginSignal()
|
|||||||
"""
|
"""
|
||||||
Arguments: ``order``, ``request``
|
Arguments: ``order``, ``request``
|
||||||
|
|
||||||
This signal is sent out to display additional information on the order approve page
|
This signal is sent out to display additional information on the order approve page.
|
||||||
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
Additionally, the argument ``order`` and ``request`` are available.
|
Additionally, the argument ``order`` and ``request`` are available.
|
||||||
@@ -286,6 +292,7 @@ order_position_buttons = EventPluginSignal()
|
|||||||
Arguments: ``order``, ``position``, ``request``
|
Arguments: ``order``, ``position``, ``request``
|
||||||
|
|
||||||
This signal is sent out to display additional buttons for a single position of an order.
|
This signal is sent out to display additional buttons for a single position of an order.
|
||||||
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
Additionally, the argument ``order`` and ``request`` are available.
|
Additionally, the argument ``order`` and ``request`` are available.
|
||||||
@@ -315,6 +322,7 @@ Arguments: 'request'
|
|||||||
|
|
||||||
This signal is sent out to include template snippets on the settings page of an event
|
This signal is sent out to include template snippets on the settings page of an event
|
||||||
that allows generating a pretix Widget code.
|
that allows generating a pretix Widget code.
|
||||||
|
Receivers should return a SafeString containing HTML, or a string that will be HTML-escaped.
|
||||||
|
|
||||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
A second keyword argument ``request`` will contain the request object.
|
A second keyword argument ``request`` will contain the request object.
|
||||||
|
|||||||
@@ -56,5 +56,4 @@
|
|||||||
</form>
|
</form>
|
||||||
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
|
<script type="text/plain" id="good_origin">{{ good_origin }}</script>
|
||||||
<script type="text/plain" id="bad_origin_report_url">{{ bad_origin_report_url }}</script>
|
<script type="text/plain" id="bad_origin_report_url">{{ bad_origin_report_url }}</script>
|
||||||
<!-- pretix-login-marker -->{# marker required for ajax calls to detect that user session is over #}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
|
{% load escapejson %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" action="" method="post" id="webauthn-form">
|
<form class="form-signin" action="" method="post" id="webauthn-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -30,8 +31,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<script type="text/json" id="webauthn-login">
|
<script type="text/json" id="webauthn-login">
|
||||||
{{ jsondata|safe }}
|
{{ jsondata|escapejson }}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x"></span>
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x"></span>
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x"></span>
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x"></span>
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x"></span>
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -50,6 +50,46 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if dkim_warning %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>
|
||||||
|
{{ dkim_warning }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% trans "Your new DKIM record should be set up as a CNAME record like this:" %}
|
||||||
|
</p>
|
||||||
|
<pre><code>{{ dkim_hostname }} CNAME {{ dkim_cname }}</code></pre>
|
||||||
|
<p>
|
||||||
|
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% elif dkim_cname %}
|
||||||
|
<div class="alert alert-success">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
We found a DKIM record on your domain for this system. Great!
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if dmarc_warning %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>
|
||||||
|
{{ dmarc_warning }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% trans "Your new DMARC record could look like this:" %}
|
||||||
|
</p>
|
||||||
|
<pre><code>_dmarc.{{ hostname }} TXT "v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;"</code></pre>
|
||||||
|
<p>
|
||||||
|
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% elif dkim_cname %}
|
||||||
|
<div class="alert alert-success">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
We found a DMARC record on your domain for this system. Great!
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if verification %}
|
{% if verification %}
|
||||||
<h3>{% trans "Verification" %}</h3>
|
<h3>{% trans "Verification" %}</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -70,7 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if spf_warning %}
|
{% if spf_warning or dkim_warning or dmarc_warning %}
|
||||||
<div class="form-group submit-group">
|
<div class="form-group submit-group">
|
||||||
<a href="" class="btn btn-default btn-save">
|
<a href="" class="btn btn-default btn-save">
|
||||||
{% trans "Cancel" %}
|
{% trans "Cancel" %}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x"></span>
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% elif w.link %}
|
{% elif w.link %}
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x´"></span>
|
<span class="fa fa-cog fa-4x´"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
{% if w.lazy %}
|
{% if w.lazy %}
|
||||||
<span class="fa fa-cog fa-4x"></span>
|
<span class="fa fa-cog fa-4x"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ w.content|safe }}
|
{{ w.content }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for issue in issues %}
|
{% for issue in issues %}
|
||||||
<li>{{ issue|safe }}</li>
|
<li>{{ issue }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for issue in issues %}
|
{% for issue in issues %}
|
||||||
<li>{{ issue|safe }}</li>
|
<li>{{ issue }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,41 +1,12 @@
|
|||||||
{% extends "pretixcontrol/items/base.html" %}
|
{% extends "pretixcontrol/items/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load icon %}
|
||||||
{% block title %}{% trans "Event logs" %}{% endblock %}
|
{% block title %}{% trans "Event logs" %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Event logs" %}</h1>
|
<h1>{% trans "Event logs" %}</h1>
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
{% include "pretixcontrol/fragment_log_filter_form.html" %}
|
||||||
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
|
||||||
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
|
||||||
<p>
|
|
||||||
<select name="user" class="form-control">
|
|
||||||
<option value="">{% trans "All actions" %}</option>
|
|
||||||
<option value="yes" {% if request.GET.user == "yes" %}selected="selected"{% endif %}>
|
|
||||||
{% trans "Team actions" %}
|
|
||||||
</option>
|
|
||||||
<option value="no" {% if request.GET.user == "no" %}selected="selected"{% endif %}>
|
|
||||||
{% trans "Customer actions" %}
|
|
||||||
</option>
|
|
||||||
{% for up in userlist %}
|
|
||||||
{% if up.user__id %}
|
|
||||||
<option value="{{ up.user__id }}"
|
|
||||||
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
|
||||||
{{ up.user__email }}
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for d in devicelist %}
|
|
||||||
{% if d.device__id %}
|
|
||||||
<option value="d-{{ d.device__id }}"
|
|
||||||
{% if "d-" in request.GET.user and request.GET.user|slice:"2:" == d.device__id|slugify %}selected="selected"{% endif %}>
|
|
||||||
{{ d.device__name }}
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for log in logs %}
|
{% for log in logs %}
|
||||||
<li class="list-group-item logentry">
|
<li class="list-group-item logentry">
|
||||||
@@ -95,5 +66,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include "pretixcontrol/pagination.html" %}
|
{% include "pretixcontrol/pagination_huge.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<table class="table table-payment-providers">
|
<table class="table table-payment-providers">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for provider in providers %}
|
{% for provider in providers %}
|
||||||
<tr>
|
<tr{% if provider.highlight %} class="success-left"{% endif %}>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ provider.verbose_name }}</strong>
|
<strong>{{ provider.verbose_name }}</strong>
|
||||||
</td>
|
</td>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
<br>
|
<br>
|
||||||
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
|
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
|
||||||
<a href="{{ plugin_settings_url }}#tab-0-1-open" class="btn btn-default">
|
<a href="{{ plugin_settings_url }}?go=payment#tab-0-1-open" class="btn btn-default">
|
||||||
<i class="fa fa-plus"></i> {% trans "Enable additional payment plugins" %}
|
<i class="fa fa-plus"></i> {% trans "Enable additional payment plugins" %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<form action="" method="post" class="form-horizontal form-plugins">
|
<form action="" method="post" class="form-horizontal form-plugins">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="go" value="{{ request.GET.go }}">
|
||||||
<div id="plugin_search_results" class="panel panel-default collapse">
|
<div id="plugin_search_results" class="panel panel-default collapse">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
|||||||
@@ -193,7 +193,6 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% bootstrap_field form.contact_mail layout="control" %}
|
{% bootstrap_field form.contact_mail layout="control" %}
|
||||||
{% bootstrap_field form.contact_url layout="control" %}
|
|
||||||
{% bootstrap_field form.imprint_url layout="control" %}
|
{% bootstrap_field form.imprint_url layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load icon %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
{% trans "Filter" %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<form class="panel-body filter-form" action="" method="get">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-xs-6">
|
||||||
|
{% bootstrap_field filter_form.source %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-xs-6">
|
||||||
|
{% bootstrap_field filter_form.device %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-xs-6">
|
||||||
|
{% bootstrap_field filter_form.user_email %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ filter_form.action_type }}
|
||||||
|
{{ filter_form.content_type }}
|
||||||
|
{{ filter_form.object }}
|
||||||
|
{% if filter_form.is_valid and filter_form.cleaned_data.action_type %}
|
||||||
|
{% icon "filter" %} <code>{{ filter_form.cleaned_data.action_type }}</code>
|
||||||
|
{% endif %}
|
||||||
|
{% if filter_form.is_valid and filter_form.cleaned_data.content_type and filter_form.cleaned_data.object %}
|
||||||
|
{% icon "filter" %} {% trans "Specific object selected" %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="text-right flip">
|
||||||
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
|
<span class="fa fa-filter"></span>
|
||||||
|
{% trans "Filter" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -156,11 +156,11 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
{% if not i.tax_rule.price_includes_tax %}
|
{% if not i.tax_rule.price_includes_tax %}
|
||||||
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name %}
|
{% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name %}
|
||||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
|
{% blocktrans trimmed with rate=i.tax_rule.rate|tax_rate_format taxname=i.tax_rule.name|default:s_taxes %}
|
||||||
incl. {{ rate }}% {{ taxname }}
|
incl. {{ rate }}% {{ taxname }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load money %}
|
{% load money %}
|
||||||
|
{% load wrap_in %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans "Cancel order" %}
|
{% trans "Cancel order" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
{% if form.cancellation_fee %}
|
{% if form.cancellation_fee %}
|
||||||
{% if fee %}
|
{% if fee %}
|
||||||
{% with fee|money:request.event.currency as f %}
|
{% with fee|money:request.event.currency as f %}
|
||||||
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
|
<p>{% blocktrans trimmed with fee=f|wrap_in:"strong" %}
|
||||||
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
||||||
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
|
|||||||
@@ -284,6 +284,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
{% bootstrap_field position.form.operation_secret layout='inline' %}
|
{% bootstrap_field position.form.operation_secret layout='inline' %}
|
||||||
|
{% if position.issued_gift_cards.exists %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Ticket secrets of order positions that have been used to issue a gift card can not
|
||||||
|
be changed. Only the link will be changed in this case.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -705,7 +705,7 @@
|
|||||||
{% if line.tax_rate %}
|
{% if line.tax_rate %}
|
||||||
<br/>
|
<br/>
|
||||||
<small>
|
<small>
|
||||||
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
|
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
|
||||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</small>
|
</small>
|
||||||
@@ -715,7 +715,7 @@
|
|||||||
{% if line.tax_rate and line.price %}
|
{% if line.tax_rate and line.price %}
|
||||||
<br/>
|
<br/>
|
||||||
<small>
|
<small>
|
||||||
{% blocktrans trimmed with rate=line.tax_rate|floatformat:-2 taxname=line.tax_rule.name|default:s_taxes %}
|
{% blocktrans trimmed with rate=line.tax_rate|tax_rate_format taxname=line.tax_rule.name|default:s_taxes %}
|
||||||
incl. {{ rate }}% {{ taxname }}
|
incl. {{ rate }}% {{ taxname }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</small>
|
</small>
|
||||||
@@ -755,7 +755,7 @@
|
|||||||
{% if fee.tax_rate %}
|
{% if fee.tax_rate %}
|
||||||
<br/>
|
<br/>
|
||||||
<small>
|
<small>
|
||||||
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
|
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
|
||||||
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
<strong>plus</strong> {{ rate }}% {{ taxname }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</small>
|
</small>
|
||||||
@@ -765,7 +765,7 @@
|
|||||||
{% if fee.tax_rate %}
|
{% if fee.tax_rate %}
|
||||||
<br/>
|
<br/>
|
||||||
<small>
|
<small>
|
||||||
{% blocktrans trimmed with rate=fee.tax_rate|floatformat:-2 taxname=fee.tax_rule.name|default:s_taxes %}
|
{% blocktrans trimmed with rate=fee.tax_rate|tax_rate_format taxname=fee.tax_rule.name|default:s_taxes %}
|
||||||
incl. {{ rate }}% {{ taxname }}
|
incl. {{ rate }}% {{ taxname }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</small>
|
</small>
|
||||||
@@ -903,7 +903,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="1"></td>
|
<td colspan="1"></td>
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
{{ p.html_info|safe }}
|
{{ p.html_info }}
|
||||||
{% if staff_session %}
|
{% if staff_session %}
|
||||||
<p>
|
<p>
|
||||||
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
|
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
|
||||||
@@ -1018,7 +1018,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if r.html_info %}
|
{% if r.html_info %}
|
||||||
{{ r.html_info|safe }}
|
{{ r.html_info }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if staff_session %}
|
{% if staff_session %}
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
{% load escapejson %}
|
||||||
{% block inner %}
|
{% block inner %}
|
||||||
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
|
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
|
||||||
</li>
|
</li>
|
||||||
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
||||||
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
|
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script><br>
|
||||||
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
||||||
<br>
|
<br>
|
||||||
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
|
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
|
||||||
|
|||||||
@@ -137,6 +137,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
||||||
|
{% if staff_session %}
|
||||||
|
<details class="admin-only">
|
||||||
|
<summary>
|
||||||
|
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||||
|
{% trans "Details" %}
|
||||||
|
</summary>
|
||||||
|
<pre class="admin-only">{{ d.info|pprint }}</pre>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if d.initialized %}
|
{% if d.initialized %}
|
||||||
|
|||||||
@@ -4,24 +4,7 @@
|
|||||||
{% block title %}{% trans "Organizer logs" %}{% endblock %}
|
{% block title %}{% trans "Organizer logs" %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Organizer logs" %}</h1>
|
<h1>{% trans "Organizer logs" %}</h1>
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
{% include "pretixcontrol/fragment_log_filter_form.html" %}
|
||||||
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
|
||||||
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
|
||||||
<p>
|
|
||||||
<select name="user" class="form-control">
|
|
||||||
<option value="">{% trans "All actions" %}</option>
|
|
||||||
{% for up in userlist %}
|
|
||||||
{% if up.user__id %}
|
|
||||||
<option value="{{ up.user__id }}"
|
|
||||||
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
|
||||||
{{ up.user__email }}
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for log in logs %}
|
{% for log in logs %}
|
||||||
<li class="list-group-item logentry">
|
<li class="list-group-item logentry">
|
||||||
@@ -81,5 +64,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include "pretixcontrol/pagination.html" %}
|
{% include "pretixcontrol/pagination_huge.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -560,11 +560,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>
|
|
||||||
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
|
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "ajv/ajv2020.bundle.min.js" %}"></script>
|
|
||||||
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "schema/pdf-layout.validate.js" %}"></script>
|
||||||
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
|
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
|
||||||
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_white.png' %}" id="poweredby-white" class="sr-only">
|
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_white.png' %}" id="poweredby-white" class="sr-only">
|
||||||
{% for family, styles in fonts.items %}
|
{% for family, styles in fonts.items %}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends "pretixcontrol/base.html" %}
|
{% extends "pretixcontrol/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
{% load escapejson %}
|
||||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
||||||
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
|
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script>
|
||||||
<p>
|
<p>
|
||||||
<a data-toggle="collapse" href="#no_scan">
|
<a data-toggle="collapse" href="#no_scan">
|
||||||
{% trans "Can't scan the barcode?" %}
|
{% trans "Can't scan the barcode?" %}
|
||||||
@@ -81,9 +82,4 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<script type="text/json" id="qrdata">
|
|
||||||
{{ qrdata|safe }}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
|
{% load escapejson %}
|
||||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||||
@@ -26,9 +27,7 @@
|
|||||||
{% trans "Device registration failed." %}
|
{% trans "Device registration failed." %}
|
||||||
</div>
|
</div>
|
||||||
<script type="text/json" id="webauthn-enroll">
|
<script type="text/json" id="webauthn-enroll">
|
||||||
{{ jsondata|safe }}
|
{{ jsondata|escapejson }}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load escapejson %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="form-signin" id="webauthn-form" action="" method="post">
|
<form class="form-signin" id="webauthn-form" action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
|
|
||||||
{% if jsondata %}
|
{% if jsondata %}
|
||||||
<script type="text/json" id="webauthn-login">
|
<script type="text/json" id="webauthn-login">
|
||||||
{{ jsondata|safe }}
|
{{ jsondata|escapejson }}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{% extends "pretixcontrol/items/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load eventsignal %}
|
||||||
|
{% load eventurl %}
|
||||||
|
{% block title %}{% trans "Change multiple vouchers" %}{% endblock %}
|
||||||
|
{% block inside %}
|
||||||
|
<h1>
|
||||||
|
{% trans "Change multiple vouchers" %}
|
||||||
|
<small>
|
||||||
|
{% blocktrans trimmed with number=vouchers.count %}
|
||||||
|
{{ number }} selected
|
||||||
|
{% endblocktrans %}
|
||||||
|
</small>
|
||||||
|
</h1>
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="hidden">
|
||||||
|
{% for v in vouchers %}
|
||||||
|
<input type="hidden" name="voucher" value="{{ v.pk }}">
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% bootstrap_form_errors form %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Voucher details" %}</legend>
|
||||||
|
{% bootstrap_field form.max_usages layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.valid_until layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.itemvar layout="bulkedit" %}
|
||||||
|
|
||||||
|
<div class="bulk-edit-field-group">
|
||||||
|
<label class="field-toggle">
|
||||||
|
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__price" {% if form.prefix|add:"__price" in bulk_selected %}checked{% endif %}>
|
||||||
|
{% trans "change" context "form_bulk" %}
|
||||||
|
</label>
|
||||||
|
<div class="field-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||||
|
<div class="col-md-5">
|
||||||
|
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
{% bootstrap_field form.value show_label=False form_group_class="" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-9 col-md-offset-3">
|
||||||
|
<div class="controls">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
If you choose "any product" for a specific quota and choose to reserve quota for this
|
||||||
|
voucher above, the product can still be unavailable to the voucher holder if another quota
|
||||||
|
associated with the product is sold out!
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.subevent %}
|
||||||
|
{% bootstrap_field form.subevent layout="bulkedit" %}
|
||||||
|
{% endif %}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{% trans "Advanced settings" %}</legend>
|
||||||
|
{% bootstrap_field form.block_quota layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.allow_ignore_quota layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.min_usages layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.budget addon_after=request.event.currency layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.tag layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.comment layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.show_hidden_items layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.all_addons_included layout="bulkedit" %}
|
||||||
|
{% bootstrap_field form.all_bundles_included layout="bulkedit" %}
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-group submit-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -99,6 +99,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<form action="{% url "control:event.vouchers.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
<form action="{% url "control:event.vouchers.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% for field in filter_form %}
|
||||||
|
{{ field.as_hidden }}
|
||||||
|
{% endfor %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-quotas">
|
<table class="table table-hover table-quotas">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -112,38 +115,50 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<th>
|
<th>
|
||||||
{% trans "Voucher code" %}
|
{% trans "Voucher code" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'code' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' 'code' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Redemptions" %}
|
{% trans "Redemptions" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-redeemed' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' '-redeemed' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'redeemed' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' 'redeemed' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Expiry" %}
|
{% trans "Expiry" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-valid_until' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' '-valid_until' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'valid_until' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' 'valid_until' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Tag" %}
|
{% trans "Tag" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-tag' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' '-tag' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'tag' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' 'tag' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Product" %}
|
{% trans "Product" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-item' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' '-item' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'item' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' 'item' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
{% if request.event.has_subevents %}
|
{% if request.event.has_subevents %}
|
||||||
<th>
|
<th>
|
||||||
{% trans "Date" context "subevent" %}
|
{% trans "Date" context "subevent" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-subevent' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' '-subevent' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'filter-ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if "event.vouchers:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
|
||||||
|
<tr class="table-select-all warning hidden">
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
|
||||||
|
</td>
|
||||||
|
<td colspan="5">
|
||||||
|
<label for="__all">
|
||||||
|
{% trans "Select all results on other pages as well" %}
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for v in vouchers %}
|
{% for v in vouchers %}
|
||||||
@@ -211,6 +226,10 @@
|
|||||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
{% trans "Delete selected" %}
|
{% trans "Delete selected" %}
|
||||||
</button>
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
|
||||||
|
formaction="{% url "control:event.vouchers.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -49,11 +49,11 @@
|
|||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
{% if t.tag %}
|
{% if t.tag %}
|
||||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
|
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
|
||||||
{{ t.tag }}
|
{{ t.tag }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '<>'|urlencode }}">
|
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?filter-tag={{ '<>'|urlencode }}">
|
||||||
{% trans "Empty tag" %}
|
{% trans "Empty tag" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ urlpatterns = [
|
|||||||
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
|
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
|
||||||
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
|
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
|
||||||
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
|
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
|
||||||
|
re_path(r'^vouchers/bulk_edit$', vouchers.VoucherBulkUpdateView.as_view(), name='event.vouchers.bulkedit'),
|
||||||
re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'),
|
re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'),
|
||||||
re_path(r'^vouchers/import/(?P<file>[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'),
|
re_path(r'^vouchers/import/(?P<file>[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'),
|
||||||
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ from pretix.base.forms.auth import (
|
|||||||
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
|
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
|
||||||
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
||||||
from pretix.helpers.http import get_client_ip, redirect_to_url
|
from pretix.helpers.http import get_client_ip, redirect_to_url
|
||||||
|
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||||
from pretix.helpers.security import handle_login_source, session_login
|
from pretix.helpers.security import handle_login_source, session_login
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -243,7 +244,8 @@ def invite(request, token):
|
|||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
if inv.team.members.filter(pk=request.user.pk).exists():
|
if inv.team.members.filter(pk=request.user.pk).exists():
|
||||||
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
|
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
|
||||||
'this team.').format(inv.team.name))
|
'this team. If you want to add a different user or create a new account, '
|
||||||
|
'log out and click the invitation link again.').format(inv.team.name))
|
||||||
return redirect('control:index')
|
return redirect('control:index')
|
||||||
else:
|
else:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@@ -318,19 +320,12 @@ class Forgot(TemplateView):
|
|||||||
if self.form.is_valid():
|
if self.form.is_valid():
|
||||||
email = self.form.cleaned_data['email']
|
email = self.form.cleaned_data['email']
|
||||||
|
|
||||||
has_redis = settings.HAS_REDIS
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(is_active=True, auth_backend='native', email__iexact=email)
|
user = User.objects.get(is_active=True, auth_backend='native', email__iexact=email)
|
||||||
|
|
||||||
if has_redis:
|
if rate_limit("pwreset", user.pk, max_num=1, expire_time=3600 * 24):
|
||||||
from django_redis import get_redis_connection
|
user.log_action('pretix.control.auth.user.forgot_password.denied.repeated')
|
||||||
rc = get_redis_connection("redis")
|
raise RepeatedResetDenied()
|
||||||
if rc.exists('pretix_pwreset_%s' % (user.id)):
|
|
||||||
user.log_action('pretix.control.auth.user.forgot_password.denied.repeated')
|
|
||||||
raise RepeatedResetDenied()
|
|
||||||
else:
|
|
||||||
rc.setex('pretix_pwreset_%s' % (user.id), 3600 * 24, '1')
|
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
|
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
|
||||||
@@ -343,6 +338,7 @@ class Forgot(TemplateView):
|
|||||||
user.log_action('pretix.control.auth.user.forgot_password.mail_sent')
|
user.log_action('pretix.control.auth.user.forgot_password.mail_sent')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
has_redis = settings.HAS_REDIS
|
||||||
if has_redis:
|
if has_redis:
|
||||||
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions. '
|
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions. '
|
||||||
'Please note that we will send at most one email every 24 hours.'))
|
'Please note that we will send at most one email every 24 hours.'))
|
||||||
@@ -411,11 +407,7 @@ class Recover(TemplateView):
|
|||||||
messages.success(request, _('You can now login using your new password.'))
|
messages.success(request, _('You can now login using your new password.'))
|
||||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||||
|
|
||||||
has_redis = settings.HAS_REDIS
|
rate_limit_reset("pwreset", user.pk)
|
||||||
if has_redis:
|
|
||||||
from django_redis import get_redis_connection
|
|
||||||
rc = get_redis_connection("redis")
|
|
||||||
rc.delete('pretix_pwreset_%s' % user.id)
|
|
||||||
return redirect('control:auth.login')
|
return redirect('control:auth.login')
|
||||||
else:
|
else:
|
||||||
return self.get(request, *args, **kwargs)
|
return self.get(request, *args, **kwargs)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ from django.shortcuts import render
|
|||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import conditional_escape, escape, format_html
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _, ngettext, pgettext
|
from django.utils.translation import gettext_lazy as _, ngettext, pgettext
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(tickc), text=_('Attendees (ordered)')),
|
'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(tickc), text=_('Attendees (ordered)')),
|
||||||
'lazy': 'attendees-ordered',
|
'lazy': 'attendees-ordered',
|
||||||
'display_size': 'small',
|
'display_size': 'small',
|
||||||
'priority': 100,
|
'priority': 100,
|
||||||
@@ -122,7 +122,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(paidc), text=_('Attendees (paid)')),
|
'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(paidc), text=_('Attendees (paid)')),
|
||||||
'lazy': 'attendees-paid',
|
'lazy': 'attendees-paid',
|
||||||
'display_size': 'small',
|
'display_size': 'small',
|
||||||
'priority': 100,
|
'priority': 100,
|
||||||
@@ -132,7 +132,8 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content': None if lazy else NUM_WIDGET.format(
|
'content': None if lazy else format_html(
|
||||||
|
NUM_WIDGET,
|
||||||
num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
|
num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
|
||||||
text=_('Total revenue ({currency})').format(currency=sender.currency)
|
text=_('Total revenue ({currency})').format(currency=sender.currency)
|
||||||
),
|
),
|
||||||
@@ -145,7 +146,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content': None if lazy else NUM_WIDGET.format(num=prodc, text=_('Active products')),
|
'content': None if lazy else format_html(NUM_WIDGET, num=prodc, text=_('Active products')),
|
||||||
'lazy': 'active-products',
|
'lazy': 'active-products',
|
||||||
'display_size': 'small',
|
'display_size': 'small',
|
||||||
'priority': 100,
|
'priority': 100,
|
||||||
@@ -209,8 +210,8 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - min(wlt['cnt'], row[1]))
|
quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - min(wlt['cnt'], row[1]))
|
||||||
|
|
||||||
widgets.append({
|
widgets.append({
|
||||||
'content': None if lazy else NUM_WIDGET.format(
|
'content': None if lazy else format_html(
|
||||||
num=intcomma(happy), text=_('available to give to people on waiting list')
|
NUM_WIDGET, num=intcomma(happy), text=_('available to give to people on waiting list')
|
||||||
),
|
),
|
||||||
'lazy': 'waitinglist-avail',
|
'lazy': 'waitinglist-avail',
|
||||||
'priority': 50,
|
'priority': 50,
|
||||||
@@ -220,7 +221,9 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
widgets.append({
|
widgets.append({
|
||||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(wles.count()), text=_('total waiting list length')),
|
'content': None if lazy else format_html(
|
||||||
|
NUM_WIDGET, num=intcomma(wles.count()), text=_('total waiting list length')
|
||||||
|
),
|
||||||
'lazy': 'waitinglist-length',
|
'lazy': 'waitinglist-length',
|
||||||
'display_size': 'small',
|
'display_size': 'small',
|
||||||
'priority': 50,
|
'priority': 50,
|
||||||
@@ -247,9 +250,10 @@ def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
if not lazy:
|
if not lazy:
|
||||||
status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
|
status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
|
||||||
widgets.append({
|
widgets.append({
|
||||||
'content': None if lazy else NUM_WIDGET.format(
|
'content': None if lazy else format_html(
|
||||||
|
NUM_WIDGET,
|
||||||
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
|
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
|
||||||
text=_('{quota} left').format(quota=escape(q.name))
|
text=format_html(_('{quota} left'), quota=q.name)
|
||||||
),
|
),
|
||||||
'lazy': 'quota-{}'.format(q.pk),
|
'lazy': 'quota-{}'.format(q.pk),
|
||||||
'display_size': 'small',
|
'display_size': 'small',
|
||||||
@@ -268,7 +272,8 @@ def shop_state_widget(sender, **kwargs):
|
|||||||
return [{
|
return [{
|
||||||
'display_size': 'small',
|
'display_size': 'small',
|
||||||
'priority': 1000,
|
'priority': 1000,
|
||||||
'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
|
'content': format_html(
|
||||||
|
'<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>',
|
||||||
t1=_('Your ticket shop is'), t2=_('Click here to change'),
|
t1=_('Your ticket shop is'), t2=_('Click here to change'),
|
||||||
state=_('live') if sender.live and not sender.testmode else (
|
state=_('live') if sender.live and not sender.testmode else (
|
||||||
_('live and in test mode') if sender.live else (
|
_('live and in test mode') if sender.live else (
|
||||||
@@ -299,9 +304,10 @@ def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
|
|||||||
qs = sender.checkin_lists.filter(subevent=subevent)
|
qs = sender.checkin_lists.filter(subevent=subevent)
|
||||||
for cl in qs:
|
for cl in qs:
|
||||||
widgets.append({
|
widgets.append({
|
||||||
'content': None if lazy else NUM_WIDGET.format(
|
'content': None if lazy else format_html(
|
||||||
|
NUM_WIDGET,
|
||||||
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
|
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
|
||||||
text=_('Present – {list}').format(list=escape(cl.name))
|
text=format_html(_('Present – {list}'), list=cl.name)
|
||||||
),
|
),
|
||||||
'lazy': 'checkin-{}'.format(cl.pk),
|
'lazy': 'checkin-{}'.format(cl.pk),
|
||||||
'display_size': 'small',
|
'display_size': 'small',
|
||||||
@@ -340,6 +346,12 @@ def welcome_wizard_widget(sender, **kwargs):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
def build_json_response(widgets):
|
||||||
|
for widget in widgets:
|
||||||
|
widget['content'] = conditional_escape(widget['content'])
|
||||||
|
return JsonResponse({'widgets': widgets})
|
||||||
|
|
||||||
|
|
||||||
def event_index(request, organizer, event):
|
def event_index(request, organizer, event):
|
||||||
from pretix.control.forms.event import CommentForm
|
from pretix.control.forms.event import CommentForm
|
||||||
|
|
||||||
@@ -418,7 +430,7 @@ def event_index_widgets_lazy(request, organizer, event):
|
|||||||
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
|
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
|
||||||
widgets.extend(result)
|
widgets.extend(result)
|
||||||
|
|
||||||
return JsonResponse({'widgets': widgets})
|
return build_json_response(widgets)
|
||||||
|
|
||||||
|
|
||||||
def event_index_log_lazy(request, organizer, event):
|
def event_index_log_lazy(request, organizer, event):
|
||||||
@@ -505,7 +517,7 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
|||||||
<a href="{url}" class="event">
|
<a href="{url}" class="event">
|
||||||
<div class="name">{event}</div>
|
<div class="name">{event}</div>
|
||||||
<div class="daterange">{daterange}</div>
|
<div class="daterange">{daterange}</div>
|
||||||
<div class="times">{times}</div>
|
<div class="times">{times}{timezone}</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="bottomrow">
|
<div class="bottomrow">
|
||||||
{orders}
|
{orders}
|
||||||
@@ -549,14 +561,16 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
|||||||
status = ('success', _('On sale'))
|
status = ('success', _('On sale'))
|
||||||
|
|
||||||
widgets.append({
|
widgets.append({
|
||||||
'content': tpl.format(
|
'content': format_html(
|
||||||
|
tpl,
|
||||||
event=escape(event.name),
|
event=escape(event.name),
|
||||||
times=_('Event series') if event.has_subevents else (
|
times=_('Event series') if event.has_subevents else (
|
||||||
((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
|
((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
|
||||||
if event.date_admission and event.date_admission != event.date_from else '')
|
if event.date_admission and event.date_admission != event.date_from else '')
|
||||||
+ (date_format(event.date_from.astimezone(tz), 'TIME_FORMAT') if event.date_from else '')
|
+ (date_format(event.date_from.astimezone(tz), 'TIME_FORMAT') if event.date_from else '')
|
||||||
) + (
|
),
|
||||||
' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>'.format(tzname)
|
timezone=(
|
||||||
|
format_html(' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>', tzname)
|
||||||
if tzname != request.timezone and not event.has_subevents else ''
|
if tzname != request.timezone and not event.has_subevents else ''
|
||||||
),
|
),
|
||||||
url=reverse('control:event.index', kwargs={
|
url=reverse('control:event.index', kwargs={
|
||||||
@@ -564,7 +578,8 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
|||||||
'organizer': event.organizer.slug
|
'organizer': event.organizer.slug
|
||||||
}),
|
}),
|
||||||
orders=(
|
orders=(
|
||||||
'<a href="{orders_url}" class="orders">{orders_text}</a>'.format(
|
format_html(
|
||||||
|
'<a href="{orders_url}" class="orders">{orders_text}</a>',
|
||||||
orders_url=reverse('control:event.orders', kwargs={
|
orders_url=reverse('control:event.orders', kwargs={
|
||||||
'event': event.slug,
|
'event': event.slug,
|
||||||
'organizer': event.organizer.slug
|
'organizer': event.organizer.slug
|
||||||
@@ -631,7 +646,7 @@ def user_index_widgets_lazy(request):
|
|||||||
request.user,
|
request.user,
|
||||||
8
|
8
|
||||||
)
|
)
|
||||||
return JsonResponse({'widgets': widgets})
|
return build_json_response(widgets)
|
||||||
|
|
||||||
|
|
||||||
def user_index(request):
|
def user_index(request):
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import quote, urlsplit
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
@@ -60,7 +60,7 @@ from django.http import (
|
|||||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
|
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import conditional_escape, format_html
|
from django.utils.html import conditional_escape, format_html
|
||||||
@@ -82,7 +82,7 @@ from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
|
|||||||
from pretix.base.models.event import EventMetaValue
|
from pretix.base.models.event import EventMetaValue
|
||||||
from pretix.base.services import tickets
|
from pretix.base.services import tickets
|
||||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import get_defining_app, register_ticket_outputs
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
from pretix.control.forms.event import (
|
from pretix.control.forms.event import (
|
||||||
CancelSettingsForm, CommentForm, ConfirmTextFormset, EventDeleteForm,
|
CancelSettingsForm, CommentForm, ConfirmTextFormset, EventDeleteForm,
|
||||||
@@ -119,8 +119,9 @@ from ...helpers.compat import CompatDeleteView
|
|||||||
from ...helpers.format import (
|
from ...helpers.format import (
|
||||||
PlainHtmlAlternativeString, SafeFormatter, format_map,
|
PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||||
)
|
)
|
||||||
|
from ..forms.filter import LogFilterForm
|
||||||
from ..logdisplay import OVERVIEW_BANLIST
|
from ..logdisplay import OVERVIEW_BANLIST
|
||||||
from . import CreateView, PaginationMixin, UpdateView
|
from . import CreateView, LargeResultSetPaginator, PaginationMixin, UpdateView
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -441,6 +442,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
|||||||
plugins_available = {
|
plugins_available = {
|
||||||
p.module: p for p in self.available_plugins(self.object)
|
p.module: p for p in self.available_plugins(self.object)
|
||||||
}
|
}
|
||||||
|
plugin_enabled = None
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
save_organizer = False
|
save_organizer = False
|
||||||
@@ -490,6 +492,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
|||||||
format_html(_('The plugin {} is now active.'),
|
format_html(_('The plugin {} is now active.'),
|
||||||
format_html("<strong>{}</strong>", pluginmeta.name)),
|
format_html("<strong>{}</strong>", pluginmeta.name)),
|
||||||
]
|
]
|
||||||
|
plugin_enabled = module
|
||||||
messages.success(self.request, mark_safe("".join(info)))
|
messages.success(self.request, mark_safe("".join(info)))
|
||||||
else:
|
else:
|
||||||
self.request.event.log_action('pretix.event.plugins.disabled', user=self.request.user,
|
self.request.event.log_action('pretix.event.plugins.disabled', user=self.request.user,
|
||||||
@@ -499,13 +502,19 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
|||||||
self.object.save()
|
self.object.save()
|
||||||
if save_organizer:
|
if save_organizer:
|
||||||
self.object.organizer.save()
|
self.object.organizer.save()
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url(plugin_enabled))
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self, plugin_enabled) -> str:
|
||||||
return reverse('control:event.settings.plugins', kwargs={
|
if plugin_enabled and self.request.POST.get('go') == 'payment':
|
||||||
'organizer': self.request.organizer.slug,
|
return reverse('control:event.settings.payment', kwargs={
|
||||||
'event': self.request.event.slug,
|
'organizer': self.request.organizer.slug,
|
||||||
})
|
'event': self.request.event.slug,
|
||||||
|
}) + '?highlight=' + quote(plugin_enabled) + '#'
|
||||||
|
else:
|
||||||
|
return reverse('control:event.settings.plugins', kwargs={
|
||||||
|
'organizer': self.request.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||||
@@ -671,6 +680,8 @@ class PaymentSettings(WritePermissionMixin, EventSettingsViewMixin, EventSetting
|
|||||||
p.sales_channels = [sales_channels[channel] for channel in p.settings.get('_restrict_to_sales_channels', as_type=list, default=['web'])]
|
p.sales_channels = [sales_channels[channel] for channel in p.settings.get('_restrict_to_sales_channels', as_type=list, default=['web'])]
|
||||||
if p.is_meta:
|
if p.is_meta:
|
||||||
p.show_enabled = p.settings._enabled in (True, 'True')
|
p.show_enabled = p.settings._enabled in (True, 'True')
|
||||||
|
if self.request.GET.get('highlight') and getattr(get_defining_app(p), 'name', None) == self.request.GET.get('highlight'):
|
||||||
|
p.highlight = True
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -952,7 +963,12 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
|||||||
context=context,
|
context=context,
|
||||||
)
|
)
|
||||||
r = HttpResponse(v, content_type='text/html')
|
r = HttpResponse(v, content_type='text/html')
|
||||||
r._csp_ignore = True
|
r['Content-Security-Policy'] = (
|
||||||
|
# Plugin-provided email templates will contain inline styles or remote images
|
||||||
|
# but emails should not contain JS
|
||||||
|
"style-src 'unsafe-inline'; "
|
||||||
|
"img-src https: data:"
|
||||||
|
)
|
||||||
return r
|
return r
|
||||||
else:
|
else:
|
||||||
raise Http404(_('Unknown email renderer.'))
|
raise Http404(_('Unknown email renderer.'))
|
||||||
@@ -1238,6 +1254,7 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
|||||||
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
template_name = 'pretixcontrol/event/logs.html'
|
template_name = 'pretixcontrol/event/logs.html'
|
||||||
model = LogEntry
|
model = LogEntry
|
||||||
|
paginator_class = LargeResultSetPaginator
|
||||||
context_object_name = 'logs'
|
context_object_name = 'logs'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -1267,32 +1284,20 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
]
|
]
|
||||||
qs = qs.filter(content_type__in=allowed_types)
|
qs = qs.filter(content_type__in=allowed_types)
|
||||||
|
|
||||||
if self.request.GET.get('user') == 'yes':
|
if self.filter_form.is_valid():
|
||||||
qs = qs.filter(user__isnull=False)
|
qs = self.filter_form.filter_qs(qs)
|
||||||
elif self.request.GET.get('user') == 'no':
|
|
||||||
qs = qs.filter(user__isnull=True)
|
|
||||||
elif self.request.GET.get('user', '').startswith('d-'):
|
|
||||||
qs = qs.filter(device_id=self.request.GET.get('user')[2:])
|
|
||||||
elif self.request.GET.get('user'):
|
|
||||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
|
||||||
|
|
||||||
if self.request.GET.get('action_type'):
|
|
||||||
qs = qs.filter(action_type=self.request.GET['action_type'])
|
|
||||||
|
|
||||||
if self.request.GET.get('content_type'):
|
|
||||||
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
|
|
||||||
|
|
||||||
if self.request.GET.get('object'):
|
|
||||||
qs = qs.filter(object_id=self.request.GET.get('object'))
|
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
|
ctx['filter_form'] = self.filter_form
|
||||||
ctx['devicelist'] = self.request.event.logentry_set.order_by('device__name').distinct().values('device__id', 'device__name')
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
|
||||||
|
|
||||||
|
|
||||||
class EventComment(EventPermissionRequiredMixin, View):
|
class EventComment(EventPermissionRequiredMixin, View):
|
||||||
permission = 'event.settings.general:write'
|
permission = 'event.settings.general:write'
|
||||||
@@ -1428,11 +1433,16 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
|
|||||||
form.instance.custom_rules = json.dumps([
|
form.instance.custom_rules = json.dumps([
|
||||||
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
||||||
], cls=I18nJSONEncoder)
|
], cls=I18nJSONEncoder)
|
||||||
if form.has_changed():
|
if form.has_changed() or self.formset.has_changed():
|
||||||
|
change_data = {
|
||||||
|
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||||
|
}
|
||||||
|
if self.formset.has_changed():
|
||||||
|
change_data["custom_rules"] = [
|
||||||
|
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
||||||
|
]
|
||||||
self.object.log_action(
|
self.object.log_action(
|
||||||
'pretix.event.taxrule.changed', user=self.request.user, data={
|
'pretix.event.taxrule.changed', user=self.request.user, data=change_data
|
||||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from django.utils.translation import ngettext
|
|||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
from pretix.base.middleware import add_to_response_csp
|
||||||
from pretix.base.models import OutgoingMail
|
from pretix.base.models import OutgoingMail
|
||||||
from pretix.base.services.mail import mail_send_task
|
from pretix.base.services.mail import mail_send_task
|
||||||
from pretix.control.forms.filter import OutgoingMailFilterForm
|
from pretix.control.forms.filter import OutgoingMailFilterForm
|
||||||
@@ -108,18 +108,12 @@ class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequir
|
|||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
if 'Content-Security-Policy' in response:
|
add_to_response_csp(response, {
|
||||||
h = _parse_csp(response['Content-Security-Policy'])
|
|
||||||
else:
|
|
||||||
h = {}
|
|
||||||
csps = {
|
|
||||||
'frame-src': ['data:'],
|
'frame-src': ['data:'],
|
||||||
# Unfortuantely, we can't avoid unsafe-inline for style here.
|
# Unfortuantely, we can't avoid unsafe-inline for style here.
|
||||||
# See outgoingmail.js for the protection measures we take.
|
# See outgoingmail.js for the protection measures we take.
|
||||||
'style-src': ["'unsafe-inline'"],
|
'style-src': ["'unsafe-inline'"],
|
||||||
}
|
})
|
||||||
_merge_csp(h, csps)
|
|
||||||
response['Content-Security-Policy'] = _render_csp(h)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|||||||
@@ -41,6 +41,30 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cname_record(hostname):
|
||||||
|
try:
|
||||||
|
r = dns.resolver.Resolver()
|
||||||
|
answers = r.resolve(hostname, 'CNAME')
|
||||||
|
answers = list(answers)
|
||||||
|
if len(answers) != 1:
|
||||||
|
logger.exception('Found multiple CNAME records for {}'.format(hostname))
|
||||||
|
return
|
||||||
|
return str(answers[0].target).lower()
|
||||||
|
except:
|
||||||
|
logger.exception('Could not fetch CNAME record for {}'.format(hostname))
|
||||||
|
|
||||||
|
|
||||||
|
def get_dmarc_record(hostname):
|
||||||
|
try:
|
||||||
|
r = dns.resolver.Resolver()
|
||||||
|
for resp in r.resolve("_dmarc." + hostname, 'TXT'):
|
||||||
|
data = b''.join(resp.strings).decode()
|
||||||
|
if 'DMARC1' in data.strip():
|
||||||
|
return data
|
||||||
|
except:
|
||||||
|
logger.exception("Could not fetch DMARC record for {}".format(hostname))
|
||||||
|
|
||||||
|
|
||||||
def get_spf_record(hostname):
|
def get_spf_record(hostname):
|
||||||
try:
|
try:
|
||||||
r = dns.resolver.Resolver()
|
r = dns.resolver.Resolver()
|
||||||
@@ -49,7 +73,7 @@ def get_spf_record(hostname):
|
|||||||
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
|
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
|
||||||
return data
|
return data
|
||||||
except:
|
except:
|
||||||
logger.exception("Could not fetch SPF record")
|
logger.exception("Could not fetch SPF record for {}".format(hostname))
|
||||||
|
|
||||||
|
|
||||||
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
|
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
|
||||||
@@ -168,10 +192,15 @@ class MailSettingsSetupView(TemplateView):
|
|||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
|
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
|
||||||
|
verify_dns = (
|
||||||
|
settings.MAIL_CUSTOM_SENDER_SPF_STRING or
|
||||||
|
(settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR) or
|
||||||
|
settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED
|
||||||
|
)
|
||||||
allow_save = (
|
allow_save = (
|
||||||
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
|
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
|
||||||
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
|
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
|
||||||
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
|
(not verify_dns or self.request.POST.get('state') == 'save')
|
||||||
)
|
)
|
||||||
|
|
||||||
if allow_save:
|
if allow_save:
|
||||||
@@ -192,8 +221,8 @@ class MailSettingsSetupView(TemplateView):
|
|||||||
|
|
||||||
spf_warning = None
|
spf_warning = None
|
||||||
spf_record = None
|
spf_record = None
|
||||||
|
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
|
||||||
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
|
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
|
||||||
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
|
|
||||||
spf_record = get_spf_record(hostname)
|
spf_record = get_spf_record(hostname)
|
||||||
if not spf_record:
|
if not spf_record:
|
||||||
spf_warning = _(
|
spf_warning = _(
|
||||||
@@ -210,7 +239,43 @@ class MailSettingsSetupView(TemplateView):
|
|||||||
'this system in the SPF record.'
|
'this system in the SPF record.'
|
||||||
)
|
)
|
||||||
|
|
||||||
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
|
dkim_warning = None
|
||||||
|
dkim_hostname = None
|
||||||
|
dkim_cname = None
|
||||||
|
if settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR:
|
||||||
|
dkim_hostname = settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR + '._domainkey.' + hostname
|
||||||
|
cname_target = get_cname_record(dkim_hostname)
|
||||||
|
dkim_cname = settings.MAIL_CUSTOM_SENDER_DKIM_CNAME
|
||||||
|
if not dkim_cname.endswith("."):
|
||||||
|
dkim_cname += "."
|
||||||
|
if "%s" in dkim_cname:
|
||||||
|
dkim_cname = dkim_cname.replace("%s", hostname.replace(".", "-").lower())
|
||||||
|
if not cname_target:
|
||||||
|
dkim_warning = _(
|
||||||
|
'We could not find a CNAME record pointing to our DKIM key for domain you are trying to use. '
|
||||||
|
'This means that there is a very high change most of the emails will be rejected or marked as '
|
||||||
|
'spam. We strongly recommend setting up DKIM through a CNAME record. You can do so through the '
|
||||||
|
'DNS settings at the provider you registered your domain with.'
|
||||||
|
)
|
||||||
|
elif cname_target != dkim_cname:
|
||||||
|
dkim_warning = _(
|
||||||
|
'We found a CNAME record for a DKIM key, but it is not pointing to the right location. '
|
||||||
|
'This means that there is a very high chance most of the emails will be rejected or marked as '
|
||||||
|
'spam. You should update the DNS settings of your domain.'
|
||||||
|
)
|
||||||
|
|
||||||
|
dmarc_warning = None
|
||||||
|
dmarc_record = None
|
||||||
|
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
|
||||||
|
dmarc_record = get_dmarc_record(hostname)
|
||||||
|
if not dmarc_record:
|
||||||
|
spf_warning = _(
|
||||||
|
'We did not find DMARC record for your domain. This means that there is a very high chance '
|
||||||
|
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
|
||||||
|
'of your domain.'
|
||||||
|
)
|
||||||
|
|
||||||
|
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning and not dkim_warning and not dmarc_warning
|
||||||
if verification:
|
if verification:
|
||||||
if 'verification' in self.request.POST:
|
if 'verification' in self.request.POST:
|
||||||
messages.error(request, _('The verification code was incorrect, please try again.'))
|
messages.error(request, _('The verification code was incorrect, please try again.'))
|
||||||
@@ -241,6 +306,12 @@ class MailSettingsSetupView(TemplateView):
|
|||||||
'spf_warning': spf_warning,
|
'spf_warning': spf_warning,
|
||||||
'spf_record': spf_record,
|
'spf_record': spf_record,
|
||||||
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
|
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
|
||||||
|
'dkim_warning': dkim_warning,
|
||||||
|
'dkim_hostname': dkim_hostname,
|
||||||
|
'dkim_cname': dkim_cname,
|
||||||
|
'dmarc_warning': dmarc_warning,
|
||||||
|
'dmarc_record': dmarc_record,
|
||||||
|
'hostname': hostname,
|
||||||
'recp': self.simple_form.cleaned_data.get('mail_from')
|
'recp': self.simple_form.cleaned_data.get('mail_from')
|
||||||
},
|
},
|
||||||
using=self.template_engine,
|
using=self.template_engine,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ from django.urls import reverse
|
|||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.formats import date_format, get_format
|
from django.utils.formats import date_format, get_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import conditional_escape, escape
|
from django.utils.html import conditional_escape, escape, format_html
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
@@ -79,7 +79,7 @@ from pretix.base.email import get_email_context
|
|||||||
from pretix.base.exporter import MultiSheetListExporter
|
from pretix.base.exporter import MultiSheetListExporter
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, CachedTicket, Checkin, Invoice, InvoiceAddress, Item,
|
CachedFile, CachedTicket, Checkin, GiftCard, Invoice, InvoiceAddress, Item,
|
||||||
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
|
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
|
||||||
ScheduledEventExport, generate_secret,
|
ScheduledEventExport, generate_secret,
|
||||||
)
|
)
|
||||||
@@ -551,10 +551,10 @@ class OrderDetail(OrderView):
|
|||||||
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
||||||
for p in ctx['payments']:
|
for p in ctx['payments']:
|
||||||
if p.payment_provider:
|
if p.payment_provider:
|
||||||
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
|
p.html_info = p.payment_provider.payment_control_render(self.request, p) or ""
|
||||||
for r in ctx['refunds']:
|
for r in ctx['refunds']:
|
||||||
if r.payment_provider:
|
if r.payment_provider:
|
||||||
r.html_info = (r.payment_provider.refund_control_render(self.request, r) or "").strip()
|
r.html_info = r.payment_provider.refund_control_render(self.request, r) or ""
|
||||||
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
||||||
ctx['comment_form'] = CommentForm(initial={
|
ctx['comment_form'] = CommentForm(initial={
|
||||||
'comment': self.order.comment,
|
'comment': self.order.comment,
|
||||||
@@ -1994,7 +1994,7 @@ class OrderChange(OrderView):
|
|||||||
positions = list(self.order.positions.select_related(
|
positions = list(self.order.positions.select_related(
|
||||||
'item', 'item__tax_rule', 'used_membership', 'used_membership__membership_type', 'tax_rule',
|
'item', 'item__tax_rule', 'used_membership', 'used_membership__membership_type', 'tax_rule',
|
||||||
'seat', 'subevent',
|
'seat', 'subevent',
|
||||||
).prefetch_related('granted_memberships'))
|
).prefetch_related('granted_memberships', 'issued_gift_cards'))
|
||||||
for p in positions:
|
for p in positions:
|
||||||
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p, items=self.items,
|
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p, items=self.items,
|
||||||
initial={'seat': p.seat.seat_guid if p.seat else None},
|
initial={'seat': p.seat.seat_guid if p.seat else None},
|
||||||
@@ -2261,6 +2261,12 @@ class OrderContactChange(OrderView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['form'] = self.form
|
ctx['form'] = self.form
|
||||||
|
if self.order.all_positions.filter(Exists(GiftCard.objects.filter(issued_in=OuterRef('pk')))).exists():
|
||||||
|
self.form.fields['regenerate_secrets'].help_text = format_html(
|
||||||
|
'{}<br><br><strong><span class="fa fa-warning"></span> {}</strong>',
|
||||||
|
self.form.fields['regenerate_secrets'].help_text,
|
||||||
|
_("Ticket secrets of order positions that have been used to issue a gift card can not be changed. Only the link will be changed in this case."),
|
||||||
|
)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ from pretix.base.views.tasks import AsyncAction
|
|||||||
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
||||||
from pretix.control.forms.filter import (
|
from pretix.control.forms.filter import (
|
||||||
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
|
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
|
||||||
OrganizerFilterForm, ReusableMediaFilterForm, TeamFilterForm,
|
LogFilterForm, OrganizerFilterForm, ReusableMediaFilterForm,
|
||||||
|
TeamFilterForm,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.orders import ExporterForm
|
from pretix.control.forms.orders import ExporterForm
|
||||||
from pretix.control.forms.organizer import (
|
from pretix.control.forms.organizer import (
|
||||||
@@ -133,7 +134,7 @@ from pretix.control.permissions import (
|
|||||||
organizer_permission_required,
|
organizer_permission_required,
|
||||||
)
|
)
|
||||||
from pretix.control.signals import nav_organizer
|
from pretix.control.signals import nav_organizer
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import LargeResultSetPaginator, PaginationMixin
|
||||||
from pretix.control.views.mailsetup import MailSettingsSetupView
|
from pretix.control.views.mailsetup import MailSettingsSetupView
|
||||||
from pretix.helpers import OF_SELF, GroupConcat
|
from pretix.helpers import OF_SELF, GroupConcat
|
||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
@@ -2663,6 +2664,7 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
template_name = 'pretixcontrol/organizers/logs.html'
|
template_name = 'pretixcontrol/organizers/logs.html'
|
||||||
permission = 'organizer.settings.general:write'
|
permission = 'organizer.settings.general:write'
|
||||||
model = LogEntry
|
model = LogEntry
|
||||||
|
paginator_class = LargeResultSetPaginator
|
||||||
context_object_name = 'logs'
|
context_object_name = 'logs'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -2672,16 +2674,21 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
||||||
).order_by('-datetime')
|
).order_by('-datetime')
|
||||||
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
||||||
if self.request.GET.get('action_type'):
|
|
||||||
qs = qs.filter(action_type=self.request.GET['action_type'])
|
if self.filter_form.is_valid():
|
||||||
if self.request.GET.get('user'):
|
qs = self.filter_form.filter_qs(qs)
|
||||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
|
ctx['filter_form'] = self.filter_form
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
|
||||||
|
|
||||||
|
|
||||||
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||||
model = MembershipType
|
model = MembershipType
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
if 'placeholders' in request.GET:
|
if 'placeholders' in request.GET:
|
||||||
return self.get_placeholders_help(request)
|
return self.get_placeholders_help(request)
|
||||||
resp = super().get(request, *args, **kwargs)
|
resp = super().get(request, *args, **kwargs)
|
||||||
resp._csp_ignore = True
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def get_placeholders_help(self, request):
|
def get_placeholders_help(self, request):
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ from pretix.control.views import PaginationMixin
|
|||||||
from pretix.control.views.event import MetaDataEditorMixin
|
from pretix.control.views.event import MetaDataEditorMixin
|
||||||
from pretix.helpers import GroupConcat
|
from pretix.helpers import GroupConcat
|
||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
|
from pretix.helpers.i18n import get_format_without_seconds
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
|
|
||||||
|
|
||||||
@@ -878,7 +879,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
|
|||||||
ctx['rrule_formset'] = self.rrule_formset
|
ctx['rrule_formset'] = self.rrule_formset
|
||||||
ctx['time_formset'] = self.time_formset
|
ctx['time_formset'] = self.time_formset
|
||||||
|
|
||||||
tf = get_format('TIME_INPUT_FORMATS')[0]
|
tf = get_format_without_seconds('TIME_INPUT_FORMATS')
|
||||||
ctx['time_admission_sample'] = time(8, 30, 0).strftime(tf)
|
ctx['time_admission_sample'] = time(8, 30, 0).strftime(tf)
|
||||||
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
|
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
|
||||||
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
|
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ from pretix.control.permissions import (
|
|||||||
)
|
)
|
||||||
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
|
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
|
||||||
from pretix.helpers.http import redirect_to_url
|
from pretix.helpers.http import redirect_to_url
|
||||||
|
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||||
from pretix.helpers.security import session_reauth
|
from pretix.helpers.security import session_reauth
|
||||||
from pretix.helpers.u2f import websafe_encode
|
from pretix.helpers.u2f import websafe_encode
|
||||||
|
|
||||||
@@ -849,6 +850,7 @@ class UserPasswordChangeView(FormView):
|
|||||||
msgs = []
|
msgs = []
|
||||||
msgs.append(_('Your password has been changed.'))
|
msgs.append(_('Your password has been changed.'))
|
||||||
self.request.user.send_security_notice(msgs)
|
self.request.user.send_security_notice(msgs)
|
||||||
|
rate_limit_reset("pwreset", self.request.user.pk)
|
||||||
|
|
||||||
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={'new_pw': True})
|
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={'new_pw': True})
|
||||||
|
|
||||||
@@ -879,6 +881,7 @@ class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
**super().get_form_kwargs(),
|
**super().get_form_kwargs(),
|
||||||
|
"request": self.request,
|
||||||
"user": self.request.user,
|
"user": self.request.user,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,6 +911,10 @@ class UserEmailVerifyView(View):
|
|||||||
messages.success(self.request, _('Your email address was already verified.'))
|
messages.success(self.request, _('Your email address was already verified.'))
|
||||||
return redirect(reverse('control:user.settings', kwargs={}))
|
return redirect(reverse('control:user.settings', kwargs={}))
|
||||||
|
|
||||||
|
if rate_limit("emailverify", self.request.user.pk, max_num=2, expire_time=300):
|
||||||
|
messages.error(self.request, _("For security reasons, please wait 5 minutes before you try again."))
|
||||||
|
return redirect(reverse('control:user.settings', kwargs={}))
|
||||||
|
|
||||||
self.request.user.send_confirmation_code(
|
self.request.user.send_confirmation_code(
|
||||||
session=self.request.session,
|
session=self.request.session,
|
||||||
reason='email_verify',
|
reason='email_verify',
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ import bleach
|
|||||||
from defusedcsv import csv
|
from defusedcsv import csv
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import (
|
||||||
|
BadRequest, PermissionDenied, ValidationError,
|
||||||
|
)
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import Exists, OuterRef, Sum
|
from django.db.models import Count, Exists, OuterRef, Sum
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
@@ -55,7 +57,7 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
CreateView, ListView, TemplateView, UpdateView, View,
|
CreateView, FormView, ListView, TemplateView, UpdateView, View,
|
||||||
)
|
)
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
@@ -70,7 +72,9 @@ from pretix.base.services.vouchers import vouchers_send
|
|||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
from pretix.base.views.tasks import AsyncFormView
|
from pretix.base.views.tasks import AsyncFormView
|
||||||
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
||||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
from pretix.control.forms.vouchers import (
|
||||||
|
VoucherBulkEditForm, VoucherBulkForm, VoucherForm,
|
||||||
|
)
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from pretix.control.signals import voucher_form_class
|
from pretix.control.signals import voucher_form_class
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import PaginationMixin
|
||||||
@@ -80,7 +84,37 @@ from pretix.helpers.models import modelcopy
|
|||||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||||
|
|
||||||
|
|
||||||
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
class VoucherQueryMixin:
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def request_data(self):
|
||||||
|
if self.request.method == "POST":
|
||||||
|
return self.request.POST
|
||||||
|
return self.request.GET
|
||||||
|
|
||||||
|
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = self.request.event.vouchers.exclude(
|
||||||
|
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
|
||||||
|
)
|
||||||
|
if 'voucher' in self.request_data and '__ALL' not in self.request_data:
|
||||||
|
qs = qs.filter(
|
||||||
|
id__in=self.request_data.getlist('voucher')
|
||||||
|
)
|
||||||
|
elif self.request.method == 'GET' or '__ALL' in self.request_data:
|
||||||
|
if self.filter_form.is_valid():
|
||||||
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
else:
|
||||||
|
raise BadRequest("No vouchers selected")
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return VoucherFilterForm(data=self.request_data, prefix='filter', event=self.request.event)
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||||
model = Voucher
|
model = Voucher
|
||||||
context_object_name = 'vouchers'
|
context_object_name = 'vouchers'
|
||||||
template_name = 'pretixcontrol/vouchers/index.html'
|
template_name = 'pretixcontrol/vouchers/index.html'
|
||||||
@@ -88,25 +122,15 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
|||||||
|
|
||||||
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Voucher.annotate_budget_used(self.request.event.vouchers.exclude(
|
return Voucher.annotate_budget_used(super().get_queryset().select_related(
|
||||||
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
|
|
||||||
).select_related(
|
|
||||||
'item', 'variation', 'seat'
|
'item', 'variation', 'seat'
|
||||||
))
|
))
|
||||||
if self.filter_form.is_valid():
|
|
||||||
qs = self.filter_form.filter_qs(qs)
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['filter_form'] = self.filter_form
|
ctx['filter_form'] = self.filter_form
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def filter_form(self):
|
|
||||||
return VoucherFilterForm(data=self.request.GET, event=self.request.event)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.GET.get("download", "") == "yes":
|
if request.GET.get("download", "") == "yes":
|
||||||
return self._download_csv()
|
return self._download_csv()
|
||||||
@@ -293,6 +317,12 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
f.disabled = True
|
f.disabled = True
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
return {
|
||||||
|
**super().get_form_kwargs(),
|
||||||
|
"event": self.request.event,
|
||||||
|
}
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> VoucherForm:
|
def get_object(self, queryset=None) -> VoucherForm:
|
||||||
url = resolve(self.request.path_info)
|
url = resolve(self.request.path_info)
|
||||||
try:
|
try:
|
||||||
@@ -603,26 +633,21 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class VoucherBulkAction(EventPermissionRequiredMixin, View):
|
class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
|
||||||
permission = 'event.vouchers:write'
|
permission = 'event.vouchers:write'
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def objects(self):
|
|
||||||
return self.request.event.vouchers.filter(
|
|
||||||
id__in=self.request.POST.getlist('voucher')
|
|
||||||
)
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if request.POST.get('action') == 'delete':
|
if request.POST.get('action') == 'delete':
|
||||||
return render(request, 'pretixcontrol/vouchers/delete_bulk.html', {
|
return render(request, 'pretixcontrol/vouchers/delete_bulk.html', {
|
||||||
'allowed': self.objects.filter(redeemed=0),
|
'allowed': self.get_queryset().filter(redeemed=0),
|
||||||
'forbidden': self.objects.exclude(redeemed=0),
|
'forbidden': self.get_queryset().exclude(redeemed=0),
|
||||||
})
|
})
|
||||||
elif request.POST.get('action') == 'delete_confirm':
|
elif request.POST.get('action') == 'delete_confirm':
|
||||||
log_entries = []
|
log_entries = []
|
||||||
to_delete = []
|
to_delete = []
|
||||||
for obj in self.objects:
|
to_update = []
|
||||||
|
for obj in self.get_queryset():
|
||||||
if obj.allow_delete():
|
if obj.allow_delete():
|
||||||
log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
|
log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
|
||||||
to_delete.append(obj.pk)
|
to_delete.append(obj.pk)
|
||||||
@@ -632,12 +657,14 @@ class VoucherBulkAction(EventPermissionRequiredMixin, View):
|
|||||||
'bulk': True
|
'bulk': True
|
||||||
}, save=False))
|
}, save=False))
|
||||||
obj.max_usages = min(obj.redeemed, obj.max_usages)
|
obj.max_usages = min(obj.redeemed, obj.max_usages)
|
||||||
obj.save(update_fields=['max_usages'])
|
to_update.append(obj)
|
||||||
|
|
||||||
if to_delete:
|
if to_delete:
|
||||||
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
|
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
|
||||||
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
|
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
|
||||||
Voucher.objects.filter(pk__in=to_delete).delete()
|
Voucher.objects.filter(pk__in=to_delete).delete()
|
||||||
|
if to_update:
|
||||||
|
Voucher.objects.bulk_update(to_update, ['max_usages'])
|
||||||
|
|
||||||
LogEntry.bulk_create_and_postprocess(log_entries)
|
LogEntry.bulk_create_and_postprocess(log_entries)
|
||||||
messages.success(request, _('The selected vouchers have been deleted or disabled.'))
|
messages.success(request, _('The selected vouchers have been deleted or disabled.'))
|
||||||
@@ -648,3 +675,117 @@ class VoucherBulkAction(EventPermissionRequiredMixin, View):
|
|||||||
'organizer': self.request.event.organizer.slug,
|
'organizer': self.request.event.organizer.slug,
|
||||||
'event': self.request.event.slug,
|
'event': self.request.event.slug,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, FormView):
|
||||||
|
template_name = 'pretixcontrol/vouchers/bulk_edit.html'
|
||||||
|
permission = 'event.vouchers:write'
|
||||||
|
context_object_name = 'vouchers'
|
||||||
|
form_class = VoucherBulkEditForm
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().prefetch_related(None).order_by()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return HttpResponse(status=405)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_submitted(self):
|
||||||
|
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
|
||||||
|
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
|
||||||
|
# that behavior
|
||||||
|
return '_bulk' in self.request.POST
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
initial = {}
|
||||||
|
mixed_values = set()
|
||||||
|
qs = self.get_queryset().annotate()
|
||||||
|
|
||||||
|
fields = (
|
||||||
|
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', 'max_usages',
|
||||||
|
'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included', 'all_bundles_included',
|
||||||
|
'budget',
|
||||||
|
)
|
||||||
|
for f in fields:
|
||||||
|
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
|
||||||
|
if len(existing_values) == 1:
|
||||||
|
initial[f] = existing_values[0][f]
|
||||||
|
elif len(existing_values) > 1:
|
||||||
|
mixed_values.add(f)
|
||||||
|
if f == "max_usages":
|
||||||
|
initial[f] = 1
|
||||||
|
else:
|
||||||
|
initial[f] = None
|
||||||
|
|
||||||
|
existing_values = list(qs.order_by("item", "variation", "quota").values("item", "variation", "quota").annotate(c=Count('*')))
|
||||||
|
if len(existing_values) == 1:
|
||||||
|
i = existing_values[0]
|
||||||
|
if i["quota"]:
|
||||||
|
initial["itemvar"] = f'q-{i["quota"]}'
|
||||||
|
elif i["variation"]:
|
||||||
|
initial["itemvar"] = f'{i["item"]}-{i["variation"]}'
|
||||||
|
elif i["item"]:
|
||||||
|
initial["itemvar"] = f'{i["item"]}'
|
||||||
|
else:
|
||||||
|
initial["itemvar"] = None
|
||||||
|
elif len(existing_values) > 1:
|
||||||
|
mixed_values.add("itemvar")
|
||||||
|
initial["itemvar"] = None
|
||||||
|
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['event'] = self.request.event
|
||||||
|
kwargs['prefix'] = 'bulkedit'
|
||||||
|
kwargs['initial'] = initial
|
||||||
|
kwargs['queryset'] = self.get_queryset()
|
||||||
|
kwargs['mixed_values'] = mixed_values
|
||||||
|
if not self.is_submitted:
|
||||||
|
kwargs['data'] = None
|
||||||
|
kwargs['files'] = None
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('control:event.vouchers', kwargs={
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
log_entries = []
|
||||||
|
|
||||||
|
# Main form
|
||||||
|
form.save()
|
||||||
|
data = {
|
||||||
|
k: v
|
||||||
|
for k, v in form.cleaned_data.items()
|
||||||
|
if k in form.changed_data
|
||||||
|
}
|
||||||
|
data['_raw_bulk_data'] = self.request.POST.dict()
|
||||||
|
for obj in self.get_queryset():
|
||||||
|
log_entries.append(
|
||||||
|
obj.log_action('pretix.voucher.changed', data=data, user=self.request.user, save=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
LogEntry.bulk_create_and_postprocess(log_entries)
|
||||||
|
|
||||||
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['vouchers'] = self.get_queryset()
|
||||||
|
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
form = self.get_form()
|
||||||
|
is_valid = (
|
||||||
|
self.is_submitted and
|
||||||
|
form.is_valid()
|
||||||
|
)
|
||||||
|
if is_valid:
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
if self.is_submitted:
|
||||||
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import itertools
|
||||||
import re
|
import re
|
||||||
|
from http.cookies import Morsel
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@@ -48,6 +50,41 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
|
|||||||
response.cookies[key]['Partitioned'] = True
|
response.cookies[key]['Partitioned'] = True
|
||||||
|
|
||||||
|
|
||||||
|
def thoroughly_delete_cookie(response, cookie_name, **kwargs):
|
||||||
|
""" Deletes different possible versions of a cookie (SameSite, Partitioned) """
|
||||||
|
properties = {"SameSite": ["", 'None'], "Partitioned": ["", True]}
|
||||||
|
for i, values in enumerate(itertools.product(*properties.values())):
|
||||||
|
m = Morsel()
|
||||||
|
m.set(cookie_name, '', '')
|
||||||
|
m.update(kwargs)
|
||||||
|
m.update(zip(properties.keys(), values))
|
||||||
|
m['expires'] = "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||||
|
|
||||||
|
response.cookies[f'___DELETECOOKIE__{i}___{cookie_name}'] = m
|
||||||
|
|
||||||
|
# Make sure settings a cookie afterwards will add a new item in the dictionary, placing
|
||||||
|
# it below our deletion headers.
|
||||||
|
response.cookies.pop(cookie_name, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_values_of_cookie(cookie_header, cookie_name):
|
||||||
|
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
|
||||||
|
values = list()
|
||||||
|
if not cookie_header:
|
||||||
|
return values
|
||||||
|
for chunk in cookie_header.split(";"):
|
||||||
|
if "=" in chunk:
|
||||||
|
key, val = chunk.split("=", 1)
|
||||||
|
else:
|
||||||
|
# Assume an empty name per
|
||||||
|
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
|
||||||
|
key, val = "", chunk
|
||||||
|
key, val = key.strip(), val.strip()
|
||||||
|
if key == cookie_name:
|
||||||
|
values.append(val)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
|
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
|
||||||
# Copyright 2019 Google LLC.
|
# Copyright 2019 Google LLC.
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|||||||
@@ -47,5 +47,5 @@ def escapejson(value):
|
|||||||
|
|
||||||
@keep_lazy(str, SafeText)
|
@keep_lazy(str, SafeText)
|
||||||
def escapejson_attr(value):
|
def escapejson_attr(value):
|
||||||
"""Hex encodes characters for use in a html attributw script."""
|
"""Hex encodes characters for use in a html attribute."""
|
||||||
return mark_safe(force_str(value).translate(_json_escapes_attr))
|
return mark_safe(force_str(value).translate(_json_escapes_attr))
|
||||||
|
|||||||
@@ -20,9 +20,11 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.fields import DecimalField
|
||||||
|
|
||||||
|
|
||||||
class Thumbnail(models.Model):
|
class Thumbnail(models.Model):
|
||||||
@@ -54,3 +56,33 @@ def flatten_choices(choices):
|
|||||||
yield from label_or_nested
|
yield from label_or_nested
|
||||||
else:
|
else:
|
||||||
yield value_or_group, label_or_nested
|
yield value_or_group, label_or_nested
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_decimal(d: Decimal) -> Decimal:
|
||||||
|
"""
|
||||||
|
Strips trailing zeros, e.g.
|
||||||
|
|
||||||
|
20.000 → 20
|
||||||
|
20.010 → 20.01
|
||||||
|
20.100 → 20.1
|
||||||
|
|
||||||
|
But unlike of Decimal.normalize(), 20.000 will not become 2e+1. Very small decimals might still be represented
|
||||||
|
in scientific notation when printed.
|
||||||
|
"""
|
||||||
|
normalized = d.normalize()
|
||||||
|
sign, digit, exponent = normalized.as_tuple()
|
||||||
|
if exponent > 0:
|
||||||
|
return normalized.quantize(1)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
class NormalizedDecimalField(DecimalField):
|
||||||
|
"""
|
||||||
|
Variant of DecimalField that never outputs the trailing zeros, so we always have normalized decimals internally.
|
||||||
|
Use this only for fields where the trailing zeros are pointless (e.g. percentages), not for monetary amounts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection):
|
||||||
|
if value is not None:
|
||||||
|
value = _normalize_decimal(value)
|
||||||
|
return value
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import ipaddress
|
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
@@ -42,8 +41,7 @@ from urllib3.util.connection import (
|
|||||||
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
||||||
|
|
||||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||||
|
from pretix.helpers.ssrf import should_block_access
|
||||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_vobject_performance():
|
def monkeypatch_vobject_performance():
|
||||||
@@ -150,16 +148,9 @@ def monkeypatch_urllib3_ssrf_protection():
|
|||||||
af, socktype, proto, canonname, sa = res
|
af, socktype, proto, canonname, sa = res
|
||||||
|
|
||||||
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||||
ip_addr = ipaddress.ip_address(sa[0])
|
is_private, msg = should_block_access(sa)
|
||||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
if is_private:
|
||||||
if ip_addr.is_multicast:
|
raise HTTPError(msg)
|
||||||
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
|
|
||||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
|
||||||
raise HTTPError(f"Request to local address {sa[0]} blocked")
|
|
||||||
if ip_addr.is_private:
|
|
||||||
raise HTTPError(f"Request to private address {sa[0]} blocked")
|
|
||||||
if check_ip4 in _cgnat_net:
|
|
||||||
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
|
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import hashlib
|
||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from pretix.helpers.http import get_client_ip
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_key(key, parameters):
|
||||||
|
return f'pretix:ratelimit:{key}:' + hashlib.sha256(','.join(str(p) for p in parameters).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ip(request):
|
||||||
|
client_ip = get_client_ip(request)
|
||||||
|
if not client_ip:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
client_ip = ipaddress.ip_address(client_ip)
|
||||||
|
except ValueError:
|
||||||
|
# Web server not set up correctly
|
||||||
|
return None
|
||||||
|
if client_ip.is_private and not settings.DEBUG:
|
||||||
|
# This is the private IP of the server, web server not set up correctly
|
||||||
|
return None
|
||||||
|
return str(client_ip)
|
||||||
|
|
||||||
|
|
||||||
|
def rate_limit(key: str, *parameters, include_ip_from_request: HttpRequest=None, max_num: int, expire_time: int, increase: bool = True):
|
||||||
|
"""
|
||||||
|
This is a shared utility to implement simple rate limiting in operations like
|
||||||
|
password resets.
|
||||||
|
|
||||||
|
:param key: The key referring to the feature like "pwreset"
|
||||||
|
:param parameters: Any number of things to be hashed as the bucket key
|
||||||
|
:param include_ip_from_request: Add IP address from request to the bucket key. If IP address cannot be determined,
|
||||||
|
rate limit is not applied.
|
||||||
|
:param max_num: The maximum number of actions to performed within expire_time of the first action
|
||||||
|
:param expire_time: The length of the time window in seconds
|
||||||
|
:param increase: Whether to count the call as an event counted towards the rate, or just check
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not settings.HAS_REDIS:
|
||||||
|
# No rate limiting
|
||||||
|
return False
|
||||||
|
|
||||||
|
from django_redis import get_redis_connection
|
||||||
|
rc = get_redis_connection("redis")
|
||||||
|
|
||||||
|
if include_ip_from_request:
|
||||||
|
ip = _get_ip(include_ip_from_request)
|
||||||
|
if not ip:
|
||||||
|
# IP not discovered, can't rate limit
|
||||||
|
return False
|
||||||
|
parameters = (*parameters, ip)
|
||||||
|
|
||||||
|
redis_key = _get_key(key, parameters)
|
||||||
|
|
||||||
|
if increase:
|
||||||
|
p = rc.pipeline()
|
||||||
|
p.set(redis_key, 0, nx=True, ex=expire_time) # Start a rate limit window if none is running
|
||||||
|
p.incr(redis_key)
|
||||||
|
new_counter = p.execute()[1]
|
||||||
|
else:
|
||||||
|
new_counter = int(rc.get(redis_key) or 0)
|
||||||
|
|
||||||
|
if new_counter > max_num:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def rate_limit_reset(key: str, *parameters, include_ip_from_request: HttpRequest=None):
|
||||||
|
"""
|
||||||
|
Reset a rate limit bucket.
|
||||||
|
"""
|
||||||
|
if not settings.HAS_REDIS:
|
||||||
|
# No rate limiting
|
||||||
|
return
|
||||||
|
|
||||||
|
from django_redis import get_redis_connection
|
||||||
|
rc = get_redis_connection("redis")
|
||||||
|
|
||||||
|
if include_ip_from_request:
|
||||||
|
ip = _get_ip(include_ip_from_request)
|
||||||
|
if not ip:
|
||||||
|
# IP not discovered, can't rate limit
|
||||||
|
return False
|
||||||
|
parameters = (*parameters, ip)
|
||||||
|
|
||||||
|
redis_key = _get_key(key, parameters)
|
||||||
|
rc.delete(redis_key)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||||
|
|
||||||
|
|
||||||
|
def should_block_access(sa):
|
||||||
|
ip_addr = ipaddress.ip_address(sa[0])
|
||||||
|
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||||
|
if ip_addr.is_multicast:
|
||||||
|
return True, f"Request to multicast address {sa[0]} blocked"
|
||||||
|
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||||
|
return True, f"Request to local address {sa[0]} blocked"
|
||||||
|
if ip_addr.is_private:
|
||||||
|
return True, f"Request to private address {sa[0]} blocked"
|
||||||
|
if check_ip4 in _cgnat_net:
|
||||||
|
return True, f"Request to RFC 6598 address {sa[0]} blocked"
|
||||||
|
|
||||||
|
return False, None
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4577
-6633
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
|
||||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
"Last-Translator: ahmed badr <ahmedbadr@oma.com.eg>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"ar/>\n"
|
"ar/>\n"
|
||||||
"Language: ar\n"
|
"Language: ar\n"
|
||||||
@@ -18,7 +18,7 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||||
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
|
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
|
||||||
"X-Generator: Weblate 4.8\n"
|
"X-Generator: Weblate 2026.7.1\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||||
msgid "Marked as paid"
|
msgid "Marked as paid"
|
||||||
@@ -30,112 +30,110 @@ msgstr "تعليق:"
|
|||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "PayPal"
|
msgid "PayPal"
|
||||||
msgstr ""
|
msgstr "باي بال"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Venmo"
|
msgid "Venmo"
|
||||||
msgstr ""
|
msgstr "Venmo"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
#: pretix/static/pretixpresale/js/walletdetection.js
|
#: pretix/static/pretixpresale/js/walletdetection.js
|
||||||
msgid "Apple Pay"
|
msgid "Apple Pay"
|
||||||
msgstr ""
|
msgstr "Apple Pay"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Itaú"
|
msgid "Itaú"
|
||||||
msgstr ""
|
msgstr "إيتاو"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "PayPal Credit"
|
msgid "PayPal Credit"
|
||||||
msgstr ""
|
msgstr "الائتمان من PayPal"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Credit Card"
|
msgid "Credit Card"
|
||||||
msgstr ""
|
msgstr "بطاقة الائتمان"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "PayPal Pay Later"
|
msgid "PayPal Pay Later"
|
||||||
msgstr ""
|
msgstr "الدفع لاحقًا عبر PayPal"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "iDEAL | Wero"
|
msgid "iDEAL | Wero"
|
||||||
msgstr ""
|
msgstr "iDEAL | التحدي"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "SEPA Direct Debit"
|
msgid "SEPA Direct Debit"
|
||||||
msgstr ""
|
msgstr "الخصم المباشر في منطقة الدفع الموحدة (SEPA)"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Bancontact"
|
msgid "Bancontact"
|
||||||
msgstr ""
|
msgstr "بانكونتاكت"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "giropay"
|
msgid "giropay"
|
||||||
msgstr ""
|
msgstr "giropay"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "SOFORT"
|
msgid "SOFORT"
|
||||||
msgstr ""
|
msgstr "فوراً"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Yes"
|
|
||||||
msgid "eps"
|
msgid "eps"
|
||||||
msgstr "نعم"
|
msgstr "نقاط إضافية"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "MyBank"
|
msgid "MyBank"
|
||||||
msgstr ""
|
msgstr "بنكي"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Przelewy24"
|
msgid "Przelewy24"
|
||||||
msgstr ""
|
msgstr "Przelewy24"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Verkkopankki"
|
msgid "Verkkopankki"
|
||||||
msgstr ""
|
msgstr "الخدمات المصرفية عبر الإنترنت"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "PayU"
|
msgid "PayU"
|
||||||
msgstr ""
|
msgstr "الدفع لك"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "BLIK"
|
msgid "BLIK"
|
||||||
msgstr ""
|
msgstr "BLIK"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Trustly"
|
msgid "Trustly"
|
||||||
msgstr ""
|
msgstr "Trustly"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Zimpler"
|
msgid "Zimpler"
|
||||||
msgstr ""
|
msgstr "Zimpler"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Maxima"
|
msgid "Maxima"
|
||||||
msgstr ""
|
msgstr "الأعظم"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "OXXO"
|
msgid "OXXO"
|
||||||
msgstr ""
|
msgstr "OXXO"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Boleto"
|
msgid "Boleto"
|
||||||
msgstr ""
|
msgstr "تذكرة"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "WeChat Pay"
|
msgid "WeChat Pay"
|
||||||
msgstr ""
|
msgstr "WeChat Pay"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Mercado Pago"
|
msgid "Mercado Pago"
|
||||||
msgstr ""
|
msgstr "Mercado Pago"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr "المتابعة"
|
msgstr "تابع"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
|
||||||
@@ -144,7 +142,7 @@ msgstr "جاري تأكيد الدفع الخاص بك …"
|
|||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||||
msgid "Payment method unavailable"
|
msgid "Payment method unavailable"
|
||||||
msgstr ""
|
msgstr "طريقة الدفع غير متاحة"
|
||||||
|
|
||||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||||
msgid "Placed orders"
|
msgid "Placed orders"
|
||||||
@@ -156,11 +154,11 @@ msgstr "الطلبات المدفوعة"
|
|||||||
|
|
||||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||||
msgid "Attendees (ordered)"
|
msgid "Attendees (ordered)"
|
||||||
msgstr ""
|
msgstr "الحاضرون (حسب الترتيب)"
|
||||||
|
|
||||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||||
msgid "Attendees (paid)"
|
msgid "Attendees (paid)"
|
||||||
msgstr ""
|
msgstr "الحاضرون (الذين دفعوا رسوم الاشتراك)"
|
||||||
|
|
||||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
|
||||||
msgid "Total revenue"
|
msgid "Total revenue"
|
||||||
@@ -192,7 +190,7 @@ msgstr "تبديل قائمة الدخول"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Search results"
|
msgid "Search results"
|
||||||
msgstr "البحث في النتائج"
|
msgstr "نتائج البحث"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "No tickets found"
|
msgid "No tickets found"
|
||||||
@@ -236,15 +234,15 @@ msgstr "غير مدفوع"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Canceled"
|
msgid "Canceled"
|
||||||
msgstr "ملغاة"
|
msgstr "تم إلغاؤه"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Confirmed"
|
msgid "Confirmed"
|
||||||
msgstr ""
|
msgstr "تم التأكيد"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Approval pending"
|
msgid "Approval pending"
|
||||||
msgstr ""
|
msgstr "في انتظار الموافقة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
@@ -252,11 +250,11 @@ msgstr "مستخدم"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "قم بالإلغاء"
|
msgstr "إلغاء"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Ticket not paid"
|
msgid "Ticket not paid"
|
||||||
msgstr "لم يتم دفع قيمة التذكرة"
|
msgstr "التذكرة لم يتم دفع ثمنها"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
@@ -276,23 +274,19 @@ msgstr "تم تسجيل الخروج"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Ticket already used"
|
msgid "Ticket already used"
|
||||||
msgstr "تم استخدام التذكرة مسبقا"
|
msgstr "تم استخدام التذكرة بالفعل"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Information required"
|
msgid "Information required"
|
||||||
msgstr "معلومات مطلوبة"
|
msgstr "المعلومات المطلوبة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Unknown error."
|
|
||||||
msgid "Unknown ticket"
|
msgid "Unknown ticket"
|
||||||
msgstr "خطأ غير معروف."
|
msgstr "تذكرة غير معروفة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Entry not allowed"
|
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr "إدخال غير مسموح"
|
msgstr "نوع تذكرة الدخول غير مسموح به هنا"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Entry not allowed"
|
msgid "Entry not allowed"
|
||||||
@@ -300,17 +294,13 @@ msgstr "إدخال غير مسموح"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Ticket code revoked/changed"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr "تم إلغاء رمز التذكرة أو تبديله"
|
msgstr "تم إلغاء/تغيير رمز التذكرة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Ticket not paid"
|
|
||||||
msgid "Ticket blocked"
|
msgid "Ticket blocked"
|
||||||
msgstr "لم يتم دفع قيمة التذكرة"
|
msgstr "التذكرة غير محجوبة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Ticket not paid"
|
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr "لم يتم دفع قيمة التذكرة"
|
msgstr "لم يتم دفع قيمة التذكرة"
|
||||||
|
|
||||||
@@ -320,11 +310,11 @@ msgstr "تم إلغاء الطلب"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr ""
|
msgstr "رمز التذكرة غير واضح في القائمة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Order not approved"
|
msgid "Order not approved"
|
||||||
msgstr ""
|
msgstr "لم تتم الموافقة على الطلب"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Checked-in Tickets"
|
msgid "Checked-in Tickets"
|
||||||
@@ -348,12 +338,9 @@ msgstr "نعم"
|
|||||||
#: pretix/static/pretixcontrol/js/ui/question.js
|
#: pretix/static/pretixcontrol/js/ui/question.js
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js
|
#: pretix/static/pretixpresale/js/ui/questions.js
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr "لا"
|
msgstr "من"
|
||||||
|
|
||||||
#: pretix/static/lightbox/js/lightbox.js
|
#: pretix/static/lightbox/js/lightbox.js
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "Close"
|
|
||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "إغلاق"
|
msgstr "إغلاق"
|
||||||
|
|
||||||
@@ -408,7 +395,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: pretix/static/pretixbase/js/asynctask.js
|
#: pretix/static/pretixbase/js/asynctask.js
|
||||||
msgid "We are processing your request …"
|
msgid "We are processing your request …"
|
||||||
msgstr "جاري معالجة طلبك …"
|
msgstr "نحن نقوم بمعالجة طلبك …"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/asynctask.js
|
#: pretix/static/pretixbase/js/asynctask.js
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -421,7 +408,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: pretix/static/pretixbase/js/asynctask.js
|
#: pretix/static/pretixbase/js/asynctask.js
|
||||||
msgid "If this takes longer than a few minutes, please contact us."
|
msgid "If this takes longer than a few minutes, please contact us."
|
||||||
msgstr ""
|
msgstr "إذا استغرق ذلك أكثر من بضع دقائق، يرجى الاتصال بنا."
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/asynctask.js
|
#: pretix/static/pretixbase/js/asynctask.js
|
||||||
msgid "Close message"
|
msgid "Close message"
|
||||||
@@ -437,11 +424,11 @@ msgstr "للنسخ اضغط Ctrl + C!"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr "تحرير"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid "Visualize"
|
msgid "Visualize"
|
||||||
msgstr ""
|
msgstr "تصور"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -449,10 +436,13 @@ msgid ""
|
|||||||
"or variations are not contained in any of your rule parts so people with "
|
"or variations are not contained in any of your rule parts so people with "
|
||||||
"these tickets will not get in:"
|
"these tickets will not get in:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"تقوم القاعدة الخاصة بك دائمًا بالتصفية حسب المنتج أو النوع، لكن المنتجات أو "
|
||||||
|
"الأنواع التالية غير مدرجة في أي من أجزاء القاعدة الخاصة بك، لذا لن يتم قبول "
|
||||||
|
"الأشخاص الذين يحملون هذه التذاكر:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid "Please double-check if this was intentional."
|
msgid "Please double-check if this was intentional."
|
||||||
msgstr ""
|
msgstr "يرجى التأكد مرة أخرى مما إذا كان ذلك مقصودًا أم لا."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
@@ -492,21 +482,21 @@ msgstr "أضف شرطا"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "minutes"
|
msgid "minutes"
|
||||||
msgstr "الدقائق"
|
msgstr "دقائق"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr ""
|
msgstr "مكرر"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgctxt "entry_status"
|
msgctxt "entry_status"
|
||||||
msgid "present"
|
msgid "present"
|
||||||
msgstr ""
|
msgstr "حاضر"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgctxt "entry_status"
|
msgctxt "entry_status"
|
||||||
msgid "absent"
|
msgid "absent"
|
||||||
msgstr ""
|
msgstr "غائب"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "is one of"
|
msgid "is one of"
|
||||||
@@ -522,11 +512,11 @@ msgstr "بعد"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "="
|
msgid "="
|
||||||
msgstr ""
|
msgstr "="
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Product"
|
msgid "Product"
|
||||||
msgstr "منتج"
|
msgstr "المنتج"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Product variation"
|
msgid "Product variation"
|
||||||
@@ -534,7 +524,7 @@ msgstr "نوع المنتج"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Gate"
|
msgid "Gate"
|
||||||
msgstr ""
|
msgstr "الشارع"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Current date and time"
|
msgid "Current date and time"
|
||||||
@@ -542,11 +532,11 @@ msgstr "التاريخ والوقت الحالي"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
||||||
msgstr ""
|
msgstr "اليوم الحالي من الأسبوع (1 = الاثنين، 7 = الأحد)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Current entry status"
|
msgid "Current entry status"
|
||||||
msgstr ""
|
msgstr "الحالة الحالية للطلب"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Number of previous entries"
|
msgid "Number of previous entries"
|
||||||
@@ -557,48 +547,40 @@ msgid "Number of previous entries since midnight"
|
|||||||
msgstr "عدد المدخلات السابقة قبل منتصف الليل"
|
msgstr "عدد المدخلات السابقة قبل منتصف الليل"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Number of previous entries"
|
|
||||||
msgid "Number of previous entries since"
|
msgid "Number of previous entries since"
|
||||||
msgstr "عدد المدخلات السابقة"
|
msgstr "عدد الإدخالات السابقة منذ"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Number of previous entries"
|
|
||||||
msgid "Number of previous entries before"
|
msgid "Number of previous entries before"
|
||||||
msgstr "عدد المدخلات السابقة"
|
msgstr "عدد الإدخالات السابقة قبل"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Number of days with a previous entry"
|
msgid "Number of days with a previous entry"
|
||||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Number of days with a previous entry"
|
|
||||||
msgid "Number of days with a previous entry since"
|
msgid "Number of days with a previous entry since"
|
||||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
msgstr "عدد الأيام التي تحتوي على قيد سابق منذ"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Number of days with a previous entry"
|
|
||||||
msgid "Number of days with a previous entry before"
|
msgid "Number of days with a previous entry before"
|
||||||
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
|
msgstr "عدد الأيام التي يوجد فيها قيد سابق قبل ذلك"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Minutes since last entry (-1 on first entry)"
|
msgid "Minutes since last entry (-1 on first entry)"
|
||||||
msgstr ""
|
msgstr "عدد الدقائق منذ آخر تسجيل (-1 عند التسجيل الأول)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "Minutes since first entry (-1 on first entry)"
|
msgid "Minutes since first entry (-1 on first entry)"
|
||||||
msgstr ""
|
msgstr "عدد الدقائق منذ أول دخول (-1 عند أول دخول)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
|
||||||
msgid "Error: Product not found!"
|
msgid "Error: Product not found!"
|
||||||
msgstr ""
|
msgstr "خطأ: لم يتم العثور على المنتج!"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
|
||||||
msgid "Error: Variation not found!"
|
msgid "Error: Variation not found!"
|
||||||
msgstr ""
|
msgstr "خطأ: لم يتم العثور على النسخة!"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
@@ -613,16 +595,12 @@ msgid "Group of objects"
|
|||||||
msgstr "مجموعة من العناصر"
|
msgstr "مجموعة من العناصر"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Text object"
|
|
||||||
msgid "Text object (deprecated)"
|
msgid "Text object (deprecated)"
|
||||||
msgstr "عنصر نص"
|
msgstr "كائن نصي (مهمل)"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Text object"
|
|
||||||
msgid "Text box"
|
msgid "Text box"
|
||||||
msgstr "عنصر نص"
|
msgstr "مربع نصي"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js
|
#: pretix/static/pretixcontrol/js/ui/editor.js
|
||||||
msgid "Barcode area"
|
msgid "Barcode area"
|
||||||
@@ -669,28 +647,28 @@ msgid "Unknown error."
|
|||||||
msgstr "خطأ غير معروف."
|
msgstr "خطأ غير معروف."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Your color has great contrast and is very easy to read!"
|
|
||||||
msgid "Your color has great contrast and will provide excellent accessibility."
|
msgid "Your color has great contrast and will provide excellent accessibility."
|
||||||
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
|
msgstr "يتميز لونك بتباين رائع ويسهل قراءته للغاية! وسيوفر إمكانية وصول ممتازة."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Your color has decent contrast and is probably good-enough to read!"
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Your color has decent contrast and is sufficient for minimum accessibility "
|
"Your color has decent contrast and is sufficient for minimum accessibility "
|
||||||
"requirements."
|
"requirements."
|
||||||
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
|
msgstr ""
|
||||||
|
"يتميز لونك بتباين جيد، ومن المرجح أنه مناسب للقراءة! وهو كافٍ لتلبية الحد "
|
||||||
|
"الأدنى من متطلبات سهولة الوصول."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
msgid ""
|
msgid ""
|
||||||
"Your color has insufficient contrast to white. Accessibility of your site "
|
"Your color has insufficient contrast to white. Accessibility of your site "
|
||||||
"will be impacted."
|
"will be impacted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"اللون الذي اخترته لا يتمتع بتباين كافٍ مع اللون الأبيض. وسيؤثر ذلك على إمكانية "
|
||||||
|
"الوصول إلى موقعك."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr "البحث في الاستفسارات"
|
msgstr "استعلام البحث"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
msgid "All"
|
msgid "All"
|
||||||
@@ -706,11 +684,11 @@ msgstr "المختارة فقط"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr "أدخل رقم الصفحة بين 1 و %(max)s."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr "رقم الصفحة غير صحيح."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js
|
#: pretix/static/pretixcontrol/js/ui/main.js
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
@@ -729,10 +707,8 @@ msgid "Calculating default price…"
|
|||||||
msgstr "حساب السعر الافتراضي…"
|
msgstr "حساب السعر الافتراضي…"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/plugins.js
|
#: pretix/static/pretixcontrol/js/ui/plugins.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Search results"
|
|
||||||
msgid "No results"
|
msgid "No results"
|
||||||
msgstr "البحث في النتائج"
|
msgstr "البحث: لا توجد نتائج"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js
|
#: pretix/static/pretixcontrol/js/ui/question.js
|
||||||
msgid "Others"
|
msgid "Others"
|
||||||
@@ -740,7 +716,7 @@ msgstr "غير ذلك"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js
|
#: pretix/static/pretixcontrol/js/ui/question.js
|
||||||
msgid "Count"
|
msgid "Count"
|
||||||
msgstr "احسب"
|
msgstr "العدد"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/subevent.js
|
#: pretix/static/pretixcontrol/js/ui/subevent.js
|
||||||
msgid "(one more date)"
|
msgid "(one more date)"
|
||||||
@@ -753,12 +729,12 @@ msgstr[4] "أيام عديدة"
|
|||||||
msgstr[5] "أخرى"
|
msgstr[5] "أخرى"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "The items in your cart are no longer reserved for you."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The items in your cart are no longer reserved for you. You can still "
|
"The items in your cart are no longer reserved for you. You can still "
|
||||||
"complete your order as long as they’re available."
|
"complete your order as long as they’re available."
|
||||||
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
|
msgstr ""
|
||||||
|
"لم تعد المنتجات الموجودة في سلة التسوق محجوزة لك. لا يزال بإمكانك إتمام طلبك "
|
||||||
|
"طالما كانت هذه المنتجات متوفرة."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
msgid "Cart expired"
|
msgid "Cart expired"
|
||||||
@@ -766,13 +742,9 @@ msgstr "انتهت صلاحية عربة التسوق"
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
msgid "Your cart is about to expire."
|
msgid "Your cart is about to expire."
|
||||||
msgstr ""
|
msgstr "سوف تنتهي صلاحية سلة التسوق الخاصة بك قريبًا."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "The items in your cart are reserved for you for one minute."
|
|
||||||
#| msgid_plural ""
|
|
||||||
#| "The items in your cart are reserved for you for {num} minutes."
|
|
||||||
msgid "The items in your cart are reserved for you for one minute."
|
msgid "The items in your cart are reserved for you for one minute."
|
||||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||||
msgstr[0] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
|
msgstr[0] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
|
||||||
@@ -783,26 +755,24 @@ msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقا
|
|||||||
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
|
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Cart expired"
|
|
||||||
msgid "Your cart has expired."
|
msgid "Your cart has expired."
|
||||||
msgstr "انتهت صلاحية عربة التسوق"
|
msgstr "انتهت صلاحية عربة التسوق"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
#, fuzzy
|
|
||||||
#| msgid "The items in your cart are no longer reserved for you."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The items in your cart are no longer reserved for you. You can still "
|
"The items in your cart are no longer reserved for you. You can still "
|
||||||
"complete your order as long as they're available."
|
"complete your order as long as they're available."
|
||||||
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
|
msgstr ""
|
||||||
|
"لم تعد المنتجات الموجودة في سلة التسوق محجوزة لك. لا يزال بإمكانك إتمام طلبك "
|
||||||
|
"طالما كانت هذه المنتجات متوفرة."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
msgid "Do you want to renew the reservation period?"
|
msgid "Do you want to renew the reservation period?"
|
||||||
msgstr ""
|
msgstr "هل ترغب في تمديد فترة الحجز؟"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||||
msgid "Renew reservation"
|
msgid "Renew reservation"
|
||||||
msgstr ""
|
msgstr "تجديد الحجز"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js
|
#: pretix/static/pretixpresale/js/ui/main.js
|
||||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||||
@@ -822,82 +792,77 @@ msgstr "التوقيت المحلي:"
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/walletdetection.js
|
#: pretix/static/pretixpresale/js/walletdetection.js
|
||||||
msgid "Google Pay"
|
msgid "Google Pay"
|
||||||
msgstr ""
|
msgstr "Google Pay"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Quantity"
|
msgid "Quantity"
|
||||||
msgstr ""
|
msgstr "الكمية"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Decrease quantity"
|
msgid "Decrease quantity"
|
||||||
msgstr ""
|
msgstr "تقليل الكمية"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Increase quantity"
|
msgid "Increase quantity"
|
||||||
msgstr ""
|
msgstr "زيادة الكمية"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Filter events by"
|
msgid "Filter events by"
|
||||||
msgstr ""
|
msgstr "تصفية الأحداث حسب"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Filter"
|
msgid "Filter"
|
||||||
msgstr ""
|
msgstr "فلتر"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Price"
|
msgid "Price"
|
||||||
msgstr ""
|
msgstr "السعر"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Original price: %s"
|
msgid "Original price: %s"
|
||||||
msgstr ""
|
msgstr "السعر الأصلي: %s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "New price: %s"
|
msgid "New price: %s"
|
||||||
msgstr ""
|
msgstr "السعر الجديد: %s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Selected only"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Select"
|
msgid "Select"
|
||||||
msgstr "المختارة فقط"
|
msgstr "المختارة فقط"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
#| msgid "Selected only"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Select %s"
|
msgid "Select %s"
|
||||||
msgstr "المختارة فقط"
|
msgstr "تم اختيار %s فقط"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "See variations"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Select variant %s"
|
msgid "Select variant %s"
|
||||||
msgstr "أنظر إلى الاختلافات"
|
msgstr "اختر النوع %s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -940,7 +905,7 @@ msgstr "من %(currency) s %(price)s"
|
|||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Image of %s"
|
msgid "Image of %s"
|
||||||
msgstr ""
|
msgstr "صورة لـ %s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -981,27 +946,21 @@ msgstr "متوفرة مع القسيمة فقط"
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "currently available: %s"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Not yet available"
|
msgid "Not yet available"
|
||||||
msgstr "متوفر حاليا: %s"
|
msgstr "حاليًا غير متوفر: %s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Not available anymore"
|
msgid "Not available anymore"
|
||||||
msgstr ""
|
msgstr "لم يعد متوفراً"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "currently available: %s"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Currently not available"
|
msgid "Currently not available"
|
||||||
msgstr "متوفر حاليا: %s"
|
msgstr "غير متوفر حاليًا: %s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -1029,24 +988,20 @@ msgid ""
|
|||||||
"There are currently a lot of users in this ticket shop. Please open the shop "
|
"There are currently a lot of users in this ticket shop. Please open the shop "
|
||||||
"in a new tab to continue."
|
"in a new tab to continue."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"يوجد حالياً عدد كبير من المستخدمين في متجر التذاكر هذا. يرجى فتح المتجر في "
|
||||||
|
"علامة تبويب جديدة للمتابعة."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "Close ticket shop"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Open ticket shop"
|
msgid "Open ticket shop"
|
||||||
msgstr "إغلاق متجر التذاكر"
|
msgstr "إغلاق/فتح متجر التذاكر"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "Resume checkout"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Checkout"
|
msgid "Checkout"
|
||||||
msgstr "استئناف الدفع"
|
msgstr "استئناف عملية الدفع"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -1061,6 +1016,9 @@ msgid ""
|
|||||||
"We could not create your cart, since there are currently too many users in "
|
"We could not create your cart, since there are currently too many users in "
|
||||||
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
"this ticket shop. Please click \"Continue\" to retry in a new tab."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"لم نتمكن من إنشاء سلة التسوق الخاصة بك، نظرًا لوجود عدد كبير جدًّا من "
|
||||||
|
"المستخدمين حاليًّا في متجر التذاكر هذا. يرجى النقر على «متابعة» لإعادة المحاولة "
|
||||||
|
"في علامة تبويب جديدة."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -1110,18 +1068,15 @@ msgstr "إغلاق"
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "Resume checkout"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Close checkout"
|
msgid "Close checkout"
|
||||||
msgstr "استئناف الدفع"
|
msgstr "إغلاق عملية الدفع"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "You cannot cancel this operation. Please wait for loading to finish."
|
msgid "You cannot cancel this operation. Please wait for loading to finish."
|
||||||
msgstr ""
|
msgstr "لا يمكنك إلغاء هذه العملية. يرجى الانتظار حتى ينتهي التحميل."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -1131,21 +1086,15 @@ msgstr "استمرار"
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "See variations"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Show variants"
|
msgid "Show variants"
|
||||||
msgstr "أنظر إلى الاختلافات"
|
msgstr "انظر الاختلافات"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgctxt "widget"
|
|
||||||
#| msgid "See variations"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Hide variants"
|
msgid "Hide variants"
|
||||||
msgstr "أنظر إلى الاختلافات"
|
msgstr "إخفاء الاختلافات"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -1203,11 +1152,11 @@ msgid ""
|
|||||||
"add yourself to the waiting list. We will then notify if seats are available "
|
"add yourself to the waiting list. We will then notify if seats are available "
|
||||||
"again."
|
"again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"تم بيع جميع فئات التذاكر أو بعضها حاليًا. إذا أردت، يمكنك إضافة اسمك إلى "
|
||||||
|
"قائمة الانتظار. وسنقوم بإخطارك إذا توفرت مقاعد مرة أخرى."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Load more"
|
|
||||||
msgctxt "widget"
|
msgctxt "widget"
|
||||||
msgid "Load more"
|
msgid "Load more"
|
||||||
msgstr "تحميل المزيد"
|
msgstr "تحميل المزيد"
|
||||||
@@ -1250,37 +1199,37 @@ msgstr "الأحد"
|
|||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "Monday"
|
msgid "Monday"
|
||||||
msgstr ""
|
msgstr "الاثنين"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "Tuesday"
|
msgid "Tuesday"
|
||||||
msgstr ""
|
msgstr "الثلاثاء"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "Wednesday"
|
msgid "Wednesday"
|
||||||
msgstr ""
|
msgstr "الأربعاء"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "Thursday"
|
msgid "Thursday"
|
||||||
msgstr ""
|
msgstr "الخميس"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "Friday"
|
msgid "Friday"
|
||||||
msgstr ""
|
msgstr "الجمعة"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr ""
|
msgstr "السبت"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "Sunday"
|
msgid "Sunday"
|
||||||
msgstr ""
|
msgstr "الأحد"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
@@ -1305,7 +1254,7 @@ msgstr "أبريل"
|
|||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr "مايو"
|
msgstr "هناك"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/widget/widget.js
|
#: pretix/static/pretixpresale/js/widget/widget.js
|
||||||
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
#: pretix/static/pretixpresale/widget/src/i18n.ts
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: 1\n"
|
"Project-Id-Version: 1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||||
"PO-Revision-Date: 2026-07-07 09:55+0000\n"
|
"PO-Revision-Date: 2026-07-15 14:20+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Jochen Siebert <siebert@rami.io>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
|
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||||
"de/>\n"
|
"de/>\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
@@ -14,7 +14,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 2026.6.1\n"
|
"X-Generator: Weblate 2026.7.1\n"
|
||||||
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
|
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
|
||||||
|
|
||||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
|
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
|
||||||
@@ -8166,7 +8166,7 @@ msgstr "Zugang zu existierenden Veranstaltungen"
|
|||||||
msgctxt "permission_level"
|
msgctxt "permission_level"
|
||||||
msgid "Access existing and create new events"
|
msgid "Access existing and create new events"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Zugang zu existieren Veranstaltungen und neue Veranstaltungen erstellen"
|
"Zugang zu existierenden Veranstaltungen und neue Veranstaltungen erstellen"
|
||||||
|
|
||||||
#: pretix/base/permissions.py
|
#: pretix/base/permissions.py
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||||
"PO-Revision-Date: 2026-05-04 07:42+0000\n"
|
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
|
||||||
"Last-Translator: Martin Gross <gross@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||||
"pretix/pretix-js/de_Informal/>\n"
|
"pretix/pretix-js/de_Informal/>\n"
|
||||||
"Language: de_Informal\n"
|
"Language: de_Informal\n"
|
||||||
@@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 5.17\n"
|
"X-Generator: Weblate 2026.7.1\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||||
msgid "Marked as paid"
|
msgid "Marked as paid"
|
||||||
@@ -430,11 +430,11 @@ msgstr "Drücke Strg+C zum kopieren!"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr "Bearbeiten"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid "Visualize"
|
msgid "Visualize"
|
||||||
msgstr ""
|
msgstr "Visualisieren"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -442,10 +442,13 @@ msgid ""
|
|||||||
"or variations are not contained in any of your rule parts so people with "
|
"or variations are not contained in any of your rule parts so people with "
|
||||||
"these tickets will not get in:"
|
"these tickets will not get in:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Deine Regeln filtern immer nach Produkt oder Variation, jedoch sind die "
|
||||||
|
"folgenden Produkte oder Variationen nicht in einer Ihrer Regeln enthalten, "
|
||||||
|
"sodass Kunden mit diesen Tickets keinen Eintritt erhalten werden:"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
|
||||||
msgid "Please double-check if this was intentional."
|
msgid "Please double-check if this was intentional."
|
||||||
msgstr ""
|
msgstr "Bitte überprüfe, ob dies beabsichtigt war."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
|
||||||
msgid "All of the conditions below (AND)"
|
msgid "All of the conditions below (AND)"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||||
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
|
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
|
||||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||||
"es/>\n"
|
"es/>\n"
|
||||||
@@ -11453,10 +11453,8 @@ msgstr ""
|
|||||||
"contacto con usted."
|
"contacto con usted."
|
||||||
|
|
||||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Contact"
|
|
||||||
msgid "Contact URL"
|
msgid "Contact URL"
|
||||||
msgstr "Contacto"
|
msgstr "URL de contacto"
|
||||||
|
|
||||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -11464,6 +11462,10 @@ msgid ""
|
|||||||
"the email address above. Please note that you still need to add a contact "
|
"the email address above. Please note that you still need to add a contact "
|
||||||
"email address that will be shared with all emails you send."
|
"email address that will be shared with all emails you send."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Si activas esta opción, el enlace de contacto del pie de página redirigirá "
|
||||||
|
"aquí, en lugar de utilizar la dirección de correo electrónico indicada más "
|
||||||
|
"arriba. Ten en cuenta que aún así tendrás que añadir una dirección de correo "
|
||||||
|
"electrónico de contacto que se incluirá en todos los correos que envíes."
|
||||||
|
|
||||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||||
msgid "Imprint URL"
|
msgid "Imprint URL"
|
||||||
@@ -19946,25 +19948,7 @@ msgstr ""
|
|||||||
"El equipo de %(instance)s\n"
|
"El equipo de %(instance)s\n"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
|
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "Hello,\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
|
|
||||||
#| "Once verified, emails sent from %(instance)s can show this address as the "
|
|
||||||
#| "sender.\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "If this was you, enter the following code in the setup form:\n"
|
|
||||||
#| "\n"
|
|
||||||
#| " %(code)s\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "Don't share this code with anyone. The %(instance)s team will never ask "
|
|
||||||
#| "you for it.\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "If you didn't request this, you can safely ignore this email.\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "Thanks, \n"
|
|
||||||
#| "The %(instance)s Team\n"
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Hello,\n"
|
"Hello,\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -19987,19 +19971,19 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Hola,\n"
|
"Hola,\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Alguien ha solicitado utilizar %(address)s como dirección de remitente en "
|
"Alguien ha solicitado utilizar %(address)s como dirección de remitente en %"
|
||||||
"%(instance)s. Una vez verificado, los correos electrónicos enviados desde "
|
"(instance)s. Una vez verificado, los correos electrónicos enviados desde %"
|
||||||
"%(instance)s podrán mostrar esta dirección como remitente.\n"
|
"(instance)s podrán mostrar esta dirección como remitente.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Si has sido tú, introduce el siguiente código en el formulario de "
|
"Si ha sido usted quien lo ha solicitado, introduzca el siguiente código en "
|
||||||
"configuración:\n"
|
"el formulario de configuración:\n"
|
||||||
"\n"
|
"\n"
|
||||||
" %(code)s\n"
|
" %(code)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"No compartas este código con nadie. El equipo de %(instance)s nunca te lo "
|
"No comparta este código con nadie a menos que quiera autorizarle a utilizar "
|
||||||
"pedirá.\n"
|
"esta dirección para este fin. El equipo de %(instance)s nunca se lo pedirá.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Si no has solicitado esto, puedes ignorar este correo electrónico sin ningún "
|
"Si no ha solicitado esto, puede ignorar este correo electrónico sin ningún "
|
||||||
"problema.\n"
|
"problema.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Gracias, \n"
|
"Gracias, \n"
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ msgstr ""
|
|||||||
"Project-Id-Version: 1\n"
|
"Project-Id-Version: 1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||||
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
|
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
|
||||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||||
">\n"
|
"fr/>\n"
|
||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -1933,7 +1933,7 @@ msgid ""
|
|||||||
"redeemed."
|
"redeemed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ce produit ne sera affiché que si un bon de réduction correspondant au "
|
"Ce produit ne sera affiché que si un bon de réduction correspondant au "
|
||||||
"produit est échangé."
|
"produit est validé."
|
||||||
|
|
||||||
#: pretix/base/exporters/items.py pretix/base/models/items.py
|
#: pretix/base/exporters/items.py pretix/base/models/items.py
|
||||||
msgid "Buying this product requires approval"
|
msgid "Buying this product requires approval"
|
||||||
@@ -4109,8 +4109,8 @@ msgstr ""
|
|||||||
msgid ""
|
msgid ""
|
||||||
"To verify your email address {email} on {instance}, use the following code:"
|
"To verify your email address {email} on {instance}, use the following code:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Pour valider votre adresse e-mail {email} sur\n"
|
"Pour valider votre adresse e-mail {email} sur {instance}, utilisez le code "
|
||||||
"{instance}, utilisez le code suivant :"
|
"suivant :"
|
||||||
|
|
||||||
#: pretix/base/models/auth.py
|
#: pretix/base/models/auth.py
|
||||||
msgid "Your confirmation code"
|
msgid "Your confirmation code"
|
||||||
@@ -6975,7 +6975,7 @@ msgstr "Nombre de fois que ce bon peut être utilisé."
|
|||||||
|
|
||||||
#: pretix/base/models/vouchers.py pretix/control/views/vouchers.py
|
#: pretix/base/models/vouchers.py pretix/control/views/vouchers.py
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
msgstr "Échangé"
|
msgstr "Validé"
|
||||||
|
|
||||||
#: pretix/base/models/vouchers.py
|
#: pretix/base/models/vouchers.py
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -11542,10 +11542,8 @@ msgstr ""
|
|||||||
"contacter."
|
"contacter."
|
||||||
|
|
||||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Contact"
|
|
||||||
msgid "Contact URL"
|
msgid "Contact URL"
|
||||||
msgstr "Contact"
|
msgstr "URL de contact"
|
||||||
|
|
||||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -11553,6 +11551,10 @@ msgid ""
|
|||||||
"the email address above. Please note that you still need to add a contact "
|
"the email address above. Please note that you still need to add a contact "
|
||||||
"email address that will be shared with all emails you send."
|
"email address that will be shared with all emails you send."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Si vous activez cette option, le lien de contact figurant dans le pied de "
|
||||||
|
"page redirigera vers cette page au lieu d'utiliser l'adresse e-mail indiquée "
|
||||||
|
"ci-dessus. Veuillez noter que vous devrez tout de même ajouter une adresse e-"
|
||||||
|
"mail de contact qui sera mentionnée dans tous les e-mails que vous enverrez."
|
||||||
|
|
||||||
#: pretix/base/settings.py pretix/control/forms/event.py
|
#: pretix/base/settings.py pretix/control/forms/event.py
|
||||||
msgid "Imprint URL"
|
msgid "Imprint URL"
|
||||||
@@ -20094,25 +20096,7 @@ msgstr ""
|
|||||||
"L'équipe %(instance)s\n"
|
"L'équipe %(instance)s\n"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
|
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
|
||||||
#, fuzzy, python-format
|
#, python-format
|
||||||
#| msgid ""
|
|
||||||
#| "Hello,\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "Someone requested to use %(address)s as a sender address on %(instance)s. "
|
|
||||||
#| "Once verified, emails sent from %(instance)s can show this address as the "
|
|
||||||
#| "sender.\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "If this was you, enter the following code in the setup form:\n"
|
|
||||||
#| "\n"
|
|
||||||
#| " %(code)s\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "Don't share this code with anyone. The %(instance)s team will never ask "
|
|
||||||
#| "you for it.\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "If you didn't request this, you can safely ignore this email.\n"
|
|
||||||
#| "\n"
|
|
||||||
#| "Thanks, \n"
|
|
||||||
#| "The %(instance)s Team\n"
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Hello,\n"
|
"Hello,\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -20136,22 +20120,23 @@ msgstr ""
|
|||||||
"Bonjour,\n"
|
"Bonjour,\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Une personne a demandé à utiliser %(address)s comme adresse d'expéditeur sur "
|
"Une personne a demandé à utiliser %(address)s comme adresse d'expéditeur sur "
|
||||||
"%(instance)s. Une fois cette demande validée, les e-mails envoyés depuis "
|
"%(instance)s. Une fois cette demande validée, les e-mails envoyés depuis %"
|
||||||
"%(instance)s pourront afficher cette adresse comme expéditeur.\n"
|
"(instance)s pourront afficher cette adresse comme expéditeur.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Si c'est vous qui avez fait cette demande, veuillez saisir le code suivant "
|
"Si c'est bien vous qui en avez fait la demande, veuillez saisir le code "
|
||||||
"dans le formulaire de configuration :\n"
|
"suivant dans le formulaire de configuration :\n"
|
||||||
"\n"
|
"\n"
|
||||||
" %(code)s\n"
|
" %(code)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Ne communiquez ce code à personne. L'équipe de %(instance)s ne vous le "
|
"Ne communiquez ce code à personne, sauf si vous souhaitez autoriser cette "
|
||||||
"demandera jamais.\n"
|
"personne à utiliser cette adresse à cette fin. L'équipe de %(instance)s ne "
|
||||||
|
"vous le demandera jamais.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Si vous n’en avez pas fait la demande, vous pouvez ignorer cet e-mail en "
|
"Si vous n'en avez pas fait la demande, vous pouvez ignorer cet e-mail en "
|
||||||
"toute sécurité.\n"
|
"toute sécurité.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Merci, \n"
|
"Merci, \n"
|
||||||
"L’équipe %(instance)s\n"
|
"L'équipe de %(instance)s\n"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/email/forgot.txt
|
#: pretix/control/templates/pretixcontrol/email/forgot.txt
|
||||||
#, python-format
|
#, python-format
|
||||||
@@ -36830,17 +36815,19 @@ msgstr ""
|
|||||||
|
|
||||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||||
msgid "How to fix this:"
|
msgid "How to fix this:"
|
||||||
msgstr ""
|
msgstr "Comment résoudre ce problème :"
|
||||||
|
|
||||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||||
msgid "Close this page."
|
msgid "Close this page."
|
||||||
msgstr ""
|
msgstr "Fermer cette page."
|
||||||
|
|
||||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||||
msgid ""
|
msgid ""
|
||||||
"Open this website again directly in your main browser (for example Safari or "
|
"Open this website again directly in your main browser (for example Safari or "
|
||||||
"Chrome)."
|
"Chrome)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Rouvrez ce site directement dans votre navigateur principal (par exemple "
|
||||||
|
"Safari ou Chrome)."
|
||||||
|
|
||||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -36848,6 +36835,9 @@ msgid ""
|
|||||||
"browser window, without refreshing the page, opening new tabs, or switching "
|
"browser window, without refreshing the page, opening new tabs, or switching "
|
||||||
"apps part-way through."
|
"apps part-way through."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Recommencez la procédure de paiement ou de connexion et menez-la à son terme "
|
||||||
|
"dans la même fenêtre de navigateur, sans actualiser la page, ouvrir de "
|
||||||
|
"nouveaux onglets ni changer d'application en cours de route."
|
||||||
|
|
||||||
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -36855,6 +36845,10 @@ msgid ""
|
|||||||
"before retrying can help. As a last step, try using a different browser or "
|
"before retrying can help. As a last step, try using a different browser or "
|
||||||
"another device (for example, a desktop or laptop computer)."
|
"another device (for example, a desktop or laptop computer)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Si le problème persiste, il peut être utile de fermer toutes les fenêtres du "
|
||||||
|
"navigateur et d'effacer les cookies avant de réessayer. En dernier recours, "
|
||||||
|
"essayez d'utiliser un autre navigateur ou un autre appareil (par exemple, un "
|
||||||
|
"ordinateur de bureau ou un ordinateur portable)."
|
||||||
|
|
||||||
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html
|
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html
|
||||||
msgid "Your membership"
|
msgid "Your membership"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: French\n"
|
"Project-Id-Version: French\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
|
||||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
"Last-Translator: Joram Schwartzmann <schwartzmann@pretix.eu>\n"
|
||||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"fr/>\n"
|
"fr/>\n"
|
||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
@@ -244,7 +244,7 @@ msgstr "En attente d'approbation"
|
|||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
msgstr "Échangé"
|
msgstr "Validé"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user