Compare commits

..

6 Commits

Author SHA1 Message Date
Raphael Michel ef51a570ae Update src/pretix/base/migrations/0304_tax_rate_decimals.py
Co-authored-by: robbi5 <richt@rami.io>
2026-07-14 20:56:02 +02:00
Raphael Michel 8fe436cb25 Rebase migration 2026-07-14 09:27:10 +02:00
Raphael Michel 3d82b79cc9 Add uncommitted tests 2026-07-14 09:26:40 +02:00
Raphael Michel 40959ee24a More tax rate handling 2026-07-14 09:26:39 +02:00
Raphael Michel b3eec43645 Fix normalization 2026-07-14 09:25:46 +02:00
Raphael Michel b46956ba0a Allow more decimal places for tax rates 2026-07-14 09:25:46 +02:00
94 changed files with 8886 additions and 40806 deletions
+1 -2
View File
@@ -351,8 +351,7 @@ Endpoints
:<json boolean error_reason: One of ``canceled``, ``invalid``, ``unpaid``, ``product``, ``rules``, ``revoked``,
``incomplete``, ``already_redeemed``, ``blocked``, ``invalid_time``, or ``error``. 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 raw_barcode: The raw barcode you scanned. Required.
:<json datetime: Date and time of the scan. Optional.
:<json type: Type of scan, defaults to ``"entry"``.
:<json position: Internal ID of an order position you matched. Optional.
-6
View File
@@ -864,9 +864,6 @@ Generating new secrets
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**:
.. sourcecode:: http
@@ -898,9 +895,6 @@ Generating new secrets
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**:
.. sourcecode:: http
+3 -3
View File
@@ -41,10 +41,10 @@ dependencies = [
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
"django-filter==26.1",
"django-filter==25.1",
"django-formset-js-improved==0.5.0.5",
"django-formtools==2.7",
"django-hierarkey==2.0.*,>=2.0.2",
"django-hierarkey==2.0.*,>=2.0.1",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
"django-libsass==0.9",
@@ -94,7 +94,7 @@ dependencies = [
"redis==7.4.*",
"reportlab==5.0.*",
"requests==2.34.*",
"sentry-sdk==2.66.*",
"sentry-sdk==2.64.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
+1 -3
View File
@@ -52,7 +52,6 @@ from pretix.api.signals import order_api_details, orderposition_api_details
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.invoicing.transmission import get_transmission_types
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
@@ -382,7 +381,6 @@ class PrintLogSerializer(serializers.ModelSerializer):
class FailedCheckinSerializer(I18nAwareModelSerializer):
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, 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)
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)
@@ -392,7 +390,7 @@ class FailedCheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('error_reason', 'error_explanation', 'raw_barcode', 'raw_item', 'raw_variation',
'raw_subevent', 'raw_source_type', 'nonce', 'datetime', 'type', 'position')
'raw_subevent', 'nonce', 'datetime', 'type', 'position')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+32 -9
View File
@@ -33,6 +33,8 @@
# 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.
import hashlib
import ipaddress
import logging
from django import forms
@@ -40,12 +42,13 @@ from django.conf import settings
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pretix.base.metrics import pretix_failed_logins
from pretix.base.models import User
from pretix.helpers.dicts import move_to_end
from pretix.helpers.ratelimit import rate_limit
from pretix.helpers.http import get_client_ip
logger = logging.getLogger(__name__)
@@ -82,20 +85,40 @@ class LoginForm(forms.Form):
else:
move_to_end(self.fields, 'keep_logged_in')
@cached_property
def ratelimit_key(self):
if not settings.HAS_REDIS:
return None
client_ip = get_client_ip(self.request)
if not client_ip:
return None
try:
client_ip = ipaddress.ip_address(client_ip)
except ValueError:
# Web server not set up correctly
return None
if client_ip.is_private:
# This is the private IP of the server, web server not set up correctly
return None
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
def clean(self):
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
rate_limit_kwargs = dict(include_ip_from_request=self.request, max_num=10, expire_time=300)
if rate_limit("login", **rate_limit_kwargs, increase=False):
# Check rate limit without counting up, we increase below only on failed logins
pretix_failed_logins.inc(1, reason="ratelimit")
logger.info("Backend login rejected due to rate limit.")
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
if self.ratelimit_key:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.get(self.ratelimit_key)
if cnt and int(cnt) > 10:
pretix_failed_logins.inc(1, reason="ratelimit")
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)
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.")
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(
self.error_messages['invalid_login'],
code='invalid_login'
+1 -1
View File
@@ -959,7 +959,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required,
help_text=help_text,
initial=_initial,
widget=TimePickerWidget(without_seconds=True),
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
if not help_text:
+11 -22
View File
@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
from django import forms
from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
@@ -45,7 +46,6 @@ from pytz import common_timezones
from pretix.base.models import User
from pretix.control.forms import SingleLanguageWidget
from pretix.helpers.format import format_map
from pretix.helpers.ratelimit import rate_limit
class UserSettingsForm(forms.ModelForm):
@@ -128,11 +128,16 @@ class UserPasswordChangeForm(forms.Form):
def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw')
if rate_limit("pwchange", self.user.pk, max_num=10, expire_time=300):
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if settings.HAS_REDIS:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
if cnt > 10:
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if not check_password(old_pw, self.user.password):
raise forms.ValidationError(
@@ -170,35 +175,19 @@ class UserEmailChangeForm(forms.Form):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this email address. "
"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)
new_email = forms.EmailField(label=_('New email address'))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
def clean_new_email(self):
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():
raise forms.ValidationError(
self.error_messages['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
+5 -50
View File
@@ -43,10 +43,6 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _
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):
@@ -112,7 +108,7 @@ class DatePickerWidget(forms.DateInput):
class TimePickerWidget(forms.TimeInput):
def __init__(self, attrs=None, time_format=None, without_seconds=False):
def __init__(self, attrs=None, time_format=None):
attrs = attrs or {}
if 'placeholder' in attrs:
del attrs['placeholder']
@@ -121,27 +117,8 @@ class TimePickerWidget(forms.TimeInput):
time_attrs['class'] += ' timepickerfield'
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():
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]
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
@@ -205,7 +182,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None, without_seconds=False):
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None):
attrs = attrs or {}
if 'placeholder' in attrs:
del attrs['placeholder']
@@ -228,36 +205,14 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
).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():
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]
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
def time_placeholder():
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]
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
+1 -22
View File
@@ -266,22 +266,6 @@ def _merge_csp(a, b):
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_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
@@ -293,11 +277,6 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
if 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
if not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
@@ -314,7 +293,7 @@ class SecurityMiddleware(MiddlewareMixin):
'object-src': ["'none'"],
'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{static}", "{dynamic}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"],
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
@@ -1,91 +0,0 @@
# 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),
]
@@ -1,17 +0,0 @@
# 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")},
),
]
+1 -2
View File
@@ -649,7 +649,7 @@ class Event(EventMixin, LoggedModel):
is_remote = models.BooleanField(
default=False,
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 to let us calculate the local time of a user."),
help_text=_("This will be used to let users know if the event is in a different timezone and lets us calculate users local times."),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
@@ -1851,7 +1851,6 @@ class EventMetaProperty(LoggedModel):
class Meta:
ordering = ("position", "name",)
unique_together = ('organizer', 'name')
@property
def choice_keys(self):
+36
View File
@@ -885,6 +885,26 @@ class Item(LoggedModel):
return False
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):
check_quotas = set(getattr(
self, '_subevent_quotas', # Utilize cache in product list
@@ -1393,6 +1413,22 @@ class ItemVariation(models.Model):
return False
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
def meta_data(self):
data = self.item.meta_data
+2 -2
View File
@@ -936,7 +936,7 @@ class BasePaymentProvider:
"""
Will be called if the *event administrator* views the details of a payment.
It should return a SafeString containing HTML code, with information regarding the current payment
It should return HTML code containing information regarding the current payment
status and, if applicable, next steps.
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.
It should return a SafeString containing HTML code, with information regarding the current refund
It should return HTML code containing information regarding the current refund
status and, if applicable, next steps.
The default implementation returns an empty string.
-3
View File
@@ -245,9 +245,6 @@ def recv_classic(sender, **kwargs):
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
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
force_invalidate = True
+2 -2
View File
@@ -29,7 +29,7 @@ from typing import List
from django.utils.functional import cached_property
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
from pretix.presale.productlist import prepare_item_list_for_shop
from pretix.presale.views.event import get_grouped_items
class DummyCategory:
@@ -162,7 +162,7 @@ class CrossSellingService:
]
def _prepare_items(self, subevent, items_qs, discount_info):
items, _btn = prepare_item_list_for_shop(
items, _btn = get_grouped_items(
self.event,
subevent=subevent,
voucher=None,
-3
View File
@@ -24,7 +24,6 @@ from typing import List, Optional
from dateutil.relativedelta import relativedelta
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
@@ -97,8 +96,6 @@ 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
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
applicable_positions = [
p for p in positions
+23 -46
View File
@@ -110,7 +110,6 @@ from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.models import modelcopy
from pretix.helpers.periodic import minimum_interval
from pretix.presale.productlist import prepare_item_list_for_shop
from pretix.testutils.middleware import debugflags_var
@@ -1600,7 +1599,6 @@ class OrderChangeManager:
'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.'),
'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(
"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.",
@@ -1758,9 +1756,6 @@ class OrderChangeManager:
self._operations.append(self.RegenerateSecretOperation(position))
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))
def change_valid_from(self, position: OrderPosition, new_value: datetime):
@@ -1948,18 +1943,13 @@ class OrderChangeManager:
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
``"count"``, and ``"price"``.
:param limit_main_positions: By default, the method works on all positions of the order. If you set this to a
:param limit_main_positions: By default, the method works on all methods 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
untouched.
"""
if self._operations:
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
item_counts = Counter()
for p in self.order.positions.all():
@@ -2053,11 +2043,13 @@ class OrderChangeManager:
if not item.is_available() or (variation and not variation.is_available()):
raise OrderError(error_messages['unavailable'])
if not _allowed_on_order_sales_channel(item, self.order):
raise OrderError(error_messages['unavailable'])
if not item.all_sales_channels:
if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
raise OrderError(error_messages['unavailable'])
if variation and not _allowed_on_order_sales_channel(variation, self.order):
raise OrderError(error_messages['unavailable'])
if variation and not variation.all_sales_channels:
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
raise OrderError(error_messages['unavailable'])
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'])
@@ -2105,36 +2097,6 @@ class OrderChangeManager:
)
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
for cp, al in list(current_addons.items()):
for k, v in al.items():
@@ -2144,7 +2106,22 @@ class OrderChangeManager:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
if not _addon_is_available(a):
is_unavailable = (
# 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
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
continue
+9 -9
View File
@@ -197,10 +197,10 @@ class EventWizardBasicsForm(I18nModelForm):
'presale_end': SplitDateTimeField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(without_seconds=True),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}, without_seconds=True),
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}, without_seconds=True),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}),
'slug': SlugWidget,
}
@@ -521,11 +521,11 @@ class EventUpdateForm(I18nModelForm):
'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(without_seconds=True),
'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'}, without_seconds=True),
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
}
+1 -1
View File
@@ -770,7 +770,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
)
elif q.type == Question.TYPE_TIME:
self.fields[fname] = forms.TimeField(
widget=TimePickerWidget(without_seconds=True),
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
help_text=_('Exact matches only'),
**kwargs,
)
+4 -4
View File
@@ -246,8 +246,8 @@ class QuestionForm(I18nModelForm):
'valid_string_length_max',
]
widgets = {
'valid_datetime_min': SplitDateTimePickerWidget(without_seconds=True),
'valid_datetime_max': SplitDateTimePickerWidget(without_seconds=True),
'valid_datetime_min': SplitDateTimePickerWidget(),
'valid_datetime_max': SplitDateTimePickerWidget(),
'valid_date_min': DatePickerWidget(),
'valid_date_max': DatePickerWidget(),
'items': forms.CheckboxSelectMultiple(
@@ -1427,6 +1427,6 @@ class ItemProgramTimeForm(I18nModelForm):
'end': forms.SplitDateTimeField,
}
widgets = {
'start': SplitDateTimePickerWidget(without_seconds=True),
'end': SplitDateTimePickerWidget(without_seconds=True),
'start': SplitDateTimePickerWidget(),
'end': SplitDateTimePickerWidget(),
}
+6 -13
View File
@@ -39,7 +39,6 @@ from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
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
@@ -81,11 +80,11 @@ class SubEventForm(I18nModelForm):
'presale_end': SplitDateTimeField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(without_seconds=True),
'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'}, without_seconds=True),
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
}
@@ -163,7 +162,7 @@ class SubEventBulkEditForm(I18nModelForm):
self.fields[k + '_time'] = forms.TimeField(
label=self._meta.model._meta.get_field(k).verbose_name,
help_text=self._meta.model._meta.get_field(k).help_text,
widget=TimePickerWidget(without_seconds=True),
widget=TimePickerWidget(),
required=False,
)
@@ -507,12 +506,6 @@ class TimeForm(forms.Form):
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(
TimeForm,
+2 -2
View File
@@ -141,7 +141,7 @@ event_dashboard_widgets = EventPluginSignal()
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:
* content (SafeString, containing HTML)
* content (str, containing HTML)
* 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")
* priority (int, used for ordering, higher comes first, default is 1)
@@ -158,7 +158,7 @@ Arguments: 'user'
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:
* content (SafeString, containing HTML)
* content (str, containing HTML)
* 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")
* priority (int, used for ordering, higher comes first, default is 1)
@@ -3,7 +3,6 @@
{% load i18n %}
{% load static %}
{% load compress %}
{% load escapejson %}
{% block content %}
<form class="form-signin" action="" method="post" id="webauthn-form">
{% csrf_token %}
@@ -31,7 +30,8 @@
</form>
{% if jsondata %}
<script type="text/json" id="webauthn-login">
{{ jsondata|escapejson }}
{{ jsondata|safe }}
</script>
{% endif %}
{% compress js %}
@@ -28,7 +28,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</div>
</div>
@@ -51,7 +51,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</div>
</div>
@@ -72,7 +72,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</div>
</div>
@@ -94,7 +94,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</a>
{% else %}
@@ -102,7 +102,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</div>
{% endif %}
@@ -50,46 +50,6 @@
{% endblocktrans %}
</div>
{% 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 %}
<h3>{% trans "Verification" %}</h3>
<p>
@@ -110,7 +70,7 @@
</div>
</div>
{% if spf_warning or dkim_warning or dmarc_warning %}
{% if spf_warning %}
<div class="form-group submit-group">
<a href="" class="btn btn-default btn-save">
{% trans "Cancel" %}
@@ -106,7 +106,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</a>
{% elif w.link %}
@@ -114,7 +114,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x´"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</a>
{% else %}
@@ -122,7 +122,7 @@
{% if w.lazy %}
<span class="fa fa-cog fa-4x"></span>
{% else %}
{{ w.content }}
{{ w.content|safe }}
{% endif %}
</div>
{% endif %}
@@ -12,7 +12,7 @@
<table class="table table-payment-providers">
<tbody>
{% for provider in providers %}
<tr{% if provider.highlight %} class="success-left"{% endif %}>
<tr>
<td>
<strong>{{ provider.verbose_name }}</strong>
</td>
@@ -56,7 +56,7 @@
<td colspan="4">
<br>
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
<a href="{{ plugin_settings_url }}?go=payment#tab-0-1-open" class="btn btn-default">
<a href="{{ plugin_settings_url }}#tab-0-1-open" class="btn btn-default">
<i class="fa fa-plus"></i> {% trans "Enable additional payment plugins" %}
</a>
</td>
@@ -28,7 +28,6 @@
</div>
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<input type="hidden" name="go" value="{{ request.GET.go }}">
<div id="plugin_search_results" class="panel panel-default collapse">
<div class="panel-heading">
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>
@@ -2,7 +2,6 @@
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% load wrap_in %}
{% block title %}
{% trans "Cancel order" %}
{% endblock %}
@@ -27,7 +26,7 @@
{% if form.cancellation_fee %}
{% if fee %}
{% with fee|money:request.event.currency as f %}
<p>{% blocktrans trimmed with fee=f|wrap_in:"strong" %}
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
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:
{% endblocktrans %}</p>
@@ -284,14 +284,6 @@
</div>
<div class="col-sm-4">
{% 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>
@@ -903,7 +903,7 @@
<tr>
<td colspan="1"></td>
<td colspan="5">
{{ p.html_info }}
{{ p.html_info|safe }}
{% if staff_session %}
<p>
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
@@ -1018,7 +1018,7 @@
</dl>
{% endif %}
{% if r.html_info %}
{{ r.html_info }}
{{ r.html_info|safe }}
{% endif %}
{% if staff_session %}
<p>
@@ -2,7 +2,6 @@
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% load escapejson %}
{% block inner %}
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
@@ -19,7 +18,7 @@
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
</li>
<li>{% trans "Scan the following configuration code:" %}<br><br>
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script><br>
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
<br>
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
@@ -137,15 +137,6 @@
</td>
<td>
{{ 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>
{% if d.initialized %}
@@ -560,10 +560,11 @@
</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 "ajv/ajv2020.bundle.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 "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_white.png' %}" id="poweredby-white" class="sr-only">
{% for family, styles in fonts.items %}
@@ -1,7 +1,6 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
{% block content %}
<h1>{% trans "Add a two-factor authentication device" %}</h1>
@@ -33,7 +32,7 @@
</li>
<li>
{% trans "Add a new account to the app by scanning the following barcode:" %}
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script>
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
<p>
<a data-toggle="collapse" href="#no_scan">
{% trans "Can't scan the barcode?" %}
@@ -82,4 +81,9 @@
</li>
</ol>
<script type="text/json" id="qrdata">
{{ qrdata|safe }}
</script>
{% endblock %}
@@ -3,7 +3,6 @@
{% load bootstrap3 %}
{% load static %}
{% load compress %}
{% load escapejson %}
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
{% block content %}
<h1>{% trans "Add a two-factor authentication device" %}</h1>
@@ -27,7 +26,9 @@
{% trans "Device registration failed." %}
</div>
<script type="text/json" id="webauthn-enroll">
{{ jsondata|escapejson }}
{{ jsondata|safe }}
</script>
{% compress js %}
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
@@ -3,7 +3,6 @@
{% load bootstrap3 %}
{% load compress %}
{% load static %}
{% load escapejson %}
{% block content %}
<form class="form-signin" id="webauthn-form" action="" method="post">
{% csrf_token %}
@@ -44,7 +43,7 @@
{% if jsondata %}
<script type="text/json" id="webauthn-login">
{{ jsondata|escapejson }}
{{ jsondata|safe }}
</script>
{% endif %}
{% compress js %}
@@ -115,34 +115,34 @@
{% endif %}
<th>
{% trans "Voucher code" %}
<a href="?{% url_replace request 'filter-ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'filter-ordering' 'code' %}"><i class="fa fa-caret-up"></i></a>
<a href="?{% url_replace request '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>
</th>
<th>
{% trans "Redemptions" %}
<a href="?{% url_replace request 'filter-ordering' '-redeemed' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'filter-ordering' 'redeemed' %}"><i class="fa fa-caret-up"></i></a>
<a href="?{% url_replace request '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>
</th>
<th>
{% trans "Expiry" %}
<a href="?{% url_replace request 'filter-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-up"></i></a>
<a href="?{% url_replace request '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>
</th>
<th>
{% trans "Tag" %}
<a href="?{% url_replace request 'filter-ordering' '-tag' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'filter-ordering' 'tag' %}"><i class="fa fa-caret-up"></i></a>
<a href="?{% url_replace request '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>
</th>
<th>
{% trans "Product" %}
<a href="?{% url_replace request 'filter-ordering' '-item' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'filter-ordering' 'item' %}"><i class="fa fa-caret-up"></i></a>
<a href="?{% url_replace request 'ordering' '-item' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'item' %}"><i class="fa fa-caret-up"></i></a>
</th>
{% if request.event.has_subevents %}
<th>
{% trans "Date" context "subevent" %}
<a href="?{% url_replace request 'filter-ordering' '-subevent' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'filter-ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
<a href="?{% url_replace request '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>
</th>
{% endif %}
<th></th>
+15 -6
View File
@@ -65,7 +65,6 @@ from pretix.base.forms.auth import (
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
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
logger = logging.getLogger(__name__)
@@ -320,12 +319,19 @@ class Forgot(TemplateView):
if self.form.is_valid():
email = self.form.cleaned_data['email']
has_redis = settings.HAS_REDIS
try:
user = User.objects.get(is_active=True, auth_backend='native', email__iexact=email)
if rate_limit("pwreset", user.pk, max_num=1, expire_time=3600 * 24):
user.log_action('pretix.control.auth.user.forgot_password.denied.repeated')
raise RepeatedResetDenied()
if has_redis:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
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:
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
@@ -338,7 +344,6 @@ class Forgot(TemplateView):
user.log_action('pretix.control.auth.user.forgot_password.mail_sent')
finally:
has_redis = settings.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. '
'Please note that we will send at most one email every 24 hours.'))
@@ -407,7 +412,11 @@ class Recover(TemplateView):
messages.success(request, _('You can now login using your new password.'))
user.log_action('pretix.control.auth.user.forgot_password.recovered')
rate_limit_reset("pwreset", user.pk)
has_redis = settings.HAS_REDIS
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')
else:
return self.get(request, *args, **kwargs)
+20 -35
View File
@@ -49,7 +49,7 @@ from django.shortcuts import render
from django.template.loader import get_template
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.html import conditional_escape, escape, format_html
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext, pgettext
@@ -112,7 +112,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
return [
{
'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(tickc), text=_('Attendees (ordered)')),
'content': None if lazy else NUM_WIDGET.format(num=intcomma(tickc), text=_('Attendees (ordered)')),
'lazy': 'attendees-ordered',
'display_size': 'small',
'priority': 100,
@@ -122,7 +122,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(paidc), text=_('Attendees (paid)')),
'content': None if lazy else NUM_WIDGET.format(num=intcomma(paidc), text=_('Attendees (paid)')),
'lazy': 'attendees-paid',
'display_size': 'small',
'priority': 100,
@@ -132,8 +132,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': None if lazy else format_html(
NUM_WIDGET,
'content': None if lazy else NUM_WIDGET.format(
num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
text=_('Total revenue ({currency})').format(currency=sender.currency)
),
@@ -146,7 +145,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
},
{
'content': None if lazy else format_html(NUM_WIDGET, num=prodc, text=_('Active products')),
'content': None if lazy else NUM_WIDGET.format(num=prodc, text=_('Active products')),
'lazy': 'active-products',
'display_size': 'small',
'priority': 100,
@@ -210,8 +209,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]))
widgets.append({
'content': None if lazy else format_html(
NUM_WIDGET, num=intcomma(happy), text=_('available to give to people on waiting list')
'content': None if lazy else NUM_WIDGET.format(
num=intcomma(happy), text=_('available to give to people on waiting list')
),
'lazy': 'waitinglist-avail',
'priority': 50,
@@ -221,9 +220,7 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
})
})
widgets.append({
'content': None if lazy else format_html(
NUM_WIDGET, num=intcomma(wles.count()), text=_('total waiting list length')
),
'content': None if lazy else NUM_WIDGET.format(num=intcomma(wles.count()), text=_('total waiting list length')),
'lazy': 'waitinglist-length',
'display_size': 'small',
'priority': 50,
@@ -250,10 +247,9 @@ def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
if not lazy:
status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
widgets.append({
'content': None if lazy else format_html(
NUM_WIDGET,
'content': None if lazy else NUM_WIDGET.format(
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
text=format_html(_('{quota} left'), quota=q.name)
text=_('{quota} left').format(quota=escape(q.name))
),
'lazy': 'quota-{}'.format(q.pk),
'display_size': 'small',
@@ -272,8 +268,7 @@ def shop_state_widget(sender, **kwargs):
return [{
'display_size': 'small',
'priority': 1000,
'content': format_html(
'<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>',
'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
t1=_('Your ticket shop is'), t2=_('Click here to change'),
state=_('live') if sender.live and not sender.testmode else (
_('live and in test mode') if sender.live else (
@@ -304,10 +299,9 @@ def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
qs = sender.checkin_lists.filter(subevent=subevent)
for cl in qs:
widgets.append({
'content': None if lazy else format_html(
NUM_WIDGET,
'content': None if lazy else NUM_WIDGET.format(
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
text=format_html(_('Present {list}'), list=cl.name)
text=_('Present {list}').format(list=escape(cl.name))
),
'lazy': 'checkin-{}'.format(cl.pk),
'display_size': 'small',
@@ -346,12 +340,6 @@ 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):
from pretix.control.forms.event import CommentForm
@@ -430,7 +418,7 @@ def event_index_widgets_lazy(request, organizer, event):
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
widgets.extend(result)
return build_json_response(widgets)
return JsonResponse({'widgets': widgets})
def event_index_log_lazy(request, organizer, event):
@@ -517,7 +505,7 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
<a href="{url}" class="event">
<div class="name">{event}</div>
<div class="daterange">{daterange}</div>
<div class="times">{times}{timezone}</div>
<div class="times">{times}</div>
</a>
<div class="bottomrow">
{orders}
@@ -561,16 +549,14 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
status = ('success', _('On sale'))
widgets.append({
'content': format_html(
tpl,
'content': tpl.format(
event=escape(event.name),
times=_('Event series') if event.has_subevents else (
((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
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 '')
),
timezone=(
format_html(' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>', tzname)
) + (
' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>'.format(tzname)
if tzname != request.timezone and not event.has_subevents else ''
),
url=reverse('control:event.index', kwargs={
@@ -578,8 +564,7 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
'organizer': event.organizer.slug
}),
orders=(
format_html(
'<a href="{orders_url}" class="orders">{orders_text}</a>',
'<a href="{orders_url}" class="orders">{orders_text}</a>'.format(
orders_url=reverse('control:event.orders', kwargs={
'event': event.slug,
'organizer': event.organizer.slug
@@ -646,7 +631,7 @@ def user_index_widgets_lazy(request):
request.user,
8
)
return build_json_response(widgets)
return JsonResponse({'widgets': widgets})
def user_index(request):
+9 -24
View File
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
from decimal import Decimal
from io import BytesIO
from itertools import groupby
from urllib.parse import quote, urlsplit
from urllib.parse import urlsplit
from zoneinfo import ZoneInfo
import bleach
@@ -82,7 +82,7 @@ from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
from pretix.base.models.event import EventMetaValue
from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.signals import get_defining_app, register_ticket_outputs
from pretix.base.signals import register_ticket_outputs
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.control.forms.event import (
CancelSettingsForm, CommentForm, ConfirmTextFormset, EventDeleteForm,
@@ -441,7 +441,6 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
plugins_available = {
p.module: p for p in self.available_plugins(self.object)
}
plugin_enabled = None
with transaction.atomic():
save_organizer = False
@@ -491,7 +490,6 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
format_html(_('The plugin {} is now active.'),
format_html("<strong>{}</strong>", pluginmeta.name)),
]
plugin_enabled = module
messages.success(self.request, mark_safe("".join(info)))
else:
self.request.event.log_action('pretix.event.plugins.disabled', user=self.request.user,
@@ -501,19 +499,13 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
self.object.save()
if save_organizer:
self.object.organizer.save()
return redirect(self.get_success_url(plugin_enabled))
return redirect(self.get_success_url())
def get_success_url(self, plugin_enabled) -> str:
if plugin_enabled and self.request.POST.get('go') == 'payment':
return reverse('control:event.settings.payment', kwargs={
'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,
})
def get_success_url(self) -> str:
return reverse('control:event.settings.plugins', kwargs={
'organizer': self.request.organizer.slug,
'event': self.request.event.slug,
})
class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
@@ -679,8 +671,6 @@ 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'])]
if p.is_meta:
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
@@ -962,12 +952,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
context=context,
)
r = HttpResponse(v, content_type='text/html')
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:"
)
r._csp_ignore = True
return r
else:
raise Http404(_('Unknown email renderer.'))
+4 -75
View File
@@ -41,30 +41,6 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
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):
try:
r = dns.resolver.Resolver()
@@ -73,7 +49,7 @@ def get_spf_record(hostname):
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
return data
except:
logger.exception("Could not fetch SPF record for {}".format(hostname))
logger.exception("Could not fetch SPF record")
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
@@ -192,15 +168,10 @@ class MailSettingsSetupView(TemplateView):
return super().get(request, *args, **kwargs)
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 = (
(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
(not verify_dns or self.request.POST.get('state') == 'save')
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
)
if allow_save:
@@ -221,8 +192,8 @@ class MailSettingsSetupView(TemplateView):
spf_warning = None
spf_record = None
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
spf_record = get_spf_record(hostname)
if not spf_record:
spf_warning = _(
@@ -239,43 +210,7 @@ class MailSettingsSetupView(TemplateView):
'this system in the SPF record.'
)
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
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
if verification:
if 'verification' in self.request.POST:
messages.error(request, _('The verification code was incorrect, please try again.'))
@@ -306,12 +241,6 @@ class MailSettingsSetupView(TemplateView):
'spf_warning': spf_warning,
'spf_record': spf_record,
'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')
},
using=self.template_engine,
+5 -11
View File
@@ -64,7 +64,7 @@ from django.urls import reverse
from django.utils import formats
from django.utils.formats import date_format, get_format
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, escape, format_html
from django.utils.html import conditional_escape, escape
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
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.i18n import language
from pretix.base.models import (
CachedFile, CachedTicket, Checkin, GiftCard, Invoice, InvoiceAddress, Item,
CachedFile, CachedTicket, Checkin, Invoice, InvoiceAddress, Item,
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
ScheduledEventExport, generate_secret,
)
@@ -551,10 +551,10 @@ class OrderDetail(OrderView):
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
for p in ctx['payments']:
if p.payment_provider:
p.html_info = p.payment_provider.payment_control_render(self.request, p) or ""
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
for r in ctx['refunds']:
if r.payment_provider:
r.html_info = r.payment_provider.refund_control_render(self.request, r) or ""
r.html_info = (r.payment_provider.refund_control_render(self.request, r) or "").strip()
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
ctx['comment_form'] = CommentForm(initial={
'comment': self.order.comment,
@@ -1994,7 +1994,7 @@ class OrderChange(OrderView):
positions = list(self.order.positions.select_related(
'item', 'item__tax_rule', 'used_membership', 'used_membership__membership_type', 'tax_rule',
'seat', 'subevent',
).prefetch_related('granted_memberships', 'issued_gift_cards'))
).prefetch_related('granted_memberships'))
for p in positions:
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p, items=self.items,
initial={'seat': p.seat.seat_guid if p.seat else None},
@@ -2261,12 +2261,6 @@ class OrderContactChange(OrderView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
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
@cached_property
+1
View File
@@ -70,6 +70,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
if 'placeholders' in request.GET:
return self.get_placeholders_help(request)
resp = super().get(request, *args, **kwargs)
resp._csp_ignore = True
return resp
def get_placeholders_help(self, request):
+1 -2
View File
@@ -85,7 +85,6 @@ from pretix.control.views import PaginationMixin
from pretix.control.views.event import MetaDataEditorMixin
from pretix.helpers import GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.i18n import get_format_without_seconds
from pretix.helpers.models import modelcopy
@@ -879,7 +878,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
ctx['rrule_formset'] = self.rrule_formset
ctx['time_formset'] = self.time_formset
tf = get_format_without_seconds('TIME_INPUT_FORMATS')
tf = get_format('TIME_INPUT_FORMATS')[0]
ctx['time_admission_sample'] = time(8, 30, 0).strftime(tf)
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
-7
View File
@@ -80,7 +80,6 @@ from pretix.control.permissions import (
)
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
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.u2f import websafe_encode
@@ -850,7 +849,6 @@ class UserPasswordChangeView(FormView):
msgs = []
msgs.append(_('Your password has been changed.'))
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})
@@ -881,7 +879,6 @@ class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
return {
**super().get_form_kwargs(),
"request": self.request,
"user": self.request.user,
}
@@ -911,10 +908,6 @@ class UserEmailVerifyView(View):
messages.success(self.request, _('Your email address was already verified.'))
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(
session=self.request.session,
reason='email_verify',
+1 -1
View File
@@ -47,5 +47,5 @@ def escapejson(value):
@keep_lazy(str, SafeText)
def escapejson_attr(value):
"""Hex encodes characters for use in a html attribute."""
"""Hex encodes characters for use in a html attributw script."""
return mark_safe(force_str(value).translate(_json_escapes_attr))
-116
View File
@@ -1,116 +0,0 @@
#
# 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)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+183 -132
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
"Last-Translator: ahmed badr <ahmedbadr@oma.com.eg>\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
"ar/>\n"
"Language: ar\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"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"
"X-Generator: Weblate 2026.7.1\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -30,110 +30,112 @@ msgstr "تعليق:"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayPal"
msgstr "باي بال"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Venmo"
msgstr "Venmo"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/static/pretixpresale/js/walletdetection.js
msgid "Apple Pay"
msgstr "Apple Pay"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Itaú"
msgstr "إيتاو"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayPal Credit"
msgstr "الائتمان من PayPal"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Credit Card"
msgstr "بطاقة الائتمان"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayPal Pay Later"
msgstr "الدفع لاحقًا عبر PayPal"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "iDEAL | Wero"
msgstr "iDEAL | التحدي"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "SEPA Direct Debit"
msgstr "الخصم المباشر في منطقة الدفع الموحدة (SEPA)"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Bancontact"
msgstr "بانكونتاكت"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "giropay"
msgstr "giropay"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "SOFORT"
msgstr "فوراً"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#, fuzzy
#| msgid "Yes"
msgid "eps"
msgstr "نقاط إضافية"
msgstr "نعم"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "MyBank"
msgstr "بنكي"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Przelewy24"
msgstr "Przelewy24"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Verkkopankki"
msgstr "الخدمات المصرفية عبر الإنترنت"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "PayU"
msgstr "الدفع لك"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "BLIK"
msgstr "BLIK"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Trustly"
msgstr "Trustly"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Zimpler"
msgstr "Zimpler"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Maxima"
msgstr "الأعظم"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "OXXO"
msgstr "OXXO"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Boleto"
msgstr "تذكرة"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "WeChat Pay"
msgstr "WeChat Pay"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Mercado Pago"
msgstr "Mercado Pago"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Continue"
msgstr "تابع"
msgstr "المتابعة"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
@@ -142,7 +144,7 @@ msgstr "جاري تأكيد الدفع الخاص بك …"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
msgid "Payment method unavailable"
msgstr "طريقة الدفع غير متاحة"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Placed orders"
@@ -154,11 +156,11 @@ msgstr "الطلبات المدفوعة"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (ordered)"
msgstr "الحاضرون (حسب الترتيب)"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Attendees (paid)"
msgstr "الحاضرون (الذين دفعوا رسوم الاشتراك)"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js
msgid "Total revenue"
@@ -190,7 +192,7 @@ msgstr "تبديل قائمة الدخول"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Search results"
msgstr "نتائج البحث"
msgstr "البحث في النتائج"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "No tickets found"
@@ -234,15 +236,15 @@ msgstr "غير مدفوع"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Canceled"
msgstr "تم إلغاؤه"
msgstr "ملغاة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Confirmed"
msgstr "تم التأكيد"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Approval pending"
msgstr "في انتظار الموافقة"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Redeemed"
@@ -250,11 +252,11 @@ msgstr "مستخدم"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Cancel"
msgstr "إلغاء"
msgstr "قم بالإلغاء"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket not paid"
msgstr "التذكرة لم يتم دفع ثمنها"
msgstr "لم يتم دفع قيمة التذكرة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "This ticket is not yet paid. Do you want to continue anyways?"
@@ -274,19 +276,23 @@ msgstr "تم تسجيل الخروج"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket already used"
msgstr "تم استخدام التذكرة بالفعل"
msgstr "تم استخدام التذكرة مسبقا"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Information required"
msgstr "المعلومات المطلوبة"
msgstr "معلومات مطلوبة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Unknown error."
msgid "Unknown ticket"
msgstr "تذكرة غير معروفة"
msgstr "خطأ غير معروف."
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Entry not allowed"
msgid "Ticket type not allowed here"
msgstr "نوع تذكرة الدخول غير مسموح به هنا"
msgstr "إدخال غير مسموح"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Entry not allowed"
@@ -294,13 +300,17 @@ msgstr "إدخال غير مسموح"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket code revoked/changed"
msgstr "تم إلغاء/تغيير رمز التذكرة"
msgstr "تم إلغاء رمز التذكرة أو تبديله"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket blocked"
msgstr "التذكرة غير محجوبة"
msgstr "لم يتم دفع قيمة التذكرة"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket not valid at this time"
msgstr "لم يتم دفع قيمة التذكرة"
@@ -310,11 +320,11 @@ msgstr "تم إلغاء الطلب"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Ticket code is ambiguous on list"
msgstr "رمز التذكرة غير واضح في القائمة"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Order not approved"
msgstr "لم تتم الموافقة على الطلب"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Checked-in Tickets"
@@ -338,9 +348,12 @@ msgstr "نعم"
#: pretix/static/pretixcontrol/js/ui/question.js
#: pretix/static/pretixpresale/js/ui/questions.js
msgid "No"
msgstr "من"
msgstr "لا"
#: pretix/static/lightbox/js/lightbox.js
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "إغلاق"
@@ -395,7 +408,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js
msgid "We are processing your request …"
msgstr "نحن نقوم بمعالجة طلبك …"
msgstr "جاري معالجة طلبك …"
#: pretix/static/pretixbase/js/asynctask.js
msgid ""
@@ -408,7 +421,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js
msgid "If this takes longer than a few minutes, please contact us."
msgstr "إذا استغرق ذلك أكثر من بضع دقائق، يرجى الاتصال بنا."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js
msgid "Close message"
@@ -424,11 +437,11 @@ msgstr "للنسخ اضغط Ctrl + C!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Edit"
msgstr "تحرير"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Visualize"
msgstr "تصور"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid ""
@@ -436,13 +449,10 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"تقوم القاعدة الخاصة بك دائمًا بالتصفية حسب المنتج أو النوع، لكن المنتجات أو "
"الأنواع التالية غير مدرجة في أي من أجزاء القاعدة الخاصة بك، لذا لن يتم قبول "
"الأشخاص الذين يحملون هذه التذاكر:"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Please double-check if this was intentional."
msgstr "يرجى التأكد مرة أخرى مما إذا كان ذلك مقصودًا أم لا."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "All of the conditions below (AND)"
@@ -482,21 +492,21 @@ msgstr "أضف شرطا"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "minutes"
msgstr "دقائق"
msgstr "الدقائق"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Duplicate"
msgstr "مكرر"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgctxt "entry_status"
msgid "present"
msgstr "حاضر"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgctxt "entry_status"
msgid "absent"
msgstr "غائب"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "is one of"
@@ -512,11 +522,11 @@ msgstr "بعد"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "="
msgstr "="
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Product"
msgstr "المنتج"
msgstr "منتج"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Product variation"
@@ -524,7 +534,7 @@ msgstr "نوع المنتج"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Gate"
msgstr "الشارع"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current date and time"
@@ -532,11 +542,11 @@ msgstr "التاريخ والوقت الحالي"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
msgstr "اليوم الحالي من الأسبوع (1 = الاثنين، 7 = الأحد)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Current entry status"
msgstr "الحالة الحالية للطلب"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Number of previous entries"
@@ -547,40 +557,48 @@ msgid "Number of previous entries since midnight"
msgstr "عدد المدخلات السابقة قبل منتصف الليل"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries since"
msgstr "عدد الإدخالات السابقة منذ"
msgstr "عدد المدخلات السابقة"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#, fuzzy
#| msgid "Number of previous entries"
msgid "Number of previous entries before"
msgstr "عدد الإدخالات السابقة قبل"
msgstr "عدد المدخلات السابقة"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Number of days with a previous entry"
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
#: 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"
msgstr "عدد الأيام التي تحتوي على قيد سابق منذ"
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
#: 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"
msgstr "عدد الأيام التي يوجد فيها قيد سابق قبل ذلك"
msgstr "عدد الأيام التي تحتوي على مدخل سابق"
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Minutes since last entry (-1 on first entry)"
msgstr "عدد الدقائق منذ آخر تسجيل (-1 عند التسجيل الأول)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "Minutes since first entry (-1 on first entry)"
msgstr "عدد الدقائق منذ أول دخول (-1 عند أول دخول)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
msgid "Error: Product not found!"
msgstr "خطأ: لم يتم العثور على المنتج!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
msgid "Error: Variation not found!"
msgstr "خطأ: لم يتم العثور على النسخة!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js
msgid "Check-in QR"
@@ -595,12 +613,16 @@ msgid "Group of objects"
msgstr "مجموعة من العناصر"
#: pretix/static/pretixcontrol/js/ui/editor.js
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "كائن نصي (مهمل)"
msgstr "عنصر نص"
#: pretix/static/pretixcontrol/js/ui/editor.js
#, fuzzy
#| msgid "Text object"
msgid "Text box"
msgstr "مربع نصي"
msgstr "عنصر نص"
#: pretix/static/pretixcontrol/js/ui/editor.js
msgid "Barcode area"
@@ -647,28 +669,28 @@ msgid "Unknown error."
msgstr "خطأ غير معروف."
#: 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."
msgstr "يتميز لونك بتباين رائع ويسهل قراءته للغاية! وسيوفر إمكانية وصول ممتازة."
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
#: pretix/static/pretixcontrol/js/ui/main.js
#, fuzzy
#| msgid "Your color has decent contrast and is probably good-enough to read!"
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
"يتميز لونك بتباين جيد، ومن المرجح أنه مناسب للقراءة! وهو كافٍ لتلبية الحد "
"الأدنى من متطلبات سهولة الوصول."
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
#: pretix/static/pretixcontrol/js/ui/main.js
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
"اللون الذي اخترته لا يتمتع بتباين كافٍ مع اللون الأبيض. وسيؤثر ذلك على إمكانية "
"الوصول إلى موقعك."
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Search query"
msgstr "استعلام البحث"
msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "All"
@@ -684,11 +706,11 @@ msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Enter page number between 1 and %(max)s."
msgstr "أدخل رقم الصفحة بين 1 و %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Invalid page number."
msgstr "رقم الصفحة غير صحيح."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js
msgid "Use a different name internally"
@@ -707,8 +729,10 @@ msgid "Calculating default price…"
msgstr "حساب السعر الافتراضي…"
#: pretix/static/pretixcontrol/js/ui/plugins.js
#, fuzzy
#| msgid "Search results"
msgid "No results"
msgstr "البحث: لا توجد نتائج"
msgstr "البحث في النتائج"
#: pretix/static/pretixcontrol/js/ui/question.js
msgid "Others"
@@ -716,7 +740,7 @@ msgstr "غير ذلك"
#: pretix/static/pretixcontrol/js/ui/question.js
msgid "Count"
msgstr "العدد"
msgstr "احسب"
#: pretix/static/pretixcontrol/js/ui/subevent.js
msgid "(one more date)"
@@ -729,12 +753,12 @@ msgstr[4] "أيام عديدة"
msgstr[5] "أخرى"
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"لم تعد المنتجات الموجودة في سلة التسوق محجوزة لك. لا يزال بإمكانك إتمام طلبك "
"طالما كانت هذه المنتجات متوفرة."
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Cart expired"
@@ -742,9 +766,13 @@ msgstr "انتهت صلاحية عربة التسوق"
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Your cart is about to expire."
msgstr "سوف تنتهي صلاحية سلة التسوق الخاصة بك قريبًا."
msgstr ""
#: 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_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
@@ -755,24 +783,26 @@ msgstr[4] "سيتم حجز العناصر لك في سلة التسوق لدقا
msgstr[5] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "Cart expired"
msgid "Your cart has expired."
msgstr "انتهت صلاحية عربة التسوق"
#: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as they're available."
msgstr ""
"لم تعد المنتجات الموجودة في سلة التسوق محجوزة لك. لا يزال بإمكانك إتمام طلبك "
"طالما كانت هذه المنتجات متوفرة."
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Do you want to renew the reservation period?"
msgstr "هل ترغب في تمديد فترة الحجز؟"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js
msgid "Renew reservation"
msgstr "تجديد الحجز"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -792,77 +822,82 @@ msgstr "التوقيت المحلي:"
#: pretix/static/pretixpresale/js/walletdetection.js
msgid "Google Pay"
msgstr "Google Pay"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Quantity"
msgstr "الكمية"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Decrease quantity"
msgstr "تقليل الكمية"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Increase quantity"
msgstr "زيادة الكمية"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Filter events by"
msgstr "تصفية الأحداث حسب"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Filter"
msgstr "فلتر"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Price"
msgstr "السعر"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr "السعر الأصلي: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr "السعر الجديد: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgid "Selected only"
msgctxt "widget"
msgid "Select"
msgstr "المختارة فقط"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
#, fuzzy, javascript-format
#| msgid "Selected only"
msgctxt "widget"
msgid "Select %s"
msgstr "تم اختيار %s فقط"
msgstr "المختارة فقط"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, javascript-format
#, fuzzy, javascript-format
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget"
msgid "Select variant %s"
msgstr "اختر النوع %s"
msgstr "أنظر إلى الاختلافات"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -905,7 +940,7 @@ msgstr "من %(currency) s %(price)s"
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr "صورة لـ %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -946,21 +981,27 @@ msgstr "متوفرة مع القسيمة فقط"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget"
msgid "Not yet available"
msgstr "حاليًا غير متوفر: %s"
msgstr "متوفر حاليا: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "Not available anymore"
msgstr "لم يعد متوفراً"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "currently available: %s"
msgctxt "widget"
msgid "Currently not available"
msgstr "غير متوفر حاليًا: %s"
msgstr "متوفر حاليا: %s"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -988,20 +1029,24 @@ msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
"يوجد حالياً عدد كبير من المستخدمين في متجر التذاكر هذا. يرجى فتح المتجر في "
"علامة تبويب جديدة للمتابعة."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Close ticket shop"
msgctxt "widget"
msgid "Open ticket shop"
msgstr "إغلاق/فتح متجر التذاكر"
msgstr "إغلاق متجر التذاكر"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Resume checkout"
msgctxt "widget"
msgid "Checkout"
msgstr "استئناف عملية الدفع"
msgstr "استئناف الدفع"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1016,9 +1061,6 @@ msgid ""
"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."
msgstr ""
"لم نتمكن من إنشاء سلة التسوق الخاصة بك، نظرًا لوجود عدد كبير جدًّا من "
"المستخدمين حاليًّا في متجر التذاكر هذا. يرجى النقر على «متابعة» لإعادة المحاولة "
"في علامة تبويب جديدة."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1068,15 +1110,18 @@ msgstr "إغلاق"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "Resume checkout"
msgctxt "widget"
msgid "Close checkout"
msgstr "إغلاق عملية الدفع"
msgstr "استئناف الدفع"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr "لا يمكنك إلغاء هذه العملية. يرجى الانتظار حتى ينتهي التحميل."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1086,15 +1131,21 @@ msgstr "استمرار"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget"
msgid "Show variants"
msgstr "انظر الاختلافات"
msgstr "أنظر إلى الاختلافات"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget"
msgid "Hide variants"
msgstr "إخفاء الاختلافات"
msgstr "أنظر إلى الاختلافات"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1152,11 +1203,11 @@ msgid ""
"add yourself to the waiting list. We will then notify if seats are available "
"again."
msgstr ""
"تم بيع جميع فئات التذاكر أو بعضها حاليًا. إذا أردت، يمكنك إضافة اسمك إلى "
"قائمة الانتظار. وسنقوم بإخطارك إذا توفرت مقاعد مرة أخرى."
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
#, fuzzy
#| msgid "Load more"
msgctxt "widget"
msgid "Load more"
msgstr "تحميل المزيد"
@@ -1199,37 +1250,37 @@ msgstr "الأحد"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Monday"
msgstr "الاثنين"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Tuesday"
msgstr "الثلاثاء"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Wednesday"
msgstr "الأربعاء"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Thursday"
msgstr "الخميس"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Friday"
msgstr "الجمعة"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Saturday"
msgstr "السبت"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "Sunday"
msgstr "الأحد"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
@@ -1254,7 +1305,7 @@ msgstr "أبريل"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
msgid "May"
msgstr "هناك"
msgstr "مايو"
#: pretix/static/pretixpresale/js/widget/widget.js
#: pretix/static/pretixpresale/widget/src/i18n.ts
+4 -4
View File
@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-15 14:20+0000\n"
"Last-Translator: Jochen Siebert <siebert@rami.io>\n"
"PO-Revision-Date: 2026-07-07 09:55+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.7.1\n"
"X-Generator: Weblate 2026.6.1\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
@@ -8166,7 +8166,7 @@ msgstr "Zugang zu existierenden Veranstaltungen"
msgctxt "permission_level"
msgid "Access existing and create new events"
msgstr ""
"Zugang zu existierenden Veranstaltungen und neue Veranstaltungen erstellen"
"Zugang zu existieren Veranstaltungen und neue Veranstaltungen erstellen"
#: pretix/base/permissions.py
msgid ""
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-07-13 11:28+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"PO-Revision-Date: 2026-05-04 07:42+0000\n"
"Last-Translator: Martin Gross <gross@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n"
"Language: de_Informal\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.7.1\n"
"X-Generator: Weblate 5.17\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid"
@@ -430,11 +430,11 @@ msgstr "Drücke Strg+C zum kopieren!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Edit"
msgstr "Bearbeiten"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid "Visualize"
msgstr "Visualisieren"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
msgid ""
@@ -442,13 +442,10 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
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
msgid "Please double-check if this was intentional."
msgstr "Bitte überprüfe, ob dies beabsichtigt war."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
msgid "All of the conditions below (AND)"
+31 -15
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -11453,8 +11453,10 @@ msgstr ""
"contacto con usted."
#: pretix/base/settings.py pretix/control/forms/event.py
#, fuzzy
#| msgid "Contact"
msgid "Contact URL"
msgstr "URL de contacto"
msgstr "Contacto"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid ""
@@ -11462,10 +11464,6 @@ msgid ""
"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."
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
msgid "Imprint URL"
@@ -19948,7 +19946,25 @@ msgstr ""
"El equipo de %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
#, python-format
#, fuzzy, 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 ""
"Hello,\n"
"\n"
@@ -19971,19 +19987,19 @@ msgid ""
msgstr ""
"Hola,\n"
"\n"
"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 podrán mostrar esta dirección como remitente.\n"
"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 podrán mostrar esta dirección como remitente.\n"
"\n"
"Si ha sido usted quien lo ha solicitado, introduzca el siguiente código en "
"el formulario de configuración:\n"
"Si has sido , introduce el siguiente código en el formulario de "
"configuración:\n"
"\n"
" %(code)s\n"
"\n"
"No comparta este código con nadie a menos que quiera autorizarle a utilizar "
"esta dirección para este fin. El equipo de %(instance)s nunca se lo pedirá.\n"
"No compartas este código con nadie. El equipo de %(instance)s nunca te lo "
"pedirá.\n"
"\n"
"Si no ha solicitado esto, puede ignorar este correo electrónico sin ningún "
"Si no has solicitado esto, puedes ignorar este correo electrónico sin ningún "
"problema.\n"
"\n"
"Gracias, \n"
+39 -33
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
"PO-Revision-Date: 2026-07-07 09:22+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -1933,7 +1933,7 @@ msgid ""
"redeemed."
msgstr ""
"Ce produit ne sera affiché que si un bon de réduction correspondant au "
"produit est validé."
"produit est échangé."
#: pretix/base/exporters/items.py pretix/base/models/items.py
msgid "Buying this product requires approval"
@@ -4109,8 +4109,8 @@ msgstr ""
msgid ""
"To verify your email address {email} on {instance}, use the following code:"
msgstr ""
"Pour valider votre adresse e-mail {email} sur {instance}, utilisez le code "
"suivant :"
"Pour valider votre adresse e-mail {email} sur\n"
"{instance}, utilisez le code suivant :"
#: pretix/base/models/auth.py
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
msgid "Redeemed"
msgstr "Validé"
msgstr "Échangé"
#: pretix/base/models/vouchers.py
msgid ""
@@ -11542,8 +11542,10 @@ msgstr ""
"contacter."
#: pretix/base/settings.py pretix/control/forms/event.py
#, fuzzy
#| msgid "Contact"
msgid "Contact URL"
msgstr "URL de contact"
msgstr "Contact"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid ""
@@ -11551,10 +11553,6 @@ msgid ""
"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."
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
msgid "Imprint URL"
@@ -20096,7 +20094,25 @@ msgstr ""
"L'équipe %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
#, python-format
#, fuzzy, 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 ""
"Hello,\n"
"\n"
@@ -20120,23 +20136,22 @@ msgstr ""
"Bonjour,\n"
"\n"
"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 pourront afficher cette adresse comme expéditeur.\n"
"%(instance)s. Une fois cette demande validée, les e-mails envoyés depuis "
"%(instance)s pourront afficher cette adresse comme expéditeur.\n"
"\n"
"Si c'est bien vous qui en avez fait la demande, veuillez saisir le code "
"suivant dans le formulaire de configuration :\n"
"Si c'est vous qui avez fait cette demande, veuillez saisir le code suivant "
"dans le formulaire de configuration :\n"
"\n"
" %(code)s\n"
"\n"
"Ne communiquez ce code à personne, sauf si vous souhaitez autoriser cette "
"personne à utiliser cette adresse à cette fin. L'équipe de %(instance)s ne "
"vous le demandera jamais.\n"
"Ne communiquez ce code à personne. L'équipe de %(instance)s ne vous le "
"demandera jamais.\n"
"\n"
"Si vous n'en avez pas fait la demande, vous pouvez ignorer cet e-mail en "
"Si vous nen avez pas fait la demande, vous pouvez ignorer cet e-mail en "
"toute sécurité.\n"
"\n"
"Merci, \n"
"L'équipe de %(instance)s\n"
"Léquipe %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/forgot.txt
#, python-format
@@ -36815,19 +36830,17 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid "How to fix this:"
msgstr "Comment résoudre ce problème :"
msgstr ""
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid "Close this page."
msgstr "Fermer cette page."
msgstr ""
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid ""
"Open this website again directly in your main browser (for example Safari or "
"Chrome)."
msgstr ""
"Rouvrez ce site directement dans votre navigateur principal (par exemple "
"Safari ou Chrome)."
#: pretix/presale/templates/pretixpresale/organizers/customer_login_interrupted_message.html
msgid ""
@@ -36835,9 +36848,6 @@ msgid ""
"browser window, without refreshing the page, opening new tabs, or switching "
"apps part-way through."
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
msgid ""
@@ -36845,10 +36855,6 @@ msgid ""
"before retrying can help. As a last step, try using a different browser or "
"another device (for example, a desktop or laptop computer)."
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
msgid "Your membership"
+3 -3
View File
@@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-07-08 16:00+0000\n"
"Last-Translator: Joram Schwartzmann <schwartzmann@pretix.eu>\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\n"
"Language: fr\n"
@@ -244,7 +244,7 @@ msgstr "En attente d'approbation"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Redeemed"
msgstr "Validé"
msgstr "Échangé"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
msgid "Cancel"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+29 -13
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-14 15:00+0000\n"
"PO-Revision-Date: 2026-07-07 09:17+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.7.1\n"
"X-Generator: Weblate 2026.6.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html
#: pretix/control/templates/pretixcontrol/events/index.html
@@ -11076,8 +11076,10 @@ msgid "We'll show this publicly to allow attendees to contact you."
msgstr "参加者があなたに連絡できるように、これを公に表示します。"
#: pretix/base/settings.py pretix/control/forms/event.py
#, fuzzy
#| msgid "Contact"
msgid "Contact URL"
msgstr "連絡先URL"
msgstr "連絡先"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid ""
@@ -11085,9 +11087,6 @@ msgid ""
"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."
msgstr ""
"これを設定すると、フッターの連絡先リンクが上記のメールアドレスではなくこちら"
"を指すことになります。追加が必要な連絡先メールアドレスは、送信するすべての電"
"子メールで共有されるのでご注意ください。"
#: pretix/base/settings.py pretix/control/forms/event.py
msgid "Imprint URL"
@@ -19361,7 +19360,25 @@ msgstr ""
"%(instance)sのチーム\n"
#: pretix/control/templates/pretixcontrol/email/email_setup.txt
#, python-format
#, fuzzy, 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 ""
"Hello,\n"
"\n"
@@ -19384,17 +19401,16 @@ msgid ""
msgstr ""
"こんにちは、\n"
"\n"
"誰かが %(address)s を送信者アドレスとして %(instance)s に使用するよう"
"リクエストしました。検証が完了すると、%(instance)s から送信されたメールはこの"
"アドレスを送信者として表示できます。\n"
"誰かが %(address)s を送信者アドレスとして %(instance)s に使用するようリクエス"
"トしました。検証が完了すると、%(instance)s から送信されたメールはこのアドレス"
"を送信者として表示できます。\n"
"\n"
"もしこれがあなたであれば、設定フォームに以下のコードを入力してください。\n"
"\n"
"…%(code)s\n"
"\n"
"このコードを誰にも共有しないでください。ただし、このアドレスをこの目的で使用"
"することを許可したい場合は除きます。%(instance)sのチームは、決してあなたにそ"
"れを求めることはありません。\n"
"このコードを誰にも共有しないでください。%(instance)sのチームは、決してあなた"
"にそれを求めることはありません。\n"
"\n"
"もしこのメールをご依頼いただいていない場合は、このメールはご無視いただいて構"
"いません。\n"
+1
View File
@@ -162,6 +162,7 @@ class XHRView(View):
paypal_order = prov._create_paypal_order(request, None, cart_total)
r = JsonResponse(paypal_order.dict() if paypal_order else {})
r._csp_ignore = True
return r
@@ -323,7 +323,7 @@ $(function () {
}
}
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
waitingDialog.hide();
@@ -432,4 +432,4 @@ $(function () {
}
}
);
});
});
@@ -9,7 +9,7 @@
<script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script>
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
{% if payment_intent_next_action_redirect_url %}
{{ payment_intent_next_action_redirect_url|json_script:"stripe_payment_intent_next_action_redirect_url" }}
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url|safe }}</script>
{% endif %}
{% if payment_intent_redirect_action_handling %}
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
+2 -2
View File
@@ -87,7 +87,6 @@ from pretix.presale.forms.checkout import (
ContactForm, InvoiceAddressForm, InvoiceNameForm, MembershipForm,
)
from pretix.presale.forms.customer import AuthenticationForm, RegistrationForm
from pretix.presale.productlist import prepare_item_list_for_shop
from pretix.presale.signals import (
checkout_all_optional, checkout_confirm_messages, checkout_flow_steps,
contact_form_fields, contact_form_fields_overrides,
@@ -100,6 +99,7 @@ from pretix.presale.views.cart import (
_items_from_post_data, cart_session, create_empty_cart_id,
get_or_create_cart_id,
)
from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.questions import QuestionsViewMixin
@@ -603,7 +603,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
if ckey not in item_cache:
# Get all items to possibly show
items, _btn = prepare_item_list_for_shop(
items, _btn = get_grouped_items(
self.request.event,
subevent=cartpos.subevent,
voucher=None,
+56 -20
View File
@@ -20,6 +20,8 @@
# <https://www.gnu.org/licenses/>.
#
import functools
import hashlib
import ipaddress
import random
from django import forms
@@ -30,6 +32,7 @@ from django.contrib.auth.password_validation import (
)
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import signing
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
from phonenumber_field.formfields import PhoneNumberField
@@ -41,7 +44,6 @@ from pretix.base.forms.questions import (
from pretix.base.i18n import get_language_without_region
from pretix.base.models import Customer
from pretix.helpers.http import get_client_ip
from pretix.helpers.ratelimit import rate_limit
from pretix.multidomain.urlreverse import eventreverse_absolute
@@ -203,6 +205,23 @@ class RegistrationForm(forms.Form):
min_value=0,
)
@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_customer_registration_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
def clean(self):
email = self.cleaned_data.get('email')
@@ -236,11 +255,17 @@ class RegistrationForm(forms.Form):
code='incomplete'
)
else:
if rate_limit("customer_signup", include_ip_from_request=self.request, max_num=10, expire_time=600):
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if self.ratelimit_key:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.incr(self.ratelimit_key)
rc.expire(self.ratelimit_key, 600)
if cnt > 10:
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
return self.cleaned_data
def create(self):
@@ -334,11 +359,6 @@ class ResetPasswordForm(forms.Form):
def clean_email(self):
if 'email' not in self.cleaned_data:
return
if rate_limit("customer_pwreset_check", max_num=10, expire_time=600):
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
try:
self.customer = self.request.organizer.customers.get(email=self.cleaned_data['email'].lower(), provider__isnull=True)
return self.customer.email
@@ -350,8 +370,13 @@ class ResetPasswordForm(forms.Form):
def clean(self):
d = super().clean()
if d.get('email'):
if rate_limit("customer_pwreset", self.customer.pk, max_num=2, expire_time=600):
if d.get('email') and settings.HAS_REDIS:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.incr('pretix_pwreset_customer_%s' % self.customer.pk)
rc.expire('pretix_pwreset_customer_%s' % self.customer.pk, 600)
if cnt > 2:
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
@@ -420,8 +445,13 @@ class ChangePasswordForm(forms.Form):
def clean_password_current(self):
old_pw = self.cleaned_data.get('password_current')
if old_pw:
if rate_limit("customer_pwchange", self.customer.pk, max_num=10, expire_time=300):
if old_pw and settings.HAS_REDIS:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.incr('pretix_pwchange_customer_%s' % self.customer.pk)
rc.expire('pretix_pwchange_customer_%s' % self.customer.pk, 300)
if cnt > 10:
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
@@ -491,11 +521,17 @@ class ChangeInfoForm(forms.ModelForm):
old_pw = self.cleaned_data.get('password_current')
if old_pw:
if rate_limit("customer_pwchange", self.instance.pk, max_num=10, expire_time=300):
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if settings.HAS_REDIS:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
cnt = rc.incr('pretix_pwchange_customer_%s' % self.instance.pk)
rc.expire('pretix_pwchange_customer_%s' % self.instance.pk, 300)
if cnt > 10:
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if not check_password(old_pw, self.instance.password):
raise forms.ValidationError(
-460
View File
@@ -1,460 +0,0 @@
#
# 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 sys
from datetime import datetime
from typing import Optional
from django.conf import settings
from django.db.models import (
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
)
from django.db.models.lookups import Exact
from pretix.base.models import (
ItemVariation, Quota, SalesChannel, SeatCategoryMapping,
)
from pretix.base.models.items import (
Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.timemachine import time_machine_now
from pretix.presale.signals import item_description
def item_group_by_category(items):
return sorted(
[
# a group is a tuple of a category and a list of items
(cat, [i for i in items if i.category == cat])
for cat in set([i.category for i in items])
# insert categories into a set for uniqueness
# a set is unsorted, so sort again by category
],
key=lambda group: (group[0].position, group[0].id) if (
group[0] is not None and group[0].id is not None) else (0, 0)
)
def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
allow_addons=False, allow_cross_sell=False,
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
ignore_hide_sold_out_for_item_ids=None):
base_qs_set = base_qs is not None
base_qs = base_qs if base_qs is not None else event.items
requires_seat = Exists(
SeatCategoryMapping.objects.filter(
product_id=OuterRef('pk'),
subevent=subevent
)
)
if not event.settings.seating_choice:
requires_seat = Value(0, output_field=IntegerField())
variation_q = (
Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info')) &
Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
)
if not voucher or not voucher.show_hidden_items:
variation_q &= Q(hide_without_voucher=False)
if memberships is not None:
prefetch_membership_types = ['require_membership_types']
else:
prefetch_membership_types = []
prefetch_var = Prefetch(
'variations',
to_attr='available_variations',
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
subevent_disabled=Exists(
SubEventItemVariation.objects.filter(
Q(disabled=True)
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
variation_id=OuterRef('pk'),
subevent=subevent,
)
),
).filter(
variation_q,
Q(all_sales_channels=True) | Q(limit_sales_channels=channel),
Exists(Quota.variations.through.objects.filter(quota__subevent_id=subevent, itemvariation_id=OuterRef("pk"))),
active=True,
subevent_disabled=False
).prefetch_related(
*prefetch_membership_types,
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent).select_related("subevent"))
).distinct()
)
prefetch_quotas = Prefetch(
'quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent).select_related("subevent")
)
prefetch_bundles = Prefetch(
'bundles',
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
Prefetch('bundled_item',
queryset=event.items.using(settings.DATABASE_REPLICA).select_related(
'tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent)),
)),
Prefetch('bundled_variation',
queryset=ItemVariation.objects.using(
settings.DATABASE_REPLICA
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent)),
)),
)
)
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(
channel=channel.identifier, voucher=voucher, allow_addons=allow_addons, allow_cross_sell=allow_cross_sell
).select_related(
'category', 'tax_rule', # for re-grouping
'hidden_if_available',
).prefetch_related(
*prefetch_membership_types,
Prefetch(
'hidden_if_item_available',
queryset=event.items.annotate(
has_variations=Count('variations'),
).prefetch_related(
prefetch_var,
prefetch_quotas,
prefetch_bundles,
)
),
prefetch_quotas,
prefetch_var,
prefetch_bundles,
).annotate(
has_variations=Count('variations'),
subevent_disabled=Exists(
SubEventItem.objects.filter(
Q(disabled=True)
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
item_id=OuterRef('pk'),
subevent=subevent,
)
),
mandatory_priced_addons=Exists(
ItemAddOn.objects.filter(
base_item_id=OuterRef('pk'),
min_count__gte=1,
price_included=False
)
),
requires_seat=requires_seat,
).filter(
Exists(Quota.items.through.objects.filter(quota__subevent_id=subevent, item_id=OuterRef("pk"))),
subevent_disabled=False,
).order_by('category__position', 'category_id', 'position', 'name')
if require_seat:
items = items.filter(requires_seat__gt=0)
elif require_seat is not None:
items = items.filter(requires_seat=0)
if filter_items:
items = items.filter(pk__in=[a for a in filter_items if a.isdigit()])
if filter_categories:
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
display_add_to_cart = False
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
quota_cache_existed = bool(quota_cache)
if subevent:
item_price_override = subevent.item_price_overrides
var_price_override = subevent.var_price_overrides
else:
item_price_override = {}
var_price_override = {}
restrict_vars = set()
if voucher and voucher.quota_id:
# If a voucher is set to a specific quota, we need to filter out on that level
restrict_vars = set(voucher.quota.variations.all())
quotas_to_compute = []
for item in items:
assert item.event_id == event.pk
item.event = event # save a database query if this is looked up
if item.has_variations:
for v in item.available_variations:
for q in v._subevent_quotas:
if q.pk not in quota_cache:
quotas_to_compute.append(q)
else:
for q in item._subevent_quotas:
if q.pk not in quota_cache:
quotas_to_compute.append(q)
if quotas_to_compute:
qa = QuotaAvailability()
qa.queue(*quotas_to_compute)
qa.compute()
quota_cache.update({q.pk: r for q, r in qa.results.items()})
for item in items:
if voucher and voucher.item_id and voucher.variation_id:
# Restrict variations if the voucher only allows one
item.available_variations = [v for v in item.available_variations
if v.pk == voucher.variation_id]
if channel.type_instance.unlimited_items_per_order:
max_per_order = sys.maxsize
else:
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
if voucher:
max_per_order = min(max_per_order, voucher.max_usages - voucher.redeemed)
if item.hidden_if_available:
q = item.hidden_if_available.availability(_cache=quota_cache)
if q[0] == Quota.AVAILABILITY_OK:
item._remove = True
continue
if item.hidden_if_item_available:
if item.hidden_if_item_available.has_variations:
item._dependency_available = any(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
for var in item.hidden_if_item_available.available_variations
)
else:
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
time_available = item.hidden_if_item_available.is_available()
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
item._remove = True
continue
if item.require_membership and item.require_membership_hidden:
if not memberships or not any([m.membership_type in item.require_membership_types.all() for m in memberships]):
item._remove = True
continue
item.current_unavailability_reason = _get_item_unavailability_reason(item, has_voucher=voucher, subevent=subevent)
item.description = str(item.description)
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
if resp:
item.description += ("<br/>" if item.description else "") + resp
if not item.has_variations:
item._remove = False
if not bool(item._subevent_quotas):
item._remove = True
continue
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
item.cached_availability = (
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
)
else:
item.cached_availability = list(
item.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
)
if not (
ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids
) and event.settings.hide_sold_out and item.cached_availability[0] < Quota.AVAILABILITY_RESERVED:
item._remove = True
continue
item.order_max = min(
item.cached_availability[1]
if item.cached_availability[1] is not None else sys.maxsize,
max_per_order
)
original_price = item_price_override.get(item.pk, item.default_price)
voucher_reduced = False
if voucher:
price = voucher.calculate_price(original_price)
voucher_reduced = price < original_price
include_bundled = not voucher.all_bundles_included
else:
price = original_price
include_bundled = True
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
else:
item.suggested_price = item.display_price
if price != original_price:
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
else:
item.original_price = (
item.tax(item.original_price, currency=event.currency, include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
if item.original_price else None
)
if not display_add_to_cart:
display_add_to_cart = not item.requires_seat and item.order_max > 0
else:
for var in item.available_variations:
if var.require_membership and var.require_membership_hidden:
if not memberships or not any([m.membership_type in var.require_membership_types.all() for m in memberships]):
var._remove = True
continue
var.description = str(var.description)
for recv, resp in item_description.send(sender=event, item=item, variation=var, subevent=subevent):
if resp:
var.description += ("<br/>" if var.description else "") + resp
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
var.cached_availability = (
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
)
else:
var.cached_availability = list(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
)
var.order_max = min(
var.cached_availability[1]
if var.cached_availability[1] is not None else sys.maxsize,
max_per_order
)
original_price = var_price_override.get(var.pk, var.price)
voucher_reduced = False
if voucher:
price = voucher.calculate_price(original_price)
voucher_reduced = price < original_price
include_bundled = not voucher.all_bundles_included
else:
price = original_price
include_bundled = True
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
else:
var.suggested_price = var.display_price
if price != original_price:
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
else:
var.original_price = (
var.tax(var.original_price or item.original_price, currency=event.currency,
include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
) if var.original_price or item.original_price else None
var.current_unavailability_reason = _get_variant_unavailability_reason(var, has_voucher=voucher, subevent=subevent)
item.original_price = (
item.tax(item.original_price, currency=event.currency, include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
if item.original_price else None
)
item.available_variations = [
v for v in item.available_variations if v._subevent_quotas and (
not voucher or not voucher.quota_id or v in restrict_vars
) and not getattr(v, '_remove', False)
]
if not (ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids) and event.settings.hide_sold_out:
item.available_variations = [v for v in item.available_variations
if v.cached_availability[0] >= Quota.AVAILABILITY_RESERVED]
if voucher and voucher.variation_id:
item.available_variations = [v for v in item.available_variations
if v.pk == voucher.variation_id]
if len(item.available_variations) > 0:
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.best_variation_availability = max([v.cached_availability[0] for v in item.available_variations])
item._remove = not bool(item.available_variations)
if not item._remove and not display_add_to_cart:
display_add_to_cart = not item.requires_seat and any(v.order_max > 0 for v in item.available_variations)
if not quota_cache_existed and not voucher and not allow_addons and not base_qs_set and not filter_items and not filter_categories:
event.cache.set(quota_cache_key, quota_cache, 5)
items = [item for item in items
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
return items, display_add_to_cart
def _get_item_unavailability_reason(item, now_dt: Optional[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(item.pk)
if not item.active:
return 'active'
elif item.available_from and item.available_from > now_dt:
return 'available_from'
elif item.available_until and item.available_until < now_dt:
return 'available_until'
elif (item.require_voucher or item.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 item.hidden_if_item_available and item._dependency_available:
return 'hidden_if_item_available'
else:
return None
def _get_variant_unavailability_reason(variant, now_dt: Optional[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(variant.pk)
if not variant.active:
return 'active'
elif variant.available_from and variant.available_from > now_dt:
return 'available_from'
elif variant.available_until and variant.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
+4 -4
View File
@@ -68,13 +68,13 @@ from pretix.base.timemachine import time_machine_now
from pretix.base.views.tasks import AsyncAction
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.productlist import (
item_group_by_category, prepare_item_list_for_shop,
)
from pretix.presale.views import (
CartMixin, EventViewMixin, allow_cors_if_namespaced,
allow_frame_if_namespaced, get_cart, iframe_entry_view_wrapper,
)
from pretix.presale.views.event import (
get_grouped_items, item_group_by_category,
)
from pretix.presale.views.robots import NoSearchIndexViewMixin
try:
@@ -669,7 +669,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
# Fetch all items
items, display_add_to_cart = prepare_item_list_for_shop(
items, display_add_to_cart = get_grouped_items(
self.request.event,
subevent=self.subevent,
voucher=self.voucher,
+400 -10
View File
@@ -34,6 +34,7 @@
import calendar
import hashlib
import sys
from collections import defaultdict
from datetime import date, datetime, timedelta
from decimal import Decimal
@@ -46,7 +47,10 @@ from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db.models import Count
from django.db.models import (
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
)
from django.db.models.lookups import Exact
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
@@ -61,9 +65,15 @@ from django.views.generic import TemplateView
from pretix.base.auth import has_event_access_permission
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Quota, Voucher
from pretix.base.models import (
ItemVariation, Quota, SalesChannel, SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.items import (
Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
)
from pretix.base.services.placeholders import PlaceholderContext
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.timemachine import time_machine_now
from pretix.helpers.compat import date_fromisocalendar
from pretix.helpers.formats.en.formats import (
@@ -72,10 +82,7 @@ from pretix.helpers.formats.en.formats import (
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_public_ical
from pretix.presale.productlist import (
item_group_by_category, prepare_item_list_for_shop,
)
from pretix.presale.signals import seatingframe_html_head
from pretix.presale.signals import item_description, seatingframe_html_head
from pretix.presale.views.organizer import (
EventListMixin, add_subevents_for_days, days_for_template,
filter_qs_by_attr, filter_subevents_with_plugins, has_before_after,
@@ -87,12 +94,391 @@ from . import (
iframe_entry_view_wrapper,
)
from pretix.presale.productlist import prepare_item_list_for_shop as get_grouped_items # noqa
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
def item_group_by_category(items):
return sorted(
[
# a group is a tuple of a category and a list of items
(cat, [i for i in items if i.category == cat])
for cat in set([i.category for i in items])
# insert categories into a set for uniqueness
# a set is unsorted, so sort again by category
],
key=lambda group: (group[0].position, group[0].id) if (
group[0] is not None and group[0].id is not None) else (0, 0)
)
def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
allow_addons=False, allow_cross_sell=False,
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
ignore_hide_sold_out_for_item_ids=None):
base_qs_set = base_qs is not None
base_qs = base_qs if base_qs is not None else event.items
requires_seat = Exists(
SeatCategoryMapping.objects.filter(
product_id=OuterRef('pk'),
subevent=subevent
)
)
if not event.settings.seating_choice:
requires_seat = Value(0, output_field=IntegerField())
variation_q = (
Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info')) &
Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
)
if not voucher or not voucher.show_hidden_items:
variation_q &= Q(hide_without_voucher=False)
if memberships is not None:
prefetch_membership_types = ['require_membership_types']
else:
prefetch_membership_types = []
prefetch_var = Prefetch(
'variations',
to_attr='available_variations',
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
subevent_disabled=Exists(
SubEventItemVariation.objects.filter(
Q(disabled=True)
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
variation_id=OuterRef('pk'),
subevent=subevent,
)
),
).filter(
variation_q,
Q(all_sales_channels=True) | Q(limit_sales_channels=channel),
Exists(Quota.variations.through.objects.filter(quota__subevent_id=subevent, itemvariation_id=OuterRef("pk"))),
active=True,
subevent_disabled=False
).prefetch_related(
*prefetch_membership_types,
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent).select_related("subevent"))
).distinct()
)
prefetch_quotas = Prefetch(
'quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent).select_related("subevent")
)
prefetch_bundles = Prefetch(
'bundles',
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
Prefetch('bundled_item',
queryset=event.items.using(settings.DATABASE_REPLICA).select_related(
'tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent)),
)),
Prefetch('bundled_variation',
queryset=ItemVariation.objects.using(
settings.DATABASE_REPLICA
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent)),
)),
)
)
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(
channel=channel.identifier, voucher=voucher, allow_addons=allow_addons, allow_cross_sell=allow_cross_sell
).select_related(
'category', 'tax_rule', # for re-grouping
'hidden_if_available',
).prefetch_related(
*prefetch_membership_types,
Prefetch(
'hidden_if_item_available',
queryset=event.items.annotate(
has_variations=Count('variations'),
).prefetch_related(
prefetch_var,
prefetch_quotas,
prefetch_bundles,
)
),
prefetch_quotas,
prefetch_var,
prefetch_bundles,
).annotate(
has_variations=Count('variations'),
subevent_disabled=Exists(
SubEventItem.objects.filter(
Q(disabled=True)
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
item_id=OuterRef('pk'),
subevent=subevent,
)
),
mandatory_priced_addons=Exists(
ItemAddOn.objects.filter(
base_item_id=OuterRef('pk'),
min_count__gte=1,
price_included=False
)
),
requires_seat=requires_seat,
).filter(
Exists(Quota.items.through.objects.filter(quota__subevent_id=subevent, item_id=OuterRef("pk"))),
subevent_disabled=False,
).order_by('category__position', 'category_id', 'position', 'name')
if require_seat:
items = items.filter(requires_seat__gt=0)
elif require_seat is not None:
items = items.filter(requires_seat=0)
if filter_items:
items = items.filter(pk__in=[a for a in filter_items if a.isdigit()])
if filter_categories:
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
display_add_to_cart = False
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
quota_cache_existed = bool(quota_cache)
if subevent:
item_price_override = subevent.item_price_overrides
var_price_override = subevent.var_price_overrides
else:
item_price_override = {}
var_price_override = {}
restrict_vars = set()
if voucher and voucher.quota_id:
# If a voucher is set to a specific quota, we need to filter out on that level
restrict_vars = set(voucher.quota.variations.all())
quotas_to_compute = []
for item in items:
assert item.event_id == event.pk
item.event = event # save a database query if this is looked up
if item.has_variations:
for v in item.available_variations:
for q in v._subevent_quotas:
if q.pk not in quota_cache:
quotas_to_compute.append(q)
else:
for q in item._subevent_quotas:
if q.pk not in quota_cache:
quotas_to_compute.append(q)
if quotas_to_compute:
qa = QuotaAvailability()
qa.queue(*quotas_to_compute)
qa.compute()
quota_cache.update({q.pk: r for q, r in qa.results.items()})
for item in items:
if voucher and voucher.item_id and voucher.variation_id:
# Restrict variations if the voucher only allows one
item.available_variations = [v for v in item.available_variations
if v.pk == voucher.variation_id]
if channel.type_instance.unlimited_items_per_order:
max_per_order = sys.maxsize
else:
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
if voucher:
max_per_order = min(max_per_order, voucher.max_usages - voucher.redeemed)
if item.hidden_if_available:
q = item.hidden_if_available.availability(_cache=quota_cache)
if q[0] == Quota.AVAILABILITY_OK:
item._remove = True
continue
if item.hidden_if_item_available:
if item.hidden_if_item_available.has_variations:
item._dependency_available = any(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
for var in item.hidden_if_item_available.available_variations
)
else:
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
time_available = item.hidden_if_item_available.is_available()
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
item._remove = True
continue
if item.require_membership and item.require_membership_hidden:
if not memberships or not any([m.membership_type in item.require_membership_types.all() for m in memberships]):
item._remove = True
continue
item.current_unavailability_reason = item.unavailability_reason(has_voucher=voucher, subevent=subevent)
item.description = str(item.description)
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
if resp:
item.description += ("<br/>" if item.description else "") + resp
if not item.has_variations:
item._remove = False
if not bool(item._subevent_quotas):
item._remove = True
continue
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
item.cached_availability = (
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
)
else:
item.cached_availability = list(
item.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
)
if not (
ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids
) and event.settings.hide_sold_out and item.cached_availability[0] < Quota.AVAILABILITY_RESERVED:
item._remove = True
continue
item.order_max = min(
item.cached_availability[1]
if item.cached_availability[1] is not None else sys.maxsize,
max_per_order
)
original_price = item_price_override.get(item.pk, item.default_price)
voucher_reduced = False
if voucher:
price = voucher.calculate_price(original_price)
voucher_reduced = price < original_price
include_bundled = not voucher.all_bundles_included
else:
price = original_price
include_bundled = True
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
else:
item.suggested_price = item.display_price
if price != original_price:
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
else:
item.original_price = (
item.tax(item.original_price, currency=event.currency, include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
if item.original_price else None
)
if not display_add_to_cart:
display_add_to_cart = not item.requires_seat and item.order_max > 0
else:
for var in item.available_variations:
if var.require_membership and var.require_membership_hidden:
if not memberships or not any([m.membership_type in var.require_membership_types.all() for m in memberships]):
var._remove = True
continue
var.description = str(var.description)
for recv, resp in item_description.send(sender=event, item=item, variation=var, subevent=subevent):
if resp:
var.description += ("<br/>" if var.description else "") + resp
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
var.cached_availability = (
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
)
else:
var.cached_availability = list(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
)
var.order_max = min(
var.cached_availability[1]
if var.cached_availability[1] is not None else sys.maxsize,
max_per_order
)
original_price = var_price_override.get(var.pk, var.price)
voucher_reduced = False
if voucher:
price = voucher.calculate_price(original_price)
voucher_reduced = price < original_price
include_bundled = not voucher.all_bundles_included
else:
price = original_price
include_bundled = True
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
else:
var.suggested_price = var.display_price
if price != original_price:
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
else:
var.original_price = (
var.tax(var.original_price or item.original_price, currency=event.currency,
include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
) if var.original_price or item.original_price else None
var.current_unavailability_reason = var.unavailability_reason(has_voucher=voucher, subevent=subevent)
item.original_price = (
item.tax(item.original_price, currency=event.currency, include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
if item.original_price else None
)
item.available_variations = [
v for v in item.available_variations if v._subevent_quotas and (
not voucher or not voucher.quota_id or v in restrict_vars
) and not getattr(v, '_remove', False)
]
if not (ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids) and event.settings.hide_sold_out:
item.available_variations = [v for v in item.available_variations
if v.cached_availability[0] >= Quota.AVAILABILITY_RESERVED]
if voucher and voucher.variation_id:
item.available_variations = [v for v in item.available_variations
if v.pk == voucher.variation_id]
if len(item.available_variations) > 0:
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.best_variation_availability = max([v.cached_availability[0] for v in item.available_variations])
item._remove = not bool(item.available_variations)
if not item._remove and not display_add_to_cart:
display_add_to_cart = not item.requires_seat and any(v.order_max > 0 for v in item.available_variations)
if not quota_cache_existed and not voucher and not allow_addons and not base_qs_set and not filter_items and not filter_categories:
event.cache.set(quota_cache_key, quota_cache, 5)
items = [item for item in items
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
return items, display_add_to_cart
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
@@ -196,7 +582,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
if not self.request.event.has_subevents or self.subevent:
# Fetch all items
items, display_add_to_cart = prepare_item_list_for_shop(
items, display_add_to_cart = get_grouped_items(
self.request.event,
subevent=self.subevent,
filter_items=self.request.GET.getlist('item'),
@@ -248,6 +634,10 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['has_addon_choices'] = any(cp.has_addon_choices for cp in get_cart(self.request))
templating_context = PlaceholderContext(event_or_subevent=self.subevent or self.request.event, event=self.request.event)
for field in ('presale_has_ended_text',):
context[field] = templating_context.format(str(self.request.event.settings[field]))
if self.subevent:
context['frontpage_text'] = templating_context.format(str(self.subevent.frontpage_text))
else:
+27 -3
View File
@@ -79,7 +79,7 @@ from pretix.base.services.invoices import (
)
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _try_auto_refund, cancel_order,
change_payment_provider,
change_payment_provider, error_messages,
)
from pretix.base.services.pricing import get_price
from pretix.base.services.tickets import generate, invalidate_cache
@@ -92,11 +92,11 @@ from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.presale.forms.checkout import InvoiceAddressForm, QuestionsForm
from pretix.presale.forms.order import OrderPositionChangeForm
from pretix.presale.productlist import prepare_item_list_for_shop
from pretix.presale.signals import question_form_fields_overrides
from pretix.presale.views import (
CartMixin, EventViewMixin, iframe_entry_view_wrapper,
)
from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.robots import NoSearchIndexViewMixin
logger = logging.getLogger(__name__)
@@ -1452,7 +1452,7 @@ class OrderChangeMixin:
if ckey not in item_cache:
# Get all items to possibly show
items, _btn = prepare_item_list_for_shop(
items, _btn = get_grouped_items(
self.request.event,
subevent=p.subevent,
voucher=None,
@@ -1592,6 +1592,30 @@ class OrderChangeMixin:
if val:
selected[i, None] = val, price
if sum(a[0] for a in selected.values()) > category['max_count']:
raise ValidationError(
error_messages['addon_max_count'] % {
'base': str(form['pos'].item.name),
'max': category['max_count'],
'cat': str(category['category'].name),
}
)
elif sum(a[0] for a in selected.values()) < category['min_count']:
raise ValidationError(
error_messages['addon_min_count'] % {
'base': str(form['pos'].item.name),
'min': category['min_count'],
'cat': str(category['category'].name),
}
)
elif any(sum(v[0] for k, v in selected.items() if k[0] == i) > 1 for i in category['items']) and not category['multi_allowed']:
raise ValidationError(
error_messages['addon_no_multi'] % {
'base': str(form['pos'].item.name),
'cat': str(category['category'].name),
}
)
return selected
def post(self, request, *args, **kwargs):
+1
View File
@@ -48,6 +48,7 @@ def theme_css(request, **kwargs):
obj = getattr(request, "event", request.organizer)
css = get_theme_vars_css(obj, widget=False)
resp = HttpResponse(css, content_type="text/css")
resp._csp_ignore = True
resp["Access-Control-Allow-Origin"] = "*"
if "version" in request.GET:
resp["Expires"] = http_date(time.time() + 3600 * 24 * 30)
+12 -7
View File
@@ -32,6 +32,7 @@
# 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.
from django.conf import settings
from django.contrib import messages
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
@@ -41,7 +42,6 @@ from django.views.generic import TemplateView
from pretix.base.email import get_email_context
from pretix.base.services.mail import INVALID_ADDRESS, mail
from pretix.helpers.http import redirect_to_url
from pretix.helpers.ratelimit import rate_limit
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.user import ResendLinkForm
from pretix.presale.views import EventViewMixin
@@ -61,12 +61,17 @@ class ResendLinkView(EventViewMixin, TemplateView):
user = self.link_form.cleaned_data.get('email')
if rate_limit("order_resend", self.request.event.pk, user, max_num=1, expire_time=3600 * 24):
messages.error(request, _('If the email address you entered is valid and associated with a ticket, we have '
'already sent you an email with a link to your ticket in the past {number} hours. '
'If the email did not arrive, please check your spam folder and also double check '
'that you used the correct email address.').format(number=24))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.resend_link'))
if settings.HAS_REDIS:
from django_redis import get_redis_connection
rc = get_redis_connection("redis")
if rc.exists('pretix_resend_{}_{}'.format(request.event.pk, user)):
messages.error(request, _('If the email address you entered is valid and associated with a ticket, we have '
'already sent you an email with a link to your ticket in the past {number} hours. '
'If the email did not arrive, please check your spam folder and also double check '
'that you used the correct email address.').format(number=24))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.resend_link'))
else:
rc.setex('pretix_resend_{}_{}'.format(request.event.pk, user), 3600 * 24, '1')
orders = self.request.event.orders.filter(email__iexact=user)
+2 -2
View File
@@ -40,8 +40,8 @@ from pretix.presale.views import EventViewMixin, iframe_entry_view_wrapper
from ...base.i18n import get_language_without_region
from ...base.models import Voucher, WaitingListEntry
from ..forms.waitinglist import WaitingListForm
from ..productlist import prepare_item_list_for_shop
from . import allow_frame_if_namespaced
from .event import get_grouped_items
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@@ -53,7 +53,7 @@ class WaitingView(EventViewMixin, FormView):
@cached_property
def itemvars(self):
customer = getattr(self.request, 'customer', None)
items, display_add_to_cart = prepare_item_list_for_shop(
items, display_add_to_cart = get_grouped_items(
self.request.event,
subevent=self.subevent,
require_seat=None,
+8 -4
View File
@@ -67,11 +67,11 @@ from pretix.helpers.daterange import daterange
from pretix.helpers.thumb import get_thumbnail
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.presale.forms.organizer import meta_filtersets
from pretix.presale.productlist import (
item_group_by_category, prepare_item_list_for_shop,
)
from pretix.presale.style import get_theme_vars_css
from pretix.presale.views.cart import get_or_create_cart_id
from pretix.presale.views.event import (
get_grouped_items, item_group_by_category,
)
from pretix.presale.views.organizer import (
EventListMixin, add_events_for_days, add_subevents_for_days,
days_for_template, filter_qs_by_attr, filter_subevents_with_plugins,
@@ -164,6 +164,7 @@ def widget_css(request, version, **kwargs):
css = f"/* v{version} */\n" + theme_css + widget_css
resp = FileResponse(css, content_type='text/css')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -245,6 +246,7 @@ def widget_js(request, version, lang, **kwargs):
cached_js = cache.get(cache_prefix)
if cached_js and not settings.DEBUG:
resp = HttpResponse(cached_js, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -276,6 +278,7 @@ def widget_js(request, version, lang, **kwargs):
gs.settings.set(checksum_key, checksum)
cache.set(cache_prefix, data, 3600 * 4)
resp = HttpResponse(data, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -321,7 +324,7 @@ class WidgetAPIProductList(EventListMixin, View):
).values_list('item_id', flat=True)
)
items, display_add_to_cart = prepare_item_list_for_shop(
items, display_add_to_cart = get_grouped_items(
self.request.event,
subevent=self.subevent,
voucher=self.voucher,
@@ -411,6 +414,7 @@ class WidgetAPIProductList(EventListMixin, View):
self.post_process(data)
resp = JsonResponse(data)
resp['Access-Control-Allow-Origin'] = '*'
resp._csp_ignore = True
return resp
def get(self, request, *args, **kwargs):
-3
View File
@@ -262,9 +262,6 @@ MAIL_FROM_NOTIFICATIONS = config.get('mail', 'from_notifications', fallback=MAIL
MAIL_FROM_ORGANIZERS = config.get('mail', 'from_organizers', fallback=MAIL_FROM)
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED = config.getboolean('mail', 'custom_sender_verification_required', fallback=True)
MAIL_CUSTOM_SENDER_SPF_STRING = config.get('mail', 'custom_sender_spf_string', fallback='')
MAIL_CUSTOM_SENDER_DKIM_SELECTOR = config.get('mail', 'custom_sender_dkim_selector', fallback='')
MAIL_CUSTOM_SENDER_DKIM_CNAME = config.get('mail', 'custom_sender_dkim_cname', fallback='')
MAIL_CUSTOM_SENDER_DMARC_REQUIRED = config.getboolean('mail', 'custom_sender_dmarc_required', fallback=False)
MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = config.getboolean('mail', 'custom_smtp_allow_private_networks', fallback=DEBUG)
EMAIL_HOST = config.get('mail', 'host', fallback='localhost')
EMAIL_PORT = config.getint('mail', 'port', fallback=25)
File diff suppressed because one or more lines are too long
@@ -1,7 +1,4 @@
/*globals $, gettext, fabric, PDFJS*/
window.module = {} // Hack to load ajv-compiled schema without actual module loading
fabric.Poweredby = fabric.util.createClass(fabric.Image, {
type: 'poweredby',
@@ -222,6 +219,7 @@ var editor = {
uploaded_file_id: null,
_window_loaded: false,
_fabric_loaded: false,
schema: null,
_px2mm: function (v) {
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
@@ -1322,11 +1320,14 @@ var editor = {
_source_save: function () {
try {
var Ajv = window.ajv2020
var ajv = new Ajv()
var validate = ajv.compile(editor.schema)
var data = JSON.parse($("#source-textarea").val())
var valid = validate30(data)
var valid = validate(data)
if (!valid) {
console.log(validate30.errors)
console.log(validate.errors)
alert("Invalid input syntax. If you're familiar with this, check out the developer console for a full " +
"error log. Otherwise, please contact support.")
} else {
@@ -1462,6 +1463,10 @@ var editor = {
editor._update_save_button();
});
$("#pdf-info-width, #pdf-info-height").bind('change input', editor._paper_size_warning);
$.getJSON($("#schema-url").text(), function (data) {
editor.schema = data;
})
}
};
+11 -3
View File
@@ -124,7 +124,7 @@ var form_handlers = function (el) {
el.find(".datetimepicker").each(function () {
$(this).datetimepicker({
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-datetimeformat"),
format: $("body").attr("data-datetimeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
@@ -147,7 +147,7 @@ var form_handlers = function (el) {
el.find(".datepickerfield").each(function () {
var opts = {
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-dateformat"),
format: $("body").attr("data-dateformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
@@ -205,7 +205,7 @@ var form_handlers = function (el) {
el.find(".timepickerfield").each(function () {
var opts = {
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-timeformat"),
format: $("body").attr("data-timeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
@@ -874,6 +874,14 @@ function setup_basics(el) {
});
});
el.find(".qrcode-canvas").each(function () {
$(this).qrcode(
{
text: $.trim($($(this).attr("data-qrdata")).html())
}
);
});
el.find(".propagated-settings-box").find("input, textarea, select").not("[readonly]")
.attr("data-propagated-locked", "true").prop("readonly", true);
@@ -1,5 +1,6 @@
/*global $, gettext*/
$(function () {
// Question view
if (!$(".form-order-change").length) {
return;
}
@@ -56,4 +57,51 @@ $(function () {
}
});
});
$('[data-model-select2=seat]').each(function () {
var $s = $(this);
$s.select2({
theme: "bootstrap",
delay: 100,
allowClear: !$s.prop("required"),
width: '100%',
language: $("body").attr("data-select2-locale"),
placeholder: $(this).attr("data-placeholder"),
ajax: {
url: function() {
var se = $(this).closest(".form-order-change, .form-horizontal").attr("data-subevent");
var url = $(this).attr('data-select2-url');
var changed = $(this).closest(".form-order-change, .form-horizontal").find("[id$=subevent]").val();
if (changed) {
return url + '?subevent=' + changed;
} else if (se) {
return url + '?subevent=' + se;
} else {
return url;
}
},
data: function (params) {
return {
query: params.term,
page: params.page || 1
}
}
},
templateResult: function (res) {
if (!res.id) {
return res.text;
}
var $ret = $("<span>").append(
$("<span>").addClass("primary").append($("<div>").text(res.text).html())
);
if (res.event) {
$ret.append(
$("<span>").addClass("secondary").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append($("<div>").text(res.event).html())
);
}
return $ret;
},
});
});
});
@@ -464,8 +464,6 @@ details.details-open .panel-title::before {
.alert > dl:last-child,
td > p:last-child,
.panel-body > dl:last-child,
.panel-body > ul:last-child,
.panel-body > ol:last-child,
.panel-body > .table:last-child,
.panel-body > .table-responsive:last-child > .table:last-child,
table td ul:last-child {
@@ -648,10 +646,7 @@ ul.pagination {
.table-payment-providers > tbody > tr > td {
vertical-align: middle;
}
.success-left {
border-left: 3px solid $brand-success;
background: var(--pretix-brand-success-lighten-50);
}
details {
list-style: none;
@@ -11,7 +11,7 @@ var form_handlers = function (el) {
el.find(".datetimepicker").each(function () {
$(this).datetimepicker({
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-datetimeformat"),
format: $("body").attr("data-datetimeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
@@ -34,7 +34,7 @@ var form_handlers = function (el) {
el.find(".datepickerfield").each(function () {
var opts = {
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-dateformat"),
format: $("body").attr("data-dateformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
@@ -91,7 +91,7 @@ var form_handlers = function (el) {
el.find(".timepickerfield").each(function () {
var opts = {
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-timeformat"),
format: $("body").attr("data-timeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
@@ -1,6 +1,5 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$comment": "If the schema is changed, regenerate pdf-layout.validate.js with $ ajv compile -s ./pretix/static/schema/pdf-layout.schema.json --spec=draft2020 -o ./pretix/static/schema/pdf-layout.validate.js",
"title": "Ticket Layout",
"description": "Dynamic elements for a PDF layout",
"type": "array",
File diff suppressed because one or more lines are too long
+7 -8
View File
@@ -252,13 +252,13 @@ TEST_HISTORY_RES = {
}
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_list_list(token_client, organizer, event, clist, item, subevent, django_assert_num_queries):
res = dict(TEST_LIST_RES)
res["id"] = clist.pk
res["limit_products"] = [item.pk]
with django_assert_num_queries(9):
with django_assert_num_queries(11):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@@ -422,7 +422,7 @@ def test_list_update(token_client, organizer, event, clist):
assert cl.name == "VIP"
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_num_queries):
with scopes_disabled():
p1 = dict(TEST_ORDERPOSITION1_RES)
@@ -437,7 +437,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
p3["addon_to"] = p1["id"]
# All items
with django_assert_num_queries(22):
with django_assert_num_queries(24):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
organizer.slug, event.slug, clist_all.pk
))
@@ -680,7 +680,7 @@ def _redeem(token_client, org, clist, p, body=None):
), body or {}, format='json')
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
with scopes_disabled():
p = order.positions.first().pk
@@ -1127,7 +1127,6 @@ def test_store_failed(token_client, organizer, clist, event, order):
organizer.slug, event.slug, clist.pk,
), {
'raw_barcode': '123456',
'raw_source_type': 'nfc_uid',
'nonce': '4321',
'error_reason': 'invalid'
}, format='json')
@@ -1368,7 +1367,7 @@ def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist,
assert ci.position == p
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
with scopes_disabled():
p1 = dict(TEST_ORDERPOSITION1_RES)
@@ -1400,7 +1399,7 @@ def test_checkin_pdf_data_requires_permission(token_client, event, team, organiz
assert not resp.data['results'][0].get('pdf_data')
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_expand(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
with scopes_disabled():
op = order.positions.first()
+2 -2
View File
@@ -214,7 +214,7 @@ def _redeem(token_client, org, clist, p, body=None, query='', headers={}):
), body, format='json', headers={})
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
with scopes_disabled():
p = order.positions.first()
@@ -996,7 +996,7 @@ def test_redeem_conflicting_lists(token_client, organizer, clist, clist_all, eve
assert resp.data == ['Selecting two check-in lists from the same event is unsupported.']
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order,
django_assert_max_num_queries):
with scopes_disabled():
+5 -5
View File
@@ -1848,7 +1848,7 @@ def test_event_block_unblock_seat_bulk(token_client, organizer, event, seatingpl
assert not s2.blocked
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_event_expand_seat_filter_and_querycount(token_client, organizer, event, seatingplan, item):
event.settings.seating_minimal_distance = 2
@@ -1865,7 +1865,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert resp.status_code == 200
event.refresh_from_db()
with assert_num_queries(10):
with assert_num_queries(12):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1875,7 +1875,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
with scope(organizer=organizer):
v0 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-0'))
with assert_num_queries(12):
with assert_num_queries(14):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
@@ -1883,7 +1883,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert len(resp.data['results']) == 1
assert resp.data['results'][0]['voucher']['id'] == v0.pk
with assert_num_queries(10):
with assert_num_queries(12):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1894,7 +1894,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
v1 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-1'))
v2 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-2'))
with assert_num_queries(14):
with assert_num_queries(16):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
+1 -1
View File
@@ -425,7 +425,7 @@ def test_item_list(token_client, organizer, event, team, item):
@pytest.mark.django_db
def test_item_list_queries(token_client, organizer, event, team, item, item3):
with assert_num_queries(16):
with assert_num_queries(18):
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
+4 -4
View File
@@ -1058,7 +1058,7 @@ def test_orderposition_list_limited_read(
('/api/v1/organizers/{}/orderpositions/', "organizer")
],
)
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_orderposition_list(
endpoint_template,
endpoint_type,
@@ -1166,10 +1166,10 @@ def test_orderposition_list(
'type': 'entry'
}]
if '/events/' in endpoint:
with django_assert_num_queries(16):
with django_assert_num_queries(18):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(15):
with django_assert_num_queries(17):
resp = token_client.get(endpoint + '?has_checkin=true')
assert [res] == resp.data['results']
@@ -2036,7 +2036,7 @@ def test_blocked_secret_list(token_client, organizer, event):
assert [res] == resp.data['results']
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_queries):
# order detail
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?pdf_data=true'.format(
+3 -3
View File
@@ -732,7 +732,7 @@ def test_five_tickets_one_free(event):
@scopes_disabled()
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
@pytest.mark.parametrize("itemcount", [3, 10, 50])
def test_query_count_many_items(event, itemcount):
setup_items(event, 'Tickets', 'both', 'discounts',
@@ -784,7 +784,7 @@ def test_query_count_many_items(event, itemcount):
@scopes_disabled()
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
@pytest.mark.parametrize("catcount", [1, 10, 50])
def test_query_count_many_categories_and_discounts(event, catcount):
for n in range(1, catcount + 1):
@@ -838,7 +838,7 @@ def test_query_count_many_categories_and_discounts(event, catcount):
@scopes_disabled()
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
@pytest.mark.parametrize("catcount", [2, 10, 50])
def test_query_count_many_cartpos(event, catcount):
for n in range(1, catcount + 1):
+1 -3
View File
@@ -26,7 +26,6 @@ from zoneinfo import ZoneInfo
import pytest
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.timezone import now
from django_scopes import scope
from freezegun import freeze_time
@@ -211,8 +210,7 @@ def test_validate_membership_required(event, customer, membership, requiring_tic
assert "requires an active" in str(excinfo.value)
@pytest.mark.django_db(transaction=True)
@transaction.atomic
@pytest.mark.django_db
def test_validate_membership_ensure_locking(event, customer, membership, requiring_ticket, membership_type, django_assert_num_queries):
with django_assert_num_queries(4) as captured:
validate_memberships_in_order(
-7
View File
@@ -28,7 +28,6 @@ from django.test import override_settings
from django.utils import translation
from django_scopes import scopes_disabled
from fakeredis import FakeRedisConnection
from hierarkey.proxy import dirty_cache_keys
from xdist.dsession import DSession
from pretix.testutils.mock import get_redis_connection
@@ -83,11 +82,6 @@ def reset_locale():
translation.activate("en")
@pytest.fixture(autouse=True)
def reset_hierarkey_cache_state():
dirty_cache_keys.set(set())
@pytest.fixture
def fakeredis_client(monkeypatch):
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
@@ -126,7 +120,6 @@ def fakeredis_client(monkeypatch):
redis = get_redis_connection("default", True)
redis.flushall()
monkeypatch.setattr('django_redis.get_redis_connection', get_redis_connection, raising=False)
monkeypatch.setattr('pretix.base.metrics.redis', redis, raising=False)
yield redis
+31 -1
View File
@@ -504,8 +504,33 @@ class Login2FAFormTest(TestCase):
assert "recovery code" in djmail.outbox[0].body
class FakeRedis(object):
def get_redis_connection(self, connection_string):
return self
def __init__(self):
self.storage = {}
def pipeline(self):
return self
def hincrbyfloat(self, rkey, key, amount):
return self
def commit(self):
return self
def exists(self, rkey):
return rkey in self.storage
def setex(self, rkey, value, expiration):
self.storage[rkey] = value
def execute(self):
pass
@pytest.mark.usefixtures("class_monkeypatch")
@pytest.mark.usefixtures("fakeredis_client")
class PasswordRecoveryFormTest(TestCase):
def setUp(self):
super().setUp()
@@ -535,6 +560,11 @@ class PasswordRecoveryFormTest(TestCase):
@override_settings(HAS_REDIS=True)
def test_email_reset_twice_redis(self):
fake_redis = FakeRedis()
m = self.monkeypatch
m.setattr('django_redis.get_redis_connection', fake_redis.get_redis_connection, raising=False)
m.setattr('pretix.base.metrics.redis', fake_redis, raising=False)
djmail.outbox = []
response = self.client.post('/control/forgot', {
+2 -63
View File
@@ -159,22 +159,6 @@ class OrganizerTest(SoupTest):
self.orga1.settings.flush()
assert "mail_from" not in self.orga1.settings._cache()
@staticmethod
def _fake_dmarc_record(hostname):
return {
'test.pretix.dev': 'v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;',
'bad.pretix.dev': None,
'none.pretix.dev': None,
}[hostname]
@staticmethod
def _fake_cname_record(hostname):
return {
'pretix._domainkey.test.pretix.dev': 'test-pretix-dev.dkim.pretix.eu.',
'pretix._domainkey.bad.pretix.dev': 'example.org',
'pretix._domainkey.none.pretix.dev': None,
}[hostname]
@staticmethod
def _fake_spf_record(hostname):
return {
@@ -188,17 +172,9 @@ class OrganizerTest(SoupTest):
'spftest.pretix.dev': None,
}[hostname]
@override_settings(
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev",
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
)
def test_email_setup_no_verification_spf_dmarc_dkim_success(self):
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev")
def test_email_setup_no_verification_spf_success(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record)
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
@@ -237,43 +213,6 @@ class OrganizerTest(SoupTest):
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
MAIL_CUSTOM_SENDER_SPF_STRING="")
def test_email_setup_no_verification_dkim_warning(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
'mode': 'simple',
'simple-mail_from': 'test@bad.pretix.dev',
},
follow=True
)
assert doc.select('.alert-danger')
self.orga1.settings.flush()
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
MAIL_CUSTOM_SENDER_SPF_STRING="")
def test_email_setup_no_verification_dmarc_warning(self):
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
doc = self.post_doc(
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
{
'mode': 'simple',
'simple-mail_from': 'test@bad.pretix.dev',
},
follow=True
)
assert doc.select('.alert-danger')
self.orga1.settings.flush()
# not yet saved
assert "mail_from" not in self.orga1.settings._cache()
def test_email_setup_smtp(self):
self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", lambda b, a: None)
self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8")
+1 -43
View File
@@ -994,7 +994,7 @@ class OrderChangeAddonsTest(BaseOrdersTest):
self.order.refresh_from_db()
assert self.order.total == Decimal('23.00')
def test_do_not_remove_timeunavailable_on_adding(self):
def test_do_not_remove_unavailable_on_adding(self):
self.iao.max_count = 2
self.iao.save()
self.workshop1.available_until = now() - datetime.timedelta(days=1)
@@ -1036,48 +1036,6 @@ class OrderChangeAddonsTest(BaseOrdersTest):
with scopes_disabled():
assert self.ticket_pos.addons.count() == 2
def test_do_not_remove_dependencyunavailable_on_adding(self):
self.iao.max_count = 2
self.iao.save()
self.workshop1.hidden_if_item_available = self.workshop2
self.workshop1.save()
with scopes_disabled():
OrderPosition.objects.create(
order=self.order,
item=self.workshop1,
variation=None,
price=Decimal("12"),
addon_to=self.ticket_pos,
attendee_name_parts={'full_name': "Peter"}
)
self.order.total += Decimal("12")
self.order.save()
response = self.client.get(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
assert '<li>1x Workshop 1</li>' in response.content.decode()
assert f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}' not in response.content.decode()
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
{
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
},
follow=True
)
doc = BeautifulSoup(response.content.decode(), "lxml")
form_data = extract_form_fields(doc.select('.main-box form')[0])
form_data['confirm'] = 'true'
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), form_data, follow=True
)
assert 'alert-success' in response.content.decode()
with scopes_disabled():
assert self.ticket_pos.addons.count() == 2
def test_do_not_overbook_unavailable_on_adding(self):
self.iao.max_count = 1
self.iao.save()