Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
45bff138d7 Send security notification when recovery code is used or created by admin
"Where to store recovery codes" is one of these problems there is no
right answer to, so many people store them in a less-than-optimal place.
If that's the reality we live in, this PR adds at least a little
security so one notices when they get used :)
2025-12-12 15:30:22 +01:00
39 changed files with 794 additions and 1100 deletions

View File

@@ -92,7 +92,7 @@ dependencies = [
"redis==6.4.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.48.*",
"sentry-sdk==2.47.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -110,7 +110,7 @@ dev = [
"aiohttp==3.13.*",
"coverage",
"coveralls",
"fakeredis==2.33.*",
"fakeredis==2.32.*",
"flake8==7.3.*",
"freezegun",
"isort==6.1.*",

View File

@@ -74,11 +74,6 @@ class ExportersMixin:
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def download(self, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
if not cf.allowed_for_session(self.request, "exporters-api"):
return Response(
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
status=status.HTTP_410_GONE
)
if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
@@ -114,8 +109,7 @@ class ExportersMixin:
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
serializer.is_valid(raise_exception=True)
cf = CachedFile(web_download=True)
cf.bind_to_session(self.request, "exporters-api")
cf = CachedFile(web_download=False)
cf.date = now()
cf.expires = now() + timedelta(hours=24)
cf.save()

View File

@@ -90,7 +90,6 @@ StaticMapping = namedtuple('StaticMapping', ('id', 'pretix_model', 'external_obj
class OutboundSyncProvider:
max_attempts = 5
list_field_joiner = "," # set to None to keep native lists in properties
def __init__(self, event):
self.event = event
@@ -282,8 +281,7 @@ class OutboundSyncProvider:
'Please update value mapping for field "{field_name}" - option "{val}" not assigned'
).format(field_name=key, val=val)])
if self.list_field_joiner:
val = self.list_field_joiner.join(val)
val = ",".join(val)
return val
def get_properties(self, inputs: dict, property_mappings: List[dict]):

View File

@@ -71,20 +71,15 @@ def assign_properties(
return out
def _add_to_list(out, field_name, current_value, new_item_input, list_sep):
def _add_to_list(out, field_name, current_value, new_item, list_sep):
new_item = str(new_item)
if list_sep is not None:
new_items = str(new_item_input).split(list_sep)
new_item = new_item.replace(list_sep, "")
current_value = current_value.split(list_sep) if current_value else []
else:
new_items = [str(new_item_input)]
if not isinstance(current_value, (list, tuple)):
current_value = [str(current_value)]
new_list = list(current_value)
for new_item in new_items:
if new_item not in current_value:
new_list.append(new_item)
if new_list != current_value:
elif not isinstance(current_value, (list, tuple)):
current_value = [str(current_value)]
if new_item not in current_value:
new_list = current_value + [new_item]
if list_sep is not None:
new_list = list_sep.join(new_list)
out[field_name] = new_list

View File

@@ -59,37 +59,6 @@ class CachedFile(models.Model):
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
session_key = models.TextField(null=True, blank=True) # only allow download in this session
def session_key_for_request(self, request, salt=None):
from ...api.models import OAuthAccessToken, OAuthApplication
from .devices import Device
from .organizer import TeamAPIToken
if hasattr(request, "auth") and isinstance(request.auth, OAuthAccessToken):
k = f'app:{request.auth.application.pk}'
elif hasattr(request, "auth") and isinstance(request.auth, OAuthApplication):
k = f'app:{request.auth.pk}'
elif hasattr(request, "auth") and isinstance(request.auth, TeamAPIToken):
k = f'token:{request.auth.pk}'
elif hasattr(request, "auth") and isinstance(request.auth, Device):
k = f'device:{request.auth.pk}'
elif request.session.session_key:
k = request.session.session_key
else:
raise ValueError("No auth method found to bind to")
if salt:
k = f"{k}!{salt}"
return k
def allowed_for_session(self, request, salt=None):
return (
not self.session_key or
self.session_key_for_request(request, salt) == self.session_key
)
def bind_to_session(self, request, salt=None):
self.session_key = self.session_key_for_request(request, salt)
@receiver(post_delete, sender=CachedFile)
def cached_file_delete(sender, instance, **kwargs):

View File

@@ -97,10 +97,6 @@ class CartError(Exception):
super().__init__(msg)
class CartPositionError(CartError):
pass
error_messages = {
'busy': gettext_lazy(
'We were not able to process your request completely as the '
@@ -110,9 +106,6 @@ error_messages = {
'unknown_position': gettext_lazy('Unknown cart position.'),
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
'positions_removed': gettext_lazy(
'Some products can no longer be purchased and have been removed from your cart for the following reason: %s'
),
'unavailable': gettext_lazy(
'Some of the products you selected are no longer available. '
'Please see below for details.'
@@ -265,138 +258,6 @@ def _get_voucher_availability(event, voucher_use_diff, now_dt, exclude_position_
return vouchers_ok, _voucher_depend_on_cart
def _check_position_constraints(
event: Event, item: Item, variation: ItemVariation, voucher: Voucher, subevent: SubEvent,
seat: Seat, sales_channel: SalesChannel, already_in_cart: bool, cart_is_expired: bool, real_now_dt: datetime,
item_requires_seat: bool, is_addon: bool, is_bundled: bool,
):
"""
Checks if a cart position with the given constraints can still be sold. This checks configuration and time-based
constraints of item, subevent, and voucher.
It does NOT
- check if quota/voucher/seat are still available
- check prices
- check memberships
- perform any checks that go beyond the single line (like item.max_per_order)
"""
time_machine_now_dt = time_machine_now(real_now_dt)
# Item or variation disabled
# Item disabled or unavailable by time
if not item.is_available(time_machine_now_dt) or (variation and not variation.is_available(time_machine_now_dt)):
raise CartPositionError(error_messages['unavailable'])
# Invalid media policy for online sale
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
mt = MEDIA_TYPES[item.media_type]
if not mt.medium_created_by_server:
raise CartPositionError(error_messages['media_usage_not_implemented'])
elif item.media_policy == Item.MEDIA_POLICY_REUSE:
raise CartPositionError(error_messages['media_usage_not_implemented'])
# Item removed from sales channel
if not item.all_sales_channels:
if sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
raise CartPositionError(error_messages['unavailable'])
# Variation removed from sales channel
if variation and not variation.all_sales_channels:
if sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
raise CartPositionError(error_messages['unavailable'])
# Item disabled or unavailable by time in subevent
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available(time_machine_now_dt):
raise CartPositionError(error_messages['not_for_sale'])
# Variation disabled or unavailable by time in subevent
if subevent and variation and variation.pk in subevent.var_overrides and \
not subevent.var_overrides[variation.pk].is_available(time_machine_now_dt):
raise CartPositionError(error_messages['not_for_sale'])
# Item requires a variation (should never happen)
if item.has_variations and not variation:
raise CartPositionError(error_messages['not_for_sale'])
# Variation belongs to wrong item (should never happen)
if variation and variation.item_id != item.pk:
raise CartPositionError(error_messages['not_for_sale'])
# Voucher does not apply to product
if voucher and not voucher.applies_to(item, variation):
raise CartPositionError(error_messages['voucher_invalid_item'])
# Voucher does not apply to seat
if voucher and voucher.seat and voucher.seat != seat:
raise CartPositionError(error_messages['voucher_invalid_seat'])
# Voucher does not apply to subevent
if voucher and voucher.subevent_id and voucher.subevent_id != subevent.pk:
raise CartPositionError(error_messages['voucher_invalid_subevent'])
# Voucher expired
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
raise CartPositionError(error_messages['voucher_expired'])
# Subevent has been disabled
if subevent and not subevent.active:
raise CartPositionError(error_messages['inactive_subevent'])
# Subevent sale not started
if subevent and subevent.effective_presale_start and time_machine_now_dt < subevent.effective_presale_start:
raise CartPositionError(error_messages['not_started'])
# Subevent sale has ended
if subevent and subevent.presale_has_ended:
raise CartPositionError(error_messages['ended'])
# Payment for subevent no longer possible
if subevent:
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(subevent).date(),
time(hour=23, minute=59, second=59)
), event.timezone)
if term_last < time_machine_now_dt:
raise CartPositionError(error_messages['payment_ended'])
# Seat required but no seat given
if item_requires_seat and not seat:
raise CartPositionError(error_messages['seat_invalid'])
# Seat given but no seat required
if seat and not item_requires_seat:
raise CartPositionError(error_messages['seat_forbidden'])
# Item requires to be add-on but is top-level position
if item.category and item.category.is_addon and not is_addon:
raise CartPositionError(error_messages['addon_only'])
# Item requires bundling but is top-level position
if item.require_bundling and not is_bundled:
raise CartPositionError(error_messages['bundled_only'])
# Seat for wrong product
if seat and seat.product != item:
raise CartPositionError(error_messages['seat_invalid'])
# Seat blocked
if seat and seat.blocked and sales_channel.identifier not in event.settings.seating_allow_blocked_seats_for_channel:
raise CartPositionError(error_messages['seat_invalid'])
# Item requires voucher but no voucher given
if item.require_voucher and voucher is None and not is_bundled:
raise CartPositionError(error_messages['voucher_required'])
# Item or variation is hidden without voucher but no voucher is given
if (
(item.hide_without_voucher or (variation and variation.hide_without_voucher)) and
(voucher is None or not voucher.show_hidden_items) and
not is_bundled
):
raise CartPositionError(error_messages['voucher_required'])
class CartManager:
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'voucher', 'quotas',
'addon_to', 'subevent', 'bundled', 'seat', 'listed_price',
@@ -433,7 +294,6 @@ class CartManager:
self._widget_data = widget_data or {}
self._sales_channel = sales_channel
self.num_extended_positions = 0
self.price_change_for_extended = False
if reservation_time:
self._reservation_time = reservation_time
@@ -561,14 +421,14 @@ class CartManager:
if cartsize > limit:
raise CartError(error_messages['max_items'] % limit)
def _check_item_constraints(self, op):
def _check_item_constraints(self, op, current_ops=[]):
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
if not (
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
):
if op.item.require_voucher and op.voucher is None:
if getattr(op, 'voucher_ignored', False): # todo??
if getattr(op, 'voucher_ignored', False):
raise CartError(error_messages['voucher_redeemed'])
raise CartError(error_messages['voucher_required'])
@@ -580,39 +440,88 @@ class CartManager:
raise CartError(error_messages['voucher_redeemed'])
raise CartError(error_messages['voucher_required'])
if op.seat and op.count > 1:
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
raise CartError(error_messages['unavailable'])
if op.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
mt = MEDIA_TYPES[op.item.media_type]
if not mt.medium_created_by_server:
raise CartError(error_messages['media_usage_not_implemented'])
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
raise CartError(error_messages['media_usage_not_implemented'])
if not op.item.all_sales_channels:
if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
raise CartError(error_messages['unavailable'])
if op.variation and not op.variation.all_sales_channels:
if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
raise CartError(error_messages['not_for_sale'])
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and \
not op.subevent.var_overrides[op.variation.pk].is_available():
raise CartError(error_messages['not_for_sale'])
if op.item.has_variations and not op.variation:
raise CartError(error_messages['not_for_sale'])
if op.variation and op.variation.item_id != op.item.pk:
raise CartError(error_messages['not_for_sale'])
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
raise CartError(error_messages['voucher_invalid_item'])
if op.voucher and op.voucher.seat and op.voucher.seat != op.seat:
raise CartError(error_messages['voucher_invalid_seat'])
if op.voucher and op.voucher.subevent_id and op.voucher.subevent_id != op.subevent.pk:
raise CartError(error_messages['voucher_invalid_subevent'])
if op.subevent and not op.subevent.active:
raise CartError(error_messages['inactive_subevent'])
if op.subevent and op.subevent.presale_start and time_machine_now(self.real_now_dt) < op.subevent.presale_start:
raise CartError(error_messages['not_started'])
if op.subevent and op.subevent.presale_has_ended:
raise CartError(error_messages['ended'])
seated = self._is_seated(op.item, op.subevent)
if (
seated and (
not op.seat or (
op.seat.blocked and
self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
)
)
):
raise CartError(error_messages['seat_invalid'])
elif op.seat and not seated:
raise CartError(error_messages['seat_forbidden'])
elif op.seat and op.seat.product != op.item:
raise CartError(error_messages['seat_invalid'])
elif op.seat and op.count > 1:
raise CartError('Invalid request: A seat can only be bought once.')
if isinstance(op, self.AddOperation):
is_addon = op.addon_to
is_bundled = op.addon_to == "FAKE"
else:
is_addon = op.position.addon_to
is_bundled = op.position.is_bundled
if op.subevent:
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(op.subevent).date(),
time(hour=23, minute=59, second=59)
), self.event.timezone)
if term_last < time_machine_now(self.real_now_dt):
raise CartError(error_messages['payment_ended'])
try:
_check_position_constraints(
event=self.event,
item=op.item,
variation=op.variation,
voucher=op.voucher,
subevent=op.subevent,
seat=op.seat,
sales_channel=self._sales_channel,
already_in_cart=isinstance(op, self.ExtendOperation),
cart_is_expired=isinstance(op, self.ExtendOperation),
real_now_dt=self.real_now_dt,
item_requires_seat=self._is_seated(op.item, op.subevent),
is_addon=is_addon,
is_bundled=is_bundled,
)
# Quota, seat, and voucher availability is checked for in perform_operations
# Price changes are checked for in extend_expired_positions
except CartPositionError as e:
if e.args[0] == error_messages['voucher_required'] and getattr(op, 'voucher_ignored', False):
# This is the case where someone clicks +1 on a voucher-only item with a fully redeemed voucher:
raise CartPositionError(error_messages['voucher_redeemed'])
raise
if isinstance(op, self.AddOperation):
if op.item.category and op.item.category.is_addon and not (op.addon_to and op.addon_to != 'FAKE'):
raise CartError(error_messages['addon_only'])
if op.item.require_bundling and not op.addon_to == 'FAKE':
raise CartError(error_messages['bundled_only'])
def _get_price(self, item: Item, variation: Optional[ItemVariation],
voucher: Optional[Voucher], custom_price: Optional[Decimal],
@@ -632,7 +541,7 @@ class CartManager:
else:
raise e
def _extend_expired_positions(self):
def extend_expired_positions(self):
requires_seat = Exists(
SeatCategoryMapping.objects.filter(
Q(product=OuterRef('item'))
@@ -695,14 +604,10 @@ class CartManager:
quotas=quotas, subevent=cp.subevent, seat=cp.seat, listed_price=listed_price,
price_after_voucher=price_after_voucher,
)
try:
self._check_item_constraints(op)
except CartPositionError as e:
self._operations.append(self.RemoveOperation(position=cp))
err = error_messages['positions_removed'] % str(e)
self._check_item_constraints(op)
if cp.voucher:
self._voucher_use_diff[cp.voucher] += 1
self._voucher_use_diff[cp.voucher] += 2
self._operations.append(op)
return err
@@ -892,7 +797,7 @@ class CartManager:
custom_price_input_is_net=False,
voucher_ignored=False,
)
self._check_item_constraints(bop)
self._check_item_constraints(bop, operations)
bundled.append(bop)
listed_price = get_listed_price(item, variation, subevent)
@@ -931,7 +836,7 @@ class CartManager:
custom_price_input_is_net=self.event.settings.display_net_prices,
voucher_ignored=voucher_ignored,
)
self._check_item_constraints(op)
self._check_item_constraints(op, operations)
operations.append(op)
self._quota_diff.update(quota_diff)
@@ -1070,7 +975,7 @@ class CartManager:
custom_price_input_is_net=self.event.settings.display_net_prices,
voucher_ignored=False,
)
self._check_item_constraints(op)
self._check_item_constraints(op, operations)
operations.append(op)
# Check constraints on the add-on combinations
@@ -1267,9 +1172,7 @@ class CartManager:
op.position.delete()
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
if isinstance(op, self.ExtendOperation) and (op.position.pk in deleted_positions or not op.position.pk):
continue # Already deleted in other operation
# Create a CartPosition for as many items as we can
# Create a CartPosition for as much items as we can
requested_count = quota_available_count = voucher_available_count = op.count
if op.seat:
@@ -1440,8 +1343,6 @@ class CartManager:
addons.delete()
op.position.delete()
elif available_count == 1:
if op.price_after_voucher != op.position.price_after_voucher:
self.price_change_for_extended = True
op.position.expires = self._expiry
op.position.max_extend = self._max_expiry_extend
op.position.listed_price = op.listed_price
@@ -1543,24 +1444,15 @@ class CartManager:
return diff
def _remove_parents_if_bundles_are_removed(self):
removed_positions = {op.position.pk for op in self._operations if isinstance(op, self.RemoveOperation)}
for op in self._operations:
if isinstance(op, self.RemoveOperation):
if op.position.is_bundled and op.position.addon_to_id not in removed_positions:
self._operations.append(self.RemoveOperation(position=op.position.addon_to))
removed_positions.add(op.position.addon_to_id)
def commit(self):
self._check_presale_dates()
self._check_max_cart_size()
err = self._delete_out_of_timeframe()
err = self._extend_expired_positions() or err
err = self.extend_expired_positions() or err
err = err or self._check_min_per_voucher()
self._extend_expiry_of_valid_existing_positions()
self._remove_parents_if_bundles_are_removed()
err = self._perform_operations() or err
self.recompute_final_prices_and_taxes()
@@ -1816,12 +1708,7 @@ def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en',
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
cm.commit()
return {
"success": cm.num_extended_positions,
"expiry": cm._expiry,
"max_expiry_extend": cm._max_expiry_extend,
"price_changed": cm.price_change_for_extended,
}
return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -81,7 +81,7 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import cart, tickets
from pretix.base.services import tickets
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified,
invoice_transmission_separately, order_invoice_transmission_separately,
@@ -130,9 +130,6 @@ class OrderError(Exception):
error_messages = {
'positions_removed': gettext_lazy(
'Some products can no longer be purchased and have been removed from your cart for the following reason: %s'
),
'unavailable': gettext_lazy(
'Some of the products you selected were no longer available. '
'Please see below for details.'
@@ -185,6 +182,14 @@ error_messages = {
'The voucher code used for one of the items in your cart is not valid for this item. We removed this item from your cart.'
),
'voucher_required': gettext_lazy('You need a valid voucher code to order one of the products.'),
'some_subevent_not_started': gettext_lazy(
'The booking period for one of the events in your cart has not yet started. The '
'affected positions have been removed from your cart.'
),
'some_subevent_ended': gettext_lazy(
'The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'
),
'seat_invalid': gettext_lazy('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': gettext_lazy('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
@@ -739,37 +744,12 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
deleted_positions.add(cp.pk)
cp.delete()
sorted_positions = list(sorted(positions, key=lambda c: (-int(c.is_bundled), c.pk)))
sorted_positions = sorted(positions, key=lambda c: (-int(c.is_bundled), c.pk))
for cp in sorted_positions:
cp._cached_quotas = list(cp.quotas)
for cp in sorted_positions:
try:
cart._check_position_constraints(
event=event,
item=cp.item,
variation=cp.variation,
voucher=cp.voucher,
subevent=cp.subevent,
seat=cp.seat,
sales_channel=sales_channel,
already_in_cart=True,
cart_is_expired=cp.expires < now_dt,
real_now_dt=now_dt,
item_requires_seat=cp.requires_seat,
is_addon=bool(cp.addon_to_id),
is_bundled=bool(cp.addon_to_id) and cp.is_bundled,
)
# Quota, seat, and voucher availability is checked for below
# Prices are checked for below
# Memberships are checked in _create_order
except cart.CartPositionError as e:
err = error_messages['positions_removed'] % str(e)
delete(cp)
# Create locks
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
if any(cp.expires < now() + timedelta(seconds=LOCK_TRUST_WINDOW) for cp in sorted_positions):
# No need to perform any locking if the cart positions still guarantee everything long enough.
full_lock_required = any(
@@ -794,12 +774,15 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
# Check availability
for i, cp in enumerate(sorted_positions):
if cp.pk in deleted_positions or not cp.pk:
if cp.pk in deleted_positions:
continue
if not cp.item.is_available() or (cp.variation and not cp.variation.is_available()):
err = err or error_messages['unavailable']
delete(cp)
continue
quotas = cp._cached_quotas
# Product per order limits
products_seen[cp.item] += 1
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
err = error_messages['max_items_per_product'] % {
@@ -809,7 +792,6 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
delete(cp)
break
# Voucher availability
if cp.voucher:
v_usages[cp.voucher] += 1
if cp.voucher not in v_avail:
@@ -824,14 +806,48 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
delete(cp)
continue
# Check duplicate seats in order
if cp.seat in seats_seen:
err = err or error_messages['seat_invalid']
if cp.subevent and cp.subevent.presale_start and time_machine_now_dt < cp.subevent.presale_start:
err = err or error_messages['some_subevent_not_started']
delete(cp)
break
if cp.subevent:
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if tlv:
term_last = make_aware(datetime.combine(
tlv.datetime(cp.subevent).date(),
time(hour=23, minute=59, second=59)
), event.timezone)
if term_last < time_machine_now_dt:
err = err or error_messages['some_subevent_ended']
delete(cp)
break
if cp.subevent and cp.subevent.presale_has_ended:
err = err or error_messages['some_subevent_ended']
delete(cp)
break
if (cp.requires_seat and not cp.seat) or (cp.seat and not cp.requires_seat) or (cp.seat and cp.seat.product != cp.item) or cp.seat in seats_seen:
err = err or error_messages['seat_invalid']
delete(cp)
break
if cp.seat:
seats_seen.add(cp.seat)
if cp.item.require_voucher and cp.voucher is None and not cp.is_bundled:
delete(cp)
err = err or error_messages['voucher_required']
break
if (cp.item.hide_without_voucher or (cp.variation and cp.variation.hide_without_voucher)) and (
cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)
) and not cp.is_bundled:
delete(cp)
err = error_messages['voucher_required']
break
if cp.seat:
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
# time, since we absolutely can not overbook a seat.
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
@@ -839,13 +855,34 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
delete(cp)
continue
# Check useful quota configuration
if cp.expires >= now_dt and not cp.voucher:
# Other checks are not necessary
continue
if len(quotas) == 0:
err = err or error_messages['unavailable']
delete(cp)
continue
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(time_machine_now_dt):
err = err or error_messages['unavailable']
delete(cp)
continue
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
not cp.subevent.var_overrides[cp.variation.pk].is_available(time_machine_now_dt):
err = err or error_messages['unavailable']
delete(cp)
continue
if cp.voucher:
if cp.voucher.valid_until and cp.voucher.valid_until < time_machine_now_dt:
err = err or error_messages['voucher_expired']
delete(cp)
continue
quota_ok = True
ignore_all_quotas = cp.expires >= now_dt or (
cp.voucher and (
cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)
@@ -877,7 +914,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
})
# Check prices
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
old_total = sum(cp.price for cp in sorted_positions)
for i, cp in enumerate(sorted_positions):
if cp.listed_price is None:
@@ -908,7 +945,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
delete(cp)
continue
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
discount_results = apply_discounts(
event,
sales_channel.identifier,

View File

@@ -1,65 +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/>.
#
from datetime import datetime
from django import template
from django.utils.html import format_html
from django.utils.timezone import get_current_timezone
from pretix.base.i18n import LazyExpiresDate
from pretix.helpers.templatetags.date_fast import date_fast
register = template.Library()
@register.simple_tag
def html_time(value: datetime, dt_format: str = "SHORT_DATE_FORMAT", **kwargs):
"""
Building a <time datetime='{html-datetime}'>{human-readable datetime}</time> html string,
where the html-datetime as well as the human-readable datetime can be set
to a value from django's FORMAT_SETTINGS or "format_expires".
If attr_fmt isnt provided, it will be set to isoformat.
Usage example:
{% html_time event_start "SHORT_DATETIME_FORMAT" %}
or
{% html_time event_start "TIME_FORMAT" attr_fmt="H:i" %}
"""
if value in (None, ''):
return ''
value = value.astimezone(get_current_timezone())
attr_fmt = kwargs["attr_fmt"] if kwargs else None
try:
if not attr_fmt:
date_html = value.isoformat()
else:
date_html = date_fast(value, attr_fmt)
if dt_format == "format_expires":
date_human = LazyExpiresDate(value)
else:
date_human = date_fast(value, dt_format)
return format_html("<time datetime='{}'>{}</time>", date_html, date_human)
except AttributeError:
return ''

View File

@@ -36,8 +36,9 @@ class DownloadView(TemplateView):
def object(self) -> CachedFile:
try:
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
if not o.allowed_for_session(self.request):
raise Http404()
if o.session_key:
if o.session_key != self.request.session.session_key:
raise Http404()
return o
except (ValueError, ValidationError): # Invalid URLs
raise Http404()

View File

@@ -61,10 +61,6 @@ from pretix.base.models import (
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
)
from pretix.base.signals import register_payment_providers
from pretix.base.timeframes import (
DateFrameField,
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
from pretix.control.forms import SplitDateTimeField
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
from pretix.control.signals import order_search_filter_q
@@ -1223,129 +1219,6 @@ class OrderPaymentSearchFilterForm(forms.Form):
return qs
class QuestionAnswerFilterForm(forms.Form):
STATUS_VARIANTS = [
("", _("All orders")),
(Order.STATUS_PAID, _("Paid")),
(Order.STATUS_PAID + 'v', _("Paid or confirmed")),
(Order.STATUS_PENDING, _("Pending")),
(Order.STATUS_PENDING + Order.STATUS_PAID, _("Pending or paid")),
("o", _("Pending (overdue)")),
(Order.STATUS_EXPIRED, _("Expired")),
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _("Pending or expired")),
(Order.STATUS_CANCELED, _("Canceled"))
]
status = forms.ChoiceField(
choices=STATUS_VARIANTS,
required=False,
label=_("Order status"),
)
item = forms.ChoiceField(
choices=[],
required=False,
label=_("Products"),
)
subevent = forms.ModelChoiceField(
queryset=SubEvent.objects.none(),
required=False,
empty_label=pgettext_lazy('subevent', 'All dates'),
label=pgettext_lazy("subevent", "Date"),
)
date_range = DateFrameField(
required=False,
include_future_frames=True,
label=_('Event date'),
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.initial['status'] = Order.STATUS_PENDING + Order.STATUS_PAID
choices = [('', _('All products'))]
for i in self.event.items.prefetch_related('variations').all():
variations = list(i.variations.all())
if variations:
choices.append((str(i.pk), _('{product} Any variation').format(product=str(i))))
for v in variations:
choices.append(('%d-%d' % (i.pk, v.pk), '%s %s' % (str(i), v.value)))
else:
choices.append((str(i.pk), str(i)))
self.fields['item'].choices = choices
if self.event.has_subevents:
self.fields["subevent"].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2(
attrs={
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
else:
del self.fields['subevent']
def clean(self):
cleaned_data = super().clean()
subevent = cleaned_data.get('subevent')
date_range = cleaned_data.get('date_range')
if subevent is not None and date_range is not None:
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
if (
(d_start and not (d_start <= subevent.date_from)) or
(d_end and not (subevent.date_from < d_end))
):
self.add_error('subevent', pgettext_lazy('subevent', "Date doesn't start in selected date range."))
return cleaned_data
def filter_qs(self, opqs):
fdata = self.cleaned_data
subevent = fdata.get('subevent', None)
date_range = fdata.get('date_range', None)
if subevent is not None:
opqs = opqs.filter(subevent=subevent)
if date_range is not None:
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
opqs = opqs.filter(
subevent__date_from__gte=d_start,
subevent__date_from__lt=d_end
)
s = fdata.get("status", Order.STATUS_PENDING + Order.STATUS_PAID)
if s != "":
if s == Order.STATUS_PENDING:
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
order__expires__lt=now().replace(hour=0, minute=0, second=0))
elif s == Order.STATUS_PENDING + Order.STATUS_PAID:
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
elif s == Order.STATUS_PAID + 'v':
opqs = opqs.filter(
Q(order__status=Order.STATUS_PAID) |
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
)
elif s == Order.STATUS_PENDING + Order.STATUS_EXPIRED:
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
else:
opqs = opqs.filter(order__status=s)
if s not in (Order.STATUS_CANCELED, ""):
opqs = opqs.filter(canceled=False)
if fdata.get("item", "") != "":
i = fdata.get("item", "")
opqs = opqs.filter(item_id__in=(i,))
return opqs
class SubEventFilterForm(FilterForm):
orders = {
'date_from': 'date_from',

View File

@@ -20,20 +20,35 @@
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-2 col-xs-6">
{% bootstrap_field form.status %}
</div>
<div class="col-md-3 col-xs-6">
{% bootstrap_field form.item %}
</div>
{% if has_subevents %}
<div class="col-md-3 col-xs-6">
{% bootstrap_field form.subevent %}
<div class="col-lg-2 col-sm-6 col-xs-6">
<select name="status" class="form-control">
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
</select>
</div>
<div class="col-md-4 col-xs-6">
{% bootstrap_field form.date_range %}
<div class="col-lg-5 col-sm-6 col-xs-6">
<select name="item" class="form-control">
<option value="">{% trans "All products" %}</option>
{% for item in items %}
<option value="{{ item.id }}"
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{{ item.name }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if request.event.has_subevents %}
<div class="col-lg-5 col-sm-6 col-xs-6">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</div>
{% endif %}
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">

View File

@@ -57,6 +57,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView
from django_otp import match_token
from django_otp.plugins.otp_static.models import StaticDevice
from webauthn.helpers import generate_challenge
from pretix.base.auth import get_auth_backends
@@ -538,6 +539,10 @@ class Login2FAView(TemplateView):
break
else:
valid = match_token(self.user, token)
if isinstance(valid, StaticDevice):
self.user.send_security_notice([
_("A recovery code for two-factor authentification was used to log in.")
])
if valid:
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")

View File

@@ -65,7 +65,7 @@ from pretix.api.serializers.item import (
)
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation,
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
SeatCategoryMapping, Voucher,
)
@@ -74,7 +74,6 @@ from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tickets import invalidate_cache
from pretix.base.signals import quota_availability
from pretix.control.forms.filter import QuestionAnswerFilterForm
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
@@ -661,26 +660,46 @@ class QuestionMixin:
return ctx
class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView):
class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingView, DetailView):
model = Question
template_name = 'pretixcontrol/items/question.html'
permission = 'can_change_items'
template_name_field = 'question'
@cached_property
def filter_form(self):
return QuestionAnswerFilterForm(event=self.request.event, data=self.request.GET)
def get_answer_statistics(self):
opqs = OrderPosition.objects.filter(
order__event=self.request.event,
)
if self.filter_form.is_valid():
opqs = self.filter_form.filter_qs(opqs)
qs = QuestionAnswer.objects.filter(
question=self.object, orderposition__isnull=False,
)
if self.request.GET.get("subevent", "") != "":
opqs = opqs.filter(subevent=self.request.GET["subevent"])
s = self.request.GET.get("status", "np")
if s != "":
if s == 'o':
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
order__expires__lt=now().replace(hour=0, minute=0, second=0))
elif s == 'np':
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
elif s == 'pv':
opqs = opqs.filter(
Q(order__status=Order.STATUS_PAID) |
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
)
elif s == 'ne':
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
else:
opqs = opqs.filter(order__status=s)
if s not in (Order.STATUS_CANCELED, ""):
opqs = opqs.filter(canceled=False)
if self.request.GET.get("item", "") != "":
i = self.request.GET.get("item", "")
opqs = opqs.filter(item_id__in=(i,))
qs = qs.filter(orderposition__in=opqs)
op_cnt = opqs.filter(item__in=self.object.items.all()).count()
@@ -727,11 +746,9 @@ class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['items'] = self.object.items.exists()
ctx['has_subevents'] = self.request.event.has_subevents
ctx['items'] = self.object.items.all()
stats = self.get_answer_statistics()
ctx['stats'], ctx['total'] = stats
ctx['form'] = self.filter_form
return ctx
def get_object(self, queryset=None) -> Question:

View File

@@ -38,7 +38,6 @@ from datetime import timedelta
from django.conf import settings
from django.contrib import messages
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
@@ -86,7 +85,6 @@ class BaseImportView(TemplateView):
filename='import.csv',
type='text/csv',
)
cf.bind_to_session(request, "modelimport")
cf.file.save('import.csv', request.FILES['file'])
if self.request.POST.get("charset") in ENCODINGS:
@@ -139,10 +137,7 @@ class BaseProcessView(AsyncAction, FormView):
@cached_property
def file(self):
cf = get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
if not cf.allowed_for_session(self.request, "modelimport"):
raise Http404()
return cf
return get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
@cached_property
def parsed(self):

View File

@@ -3016,7 +3016,7 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
return _('All orders have been canceled.')
else:
return _('The orders have been canceled. An error occurred with {count} orders, please '
'check all uncanceled orders.').format(count=value["failed"])
'check all uncanceled orders.').format(count=value)
def get_success_url(self, value):
if settings.HAS_CELERY:
@@ -3097,7 +3097,7 @@ class EventCancelConfirm(EventPermissionRequiredMixin, AsyncAction, FormView):
return _('All orders have been canceled.')
else:
return _('The orders have been canceled. An error occurred with {count} orders, please '
'check all uncanceled orders.').format(count=value["failed"])
'check all uncanceled orders.').format(count=value)
def get_success_url(self, value):
return reverse('control:event.cancel', kwargs={

View File

@@ -247,7 +247,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
cf = None
if request.POST.get("background", "").strip():
try:
cf = CachedFile.objects.get(id=request.POST.get("background"), web_download=True)
cf = CachedFile.objects.get(id=request.POST.get("background"))
except CachedFile.DoesNotExist:
pass

View File

@@ -38,8 +38,7 @@ from collections import OrderedDict
from zipfile import ZipFile
from django.contrib import messages
from django.http import Http404
from django.shortcuts import redirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import get_language, gettext_lazy as _
@@ -95,8 +94,6 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
cf = CachedFile.objects.get(pk=kwargs['file'])
except CachedFile.DoesNotExist:
raise ShredError(_("The download file could no longer be found on the server, please try to start again."))
if not cf.allowed_for_session(self.request):
raise Http404()
with ZipFile(cf.file.file, 'r') as zipfile:
indexdata = json.loads(zipfile.read('index.json').decode())
@@ -114,7 +111,7 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
ctx = super().get_context_data(**kwargs)
ctx['shredders'] = self.shredders
ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders)
ctx['file'] = cf
ctx['file'] = get_object_or_404(CachedFile, pk=kwargs.get("file"))
return ctx

View File

@@ -165,6 +165,9 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
d, __ = StaticDevice.objects.get_or_create(user=self.object, name='emergency')
token = d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
self.object.log_action('pretix.user.settings.2fa.emergency', user=self.request.user)
self.object.send_security_notice([
_('A two-factor emergency code has been generated by a system administrator.')
])
messages.success(request, _(
'The emergency token for this user is "{token}". It can only be used once. Please make sure to transmit '

View File

@@ -5,16 +5,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-27 13:57+0000\n"
"PO-Revision-Date: 2025-12-18 15:04+0000\n"
"PO-Revision-Date: 2025-11-27 14:28+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
">\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15\n"
"X-Generator: Weblate 5.14.3\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py:87

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-27 13:57+0000\n"
"PO-Revision-Date: 2025-12-15 20:00+0000\n"
"PO-Revision-Date: 2025-12-05 18:00+0000\n"
"Last-Translator: sandra r <sandrarial@gestiontickets.online>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix/"
"gl/>\n"
@@ -6506,6 +6506,7 @@ msgstr "pendiente"
#: pretix/base/models/orders.py:203 pretix/base/payment.py:570
#: pretix/base/services/invoices.py:581
#, fuzzy
msgid "paid"
msgstr "pagado"
@@ -6839,8 +6840,9 @@ msgid "This reference will be printed on your invoice for your convenience."
msgstr "Esta referencia imprimirase na súa factura para a súa conveniencia."
#: pretix/base/models/orders.py:3534
#, fuzzy
msgid "Transmission type"
msgstr "Medio de contacto"
msgstr "digo de transacción"
#: pretix/base/models/orders.py:3632
#: pretix/plugins/badges/templates/pretixplugins/badges/control_order_position_buttons.html:9
@@ -9293,10 +9295,10 @@ msgid "Your exported data exceeded the size limit for scheduled exports."
msgstr ""
#: pretix/base/services/invoices.py:116
#, python-brace-format
#, fuzzy, python-brace-format
msgctxt "invoice"
msgid "Please complete your payment before {expire_date}."
msgstr "Complete o seu pago antes de {expire_date}."
msgstr "Por favor complete su pago antes de {expire_date}."
#: pretix/base/services/invoices.py:128
#, python-brace-format
@@ -21607,8 +21609,9 @@ msgid "Placed order"
msgstr "Pedido realizado"
#: pretix/control/templates/pretixcontrol/event/mail.html:93
#, fuzzy
msgid "Paid order"
msgstr "Pedido pagado"
msgstr "Orden de pago"
#: pretix/control/templates/pretixcontrol/event/mail.html:96
msgid "Free order"
@@ -24213,8 +24216,9 @@ msgstr "Sí, aprobar la orden"
#: pretix/control/templates/pretixcontrol/order/index.html:166
#: pretix/presale/templates/pretixpresale/event/order.html:483
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:7
#, fuzzy
msgid "Cancel order"
msgstr "Cancelar a orde"
msgstr "Cancelar orden"
#: pretix/control/templates/pretixcontrol/order/cancel.html:12
#: pretix/control/templates/pretixcontrol/order/deny.html:11
@@ -24974,8 +24978,9 @@ msgstr "Cambiar"
#: pretix/control/templates/pretixcontrol/order/index.html:1034
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:90
#: pretix/presale/templates/pretixpresale/event/order.html:318
#, fuzzy
msgid "ZIP code and city"
msgstr "Código postal e cidade"
msgstr "Código postal y ciudad"
#: pretix/control/templates/pretixcontrol/order/index.html:1047
#, fuzzy
@@ -25432,8 +25437,9 @@ msgid "Preview refund amount"
msgstr "Reembolso"
#: pretix/control/templates/pretixcontrol/orders/cancel.html:88
#, fuzzy
msgid "Cancel all orders"
msgstr "Cancelar todos os pedidos"
msgstr "Cancelar orden"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:13
#, fuzzy
@@ -33248,8 +33254,9 @@ msgid "Payment reversed."
msgstr "Pago anulado."
#: pretix/plugins/paypal2/signals.py:62
#, fuzzy
msgid "Payment pending."
msgstr "Pago pendente."
msgstr "Pago pendiente."
#: pretix/plugins/paypal2/signals.py:63
#, fuzzy
@@ -33391,8 +33398,9 @@ msgstr "Por favor, inténtalo de nuevo."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pay.html:29
#: pretix/presale/templates/pretixpresale/event/checkout_payment.html:57
#, fuzzy
msgid "Please select how you want to pay."
msgstr "Por favor, seleccione cómo desexa pagar."
msgstr "Por favor, seleccione cómo desea pagar."
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html:10
#, fuzzy
@@ -34542,7 +34550,7 @@ msgstr ""
#: pretix/plugins/stripe/payment.py:337
msgid "Credit card payments"
msgstr "Pagos con tarxeta de crédito"
msgstr "Pagos con cartón de crédito"
#: pretix/plugins/stripe/payment.py:342 pretix/plugins/stripe/payment.py:1527
msgid "iDEAL"
@@ -35095,12 +35103,18 @@ msgstr "Titular de la cuenta"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_simple.html:7
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_simple_messaging_noform.html:13
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_simple_noform.html:5
#, fuzzy
#| msgid ""
#| "After you submitted your order, we will redirect you to the payment "
#| "service provider to complete your payment. You will then be redirected "
#| "back here to get your tickets."
msgid ""
"After you submitted your order, we will redirect you to the payment service "
"provider to complete your payment. You will then be redirected back here."
msgstr ""
"Despois de enviar o teu pedido, redirixirémoste ao provedor de servizos de "
"pagamento para completar o pago. Despois, serás redirixido de novo aquí."
"Despois de que enviase o seu pedido, redirixirémoslle ao provedor de "
"servizos de pago para completar o seu pago. A continuación, redirixiráselle "
"de novo aquí para obter as súas entradas."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form_card.html:9
msgid ""
@@ -35476,8 +35490,9 @@ msgid "Download tickets (PDF)"
msgstr "Descargar tickets (PDF)"
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:66
#, fuzzy
msgid "Download ticket (PDF)"
msgstr "Descargar ticket (PDF)"
msgstr "Descargar ticket"
#: pretix/plugins/ticketoutputpdf/views.py:62
#, fuzzy
@@ -35616,6 +35631,7 @@ msgstr ""
"selecciona un método de pago."
#: pretix/presale/checkoutflow.py:1393 pretix/presale/views/order.py:679
#, fuzzy
msgid "Please select a payment method."
msgstr "Por favor seleccione un método de pago."
@@ -36076,8 +36092,9 @@ msgid "Cart expired"
msgstr "O carro da compra caducou"
#: pretix/presale/templates/pretixpresale/event/checkout_base.html:36
#, fuzzy
msgid "Show full cart"
msgstr "Mostrar o carro completo"
msgstr "Mostrar información"
#: pretix/presale/templates/pretixpresale/event/checkout_base.html:52
#: pretix/presale/templates/pretixpresale/event/index.html:86
@@ -36169,8 +36186,9 @@ msgstr ""
"un enlace que puede utilizar para pagar."
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:215
#, fuzzy
msgid "Place binding order"
msgstr "Realizar orde"
msgstr "Colocar orden de compra"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:217
msgid "Submit registration"
@@ -36769,9 +36787,9 @@ msgstr[0] "Unha entrada"
msgstr[1] "%(num)s entradas"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:485
#, python-format
#, fuzzy, python-format
msgid "incl. %(tax_sum)s taxes"
msgstr "incl. %(tax_sum)s IVE"
msgstr "incl. %(tax_sum)s impuestos"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:505
#, python-format
@@ -37094,8 +37112,9 @@ msgid "Confirmed"
msgstr "Confirmado"
#: pretix/presale/templates/pretixpresale/event/fragment_order_status.html:15
#, fuzzy
msgid "Payment pending"
msgstr "Pago pendente"
msgstr "Pago pendiente"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:21
#, fuzzy
@@ -37119,11 +37138,11 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:131
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:288
#, python-format
#, fuzzy, python-format
msgid "%(amount)s× in your cart"
msgid_plural "%(amount)s× in your cart"
msgstr[0] "%(amount)s× no teu carro"
msgstr[1] "%(amount)s× no teu carro"
msgstr[0] "%(count)s elementos"
msgstr[1] "%(count)s elementos"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:209
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:374
@@ -37354,9 +37373,9 @@ msgstr "O seu pedido foi procesado con éxito! Ver abaixo para máis detalles."
#: pretix/presale/templates/pretixpresale/event/order.html:19
#: pretix/presale/templates/pretixpresale/event/order.html:50
#, fuzzy
msgid "We successfully received your payment. See below for details."
msgstr ""
"Recibimos o teu pagamento correctamente. Consulta os detalles a continuación."
msgstr "Hemos recibido con éxito su pago. Ver abajo para más detalles."
#: pretix/presale/templates/pretixpresale/event/order.html:35
#, fuzzy
@@ -37377,18 +37396,24 @@ msgstr ""
"organizador del evento antes de que pueda pagar y completar este pedido."
#: pretix/presale/templates/pretixpresale/event/order.html:43
#, fuzzy
msgid "Please note that we still await your payment to complete the process."
msgstr "Ten en conta que aínda agardamos o teu pago para completar o proceso."
msgstr "Tenga en cuenta que aún esperamos su pago para completar el proceso."
#: pretix/presale/templates/pretixpresale/event/order.html:55
#, fuzzy
#| msgid ""
#| "Please bookmark or save the link to this exact page if you want to access "
#| "your order later. We also sent you an email containing the link to the "
#| "address you specified."
msgid ""
"Please bookmark or save the link to this exact page if you want to access "
"your order later. We also sent you an email to the address you specified "
"containing the link to this page."
msgstr ""
"Por favor, garda a ligazón a esta páxina exacta se queres acceder ao teu "
"pedido máis tarde. Tamén che enviamos un correo electrónico ao enderezo que "
"especificaches coa ligazón a esta páxina."
"Por favor, marque ou garde a ligazón a esta páxina exacta se desexa acceder "
"ao seu pedido máis tarde. Tamén lle enviamos un correo electrónico coa "
"ligazón ao enderezo que vostede especificou."
#: pretix/presale/templates/pretixpresale/event/order.html:59
#, fuzzy
@@ -37410,9 +37435,9 @@ msgid "View in backend"
msgstr "Ver en el backend"
#: pretix/presale/templates/pretixpresale/event/order.html:91
#, python-format
#, fuzzy, python-format
msgid "A payment of %(total)s is still pending for this order."
msgstr "Un pago de %(total)s aínda está pendente para esta orde."
msgstr "Un pago de %(total)s todavía está pendiente para esta orden."
#: pretix/presale/templates/pretixpresale/event/order.html:96
#, fuzzy, python-format
@@ -37521,9 +37546,10 @@ msgid "Change your order"
msgstr "Cancelar orden"
#: pretix/presale/templates/pretixpresale/event/order.html:358
#, fuzzy
msgctxt "action"
msgid "Cancel your order"
msgstr "Cancela o teu pedido"
msgstr "Cancelar orden"
#: pretix/presale/templates/pretixpresale/event/order.html:366
msgid ""
@@ -37639,9 +37665,9 @@ msgid "Request cancellation: %(code)s"
msgstr "Pedido cancelado: %(code)s"
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:15
#, python-format
#, fuzzy, python-format
msgid "Cancel order: %(code)s"
msgstr "Cancelar orde: %(code)s"
msgstr "Cancelar orden: %(code)s"
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:38
msgid ""
@@ -37727,12 +37753,14 @@ msgid "Modify order: %(code)s"
msgstr "Modificar pedido: %(code)s"
#: pretix/presale/templates/pretixpresale/event/order_modify.html:18
#, fuzzy
msgid ""
"Modifying your invoice address will not automatically generate a new "
"invoice. Please contact us if you need a new invoice."
msgstr ""
"A modificación do enderezo de facturación non xerará automaticamente unha "
"nova factura. Póñase en contacto connosco se precisa unha nova factura."
"La modificación de la dirección de facturación no generará automáticamente "
"una nueva factura. Póngase en contacto con nosotros si necesita una nueva "
"factura."
#: pretix/presale/templates/pretixpresale/event/order_modify.html:88
#: pretix/presale/templates/pretixpresale/event/position_modify.html:49
@@ -38581,8 +38609,10 @@ msgid "Your cart is now empty."
msgstr "Baleirouse o seu pedido."
#: pretix/presale/views/cart.py:569
#, fuzzy
#| msgid "Your cart has been updated."
msgid "Your cart timeout was extended."
msgstr "Ampliouse o tempo de espera do teu carro."
msgstr "O seu pedido actualizouse."
#: pretix/presale/views/cart.py:584
msgid "The products have been successfully added to your cart."

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-27 13:57+0000\n"
"PO-Revision-Date: 2025-12-18 01:00+0000\n"
"PO-Revision-Date: 2025-12-11 01:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\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 5.15\n"
"X-Generator: Weblate 5.14.3\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -9157,8 +9157,6 @@ msgid ""
"Inconsistent data in row {row}: Column {col} contains value \"{val_line}\", "
"but for this order, the value has already been set to \"{val_order}\"."
msgstr ""
"Dado inconsistente na linha {row}: Coluna {col} contém o valor \"{val_line}"
"\", mas para este pedido, o valor está definido como \"{val_order}\"."
#: pretix/base/services/modelimport.py:168
#: pretix/base/services/modelimport.py:289
@@ -9624,7 +9622,7 @@ msgstr "O cupom foi enviado para {recipient}."
#: pretix/base/settings.py:82
msgid "Compute taxes for every line individually"
msgstr "Computar impostos de cada linha individualmente"
msgstr ""
#: pretix/base/settings.py:83
msgid "Compute taxes based on net total"
@@ -10386,7 +10384,7 @@ msgstr ""
#: pretix/base/settings.py:1139 pretix/base/settings.py:1150
msgid "Automatic, but prefer invoice date over event date"
msgstr "Automático, mas preferir data da fatura ao invés da data do evento"
msgstr ""
#: pretix/base/settings.py:1142 pretix/base/settings.py:1153
msgid "Invoice date"
@@ -10401,8 +10399,6 @@ msgid ""
"This controls what dates are shown on the invoice, but is especially "
"important for electronic invoicing."
msgstr ""
"Controla quais datas são exibidas na fatura, mas é especialmente importante "
"para faturamento eletrônico."
#: pretix/base/settings.py:1166
msgid "Automatically cancel and reissue invoice on address changes"
@@ -13988,8 +13984,6 @@ msgstr "Preços excluindo impostos"
#: pretix/control/forms/event.py:819
msgid "Recommended only if you sell tickets primarily to business customers."
msgstr ""
"Recomendado apenas se você vende ingressos majoritariamente para clientes "
"corporativos."
#: pretix/control/forms/event.py:855
#, fuzzy

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-27 13:57+0000\n"
"PO-Revision-Date: 2025-12-14 00:00+0000\n"
"PO-Revision-Date: 2025-12-09 22:00+0000\n"
"Last-Translator: Lachlan Struthers <lachlan.struthers@om.org>\n"
"Language-Team: Albanian <https://translate.pretix.eu/projects/pretix/pretix/"
"sq/>\n"
@@ -25,19 +25,19 @@ msgstr "Anglisht"
#: pretix/_base_settings.py:88
msgid "German"
msgstr "Gjermanisht"
msgstr ""
#: pretix/_base_settings.py:89
msgid "German (informal)"
msgstr "Gjermanisht (joformale)"
msgstr ""
#: pretix/_base_settings.py:90
msgid "Arabic"
msgstr "Arabisht"
msgstr ""
#: pretix/_base_settings.py:91
msgid "Basque"
msgstr "Baskisht"
msgstr ""
#: pretix/_base_settings.py:92
msgid "Catalan"
@@ -2954,7 +2954,7 @@ msgstr ""
#: pretix/plugins/reports/accountingreport.py:105
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/rule_list.html:67
msgid "All"
msgstr "Të gjitha"
msgstr ""
#: pretix/base/exporters/orderlist.py:1329 pretix/control/forms/filter.py:1450
msgid "Live"
@@ -7434,7 +7434,7 @@ msgstr ""
#: pretix/base/pdf.py:278 pretix/base/pdf.py:307
#: pretix/base/services/checkin.py:362 pretix/control/forms/filter.py:1271
msgid "Friday"
msgstr "E Premte"
msgstr ""
#: pretix/base/pdf.py:282
msgid "Event end date and time"
@@ -8132,27 +8132,27 @@ msgstr ""
#: pretix/base/services/checkin.py:358 pretix/control/forms/filter.py:1267
msgid "Monday"
msgstr "E Hënë"
msgstr ""
#: pretix/base/services/checkin.py:359 pretix/control/forms/filter.py:1268
msgid "Tuesday"
msgstr "E Martë"
msgstr ""
#: pretix/base/services/checkin.py:360 pretix/control/forms/filter.py:1269
msgid "Wednesday"
msgstr "E Mërkurë"
msgstr ""
#: pretix/base/services/checkin.py:361 pretix/control/forms/filter.py:1270
msgid "Thursday"
msgstr "E Enjte"
msgstr ""
#: pretix/base/services/checkin.py:363 pretix/control/forms/filter.py:1272
msgid "Saturday"
msgstr "E Shtunë"
msgstr ""
#: pretix/base/services/checkin.py:364 pretix/control/forms/filter.py:1273
msgid "Sunday"
msgstr "E Diel"
msgstr ""
#: pretix/base/services/checkin.py:368
#, python-brace-format
@@ -13044,7 +13044,7 @@ msgstr ""
#: pretix/control/forms/filter.py:2052 pretix/control/forms/filter.py:2054
#: pretix/control/forms/filter.py:2620 pretix/control/forms/filter.py:2622
msgid "Search query"
msgstr "Kërkim"
msgstr ""
#: pretix/control/forms/filter.py:1528 pretix/control/forms/filter.py:1600
#: pretix/control/templates/pretixcontrol/organizers/customer.html:47
@@ -18274,7 +18274,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/device_logs.html:50
#: pretix/control/templates/pretixcontrol/organizers/logs.html:80
msgid "No results"
msgstr "S'ka rezultate"
msgstr ""
#: pretix/control/templates/pretixcontrol/event/mail.html:7
#: pretix/control/templates/pretixcontrol/organizers/mail.html:11
@@ -19326,51 +19326,51 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:16
msgid "January"
msgstr "Janar"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:17
msgid "February"
msgstr "Shkurt"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:18
msgid "March"
msgstr "Mars"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:19
msgid "April"
msgstr "Prill"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:20
msgid "May"
msgstr "Maj"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:21
msgid "June"
msgstr "Qershor"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:22
msgid "July"
msgstr "Korrik"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:23
msgid "August"
msgstr "Gusht"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:24
msgid "September"
msgstr "Shtator"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:25
msgid "October"
msgstr "Tetor"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:26
msgid "November"
msgstr "Nëntor"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:27
msgid "December"
msgstr "Dhjetor"
msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:32
msgid "Generate report"
@@ -20098,7 +20098,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/items/question.html:91
msgid "Count"
msgstr "Sasia"
msgstr ""
#: pretix/control/templates/pretixcontrol/items/question.html:92
#, python-format
@@ -23098,7 +23098,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/pdf/index.html:52
msgid "Text box"
msgstr "Kutia teksti"
msgstr ""
#: pretix/control/templates/pretixcontrol/pdf/index.html:59
msgid "QR Code"
@@ -29765,7 +29765,7 @@ msgstr ""
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/edit.html:23
msgid "Ticket design"
msgstr "Dizajni i biletës"
msgstr ""
#: pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/edit.html:27
msgid "You can modify the design after you saved this page."
@@ -30328,7 +30328,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:27
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:18
msgid "Cart expired"
msgstr "Shporta juaj u skadua"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_base.html:36
msgid "Show full cart"
@@ -30936,13 +30936,11 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Sendet në shportën tuaj nuk janë të rezervuar më për ju. Ju mund t'a "
"përmbushni porosinë tuaj derisa janë ende të disponueshëm."
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:514
#: pretix/presale/templates/pretixpresale/fragment_modals.html:48
msgid "Renew reservation"
msgstr "Rivendosni rezervimin"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:526
msgid "Reservation renewed"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
"PO-Revision-Date: 2025-12-14 00:00+0000\n"
"PO-Revision-Date: 2025-12-09 22:00+0000\n"
"Last-Translator: Lachlan Struthers <lachlan.struthers@om.org>\n"
"Language-Team: Albanian <https://translate.pretix.eu/projects/pretix/"
"pretix-js/sq/>\n"
@@ -597,367 +597,356 @@ msgstr "Kodi QR për check-in"
#: pretix/static/pretixcontrol/js/ui/editor.js:549
msgid "The PDF background file could not be loaded for the following reason:"
msgstr "Faili bazë PDF nuk mund të shkarkohej për këto arsye:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:904
msgid "Group of objects"
msgstr "Grupi i sendeve"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:909
msgid "Text object (deprecated)"
msgstr "Objekt teksti (i dalë nga përdorimi)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:911
msgid "Text box"
msgstr "Kutia teksti"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:913
msgid "Barcode area"
msgstr "Zona e barkodit"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:915
msgid "Image area"
msgstr "Zona për imazhe"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:917
msgid "Powered by pretix"
msgstr "Mundësuar nga pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:919
msgid "Object"
msgstr "Objekt"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:923
msgid "Ticket design"
msgstr "Dizajni i biletës"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1292
msgid "Saving failed."
msgstr "Ruajtja nuk u krye."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1361
#: pretix/static/pretixcontrol/js/ui/editor.js:1412
msgid "Error while uploading your PDF file, please try again."
msgstr "Pati gabim në ngarkimin e failit tuaj PDF, ju lutemi, provoni përsëri."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1395
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "A vërtetë dëshironi të dilni nga redaktori pa ruajtur ndryshimet tuaja?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:19
msgid "An error has occurred."
msgstr "Një gabim ka ndodhur."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:52
msgid "Generating messages …"
msgstr "Duke prodhuar mesazhe …"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:69
msgid "Unknown error."
msgstr "Gabim i panjohur."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
"Ngjyra juaj ka kontrast shumë të mirë dhe do të ofrojë aksesueshmëri të "
"shkëlqyer."
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
"Ngjyra juaj ka kontrast të mirë dhe përmbush kërkesat minimale të "
"aksesueshmërisë."
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
"Ngjyra juaj nuk ka kontrast të mjaftueshëm me të bardhën. Kjo mund të "
"ndikojë në aksesueshmërinë e faqes suaj."
#: pretix/static/pretixcontrol/js/ui/main.js:443
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "Search query"
msgstr "Kërkim"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr "Të gjitha"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None"
msgstr "Asnjë"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:466
msgid "Selected only"
msgstr "Vetëm të zghedhura"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:839
msgid "Enter page number between 1 and %(max)s."
msgstr "Shkruani numrin e faqes midis 1 dhe %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
msgid "Invalid page number."
msgstr "Numër faqe i pavlefshëm."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1000
msgid "Use a different name internally"
msgstr "Përdorni një emër tjetër brenda sistemit"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1040
msgid "Click to close"
msgstr "Shtypni për të mbyllur"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1121
msgid "You have unsaved changes!"
msgstr "Ju keni ndryshime të paruajtur!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
msgid "Calculating default price…"
msgstr "Duke e llogaritur çmimin e parazgjedhur…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/plugins.js:69
msgid "No results"
msgstr "S'ka rezultate"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:41
msgid "Others"
msgstr "Të tjera"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:81
msgid "Count"
msgstr "Sasia"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:112
msgid "(one more date)"
msgid_plural "({num} more dates)"
msgstr[0] "(edhe një datë)"
msgstr[1] "(edhe {num} më shumë data)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:47
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Sendet në shportën tuaj nuk janë të rezervuar më për ju. Ju mund t'a "
"përmbushni porosinë tuaj derisa janë ende të disponueshëm."
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"
msgstr "Shporta juaj u skadua"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:58
#: pretix/static/pretixpresale/js/ui/cart.js:84
msgid "Your cart is about to expire."
msgstr "Shporta juaj do të skadohet së shpejti."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:62
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] "Sendet në shportën tuaj janë të rezervuar për ju për një minutë."
msgstr[1] "Sendet në shportën tuaj janë të rezervuar për ju për {num} minuta."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:83
msgid "Your cart has expired."
msgstr "Shporta juaj është skaduar."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:86
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 ""
"Sendet në shportën tuaj nuk janë të rezervuar më për ju. Ju mund t'a "
"përmbushni porosinë derisa janë ende të disponueshëm."
#: pretix/static/pretixpresale/js/ui/cart.js:87
msgid "Do you want to renew the reservation period?"
msgstr "A dëshironi t'a rivendosni periudhën e rezervimit?"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:90
msgid "Renew reservation"
msgstr "Rivendosni rezervimin"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:194
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "Organizatori mban %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:202
msgid "You get %(currency)s %(amount)s back"
msgstr "Ju merrni %(currency)s %(amount)s kusur"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:218
msgid "Please enter the amount the organizer can keep."
msgstr "Ju lutemi të shkruani shumën që mund të mbajë organizatori."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:577
msgid "Your local time:"
msgstr "Koha juaj vendase:"
msgstr ""
#: pretix/static/pretixpresale/js/walletdetection.js:39
#, fuzzy
msgid "Google Pay"
msgstr "Google Pay"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Quantity"
msgstr "Sasi"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Decrease quantity"
msgstr "Ulni sasinë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Increase quantity"
msgstr "Rrisni sasinë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Filter events by"
msgstr "Filtroni ngjarjet nga"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Filter"
msgstr "Filtri"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "Price"
msgstr "Çmimi"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr "Çmimi origjinal:%s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr "Çmimi i ri:%s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "Select"
msgstr "Zgjidhni"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr "Zgjidhni%s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "Select variant %s"
msgstr "Zgjidhni variant %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Sold out"
msgstr "Shitur të gjitha"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "Buy"
msgstr "Blini"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "Register"
msgstr "Regjistrohuni"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Reserved"
msgstr "Të rezervuar"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "FREE"
msgstr "FALAS"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr "nga %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr "Imazh i %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr "përfsh. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr "shtesë %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "incl. taxes"
msgstr "përfsh. taksat"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "plus taxes"
msgstr "duke shtuar taksat"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr "aktualisht të disponueshëm: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Only available with a voucher"
msgstr "Të disponueshëm vetëm me një kupon"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Not yet available"
msgstr "Të padisponushëm ende"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Not available anymore"
msgstr "Nuk është të disponueshëm më"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Currently not available"
msgstr "Të padisponueshëm për momentin"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr "sasia minimale për porosi: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Close ticket shop"
msgstr "Mbyllni dyqanin e biletave"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr "Dyqani i biletave nuk mund të hapej."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
@@ -965,23 +954,21 @@ msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
"Ka shumë përdorës në dyqanin e biletave tani. Ju lutemi, hapni dyqanin në "
"një skedë të re për të vazhduar."
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgctxt "widget"
msgid "Open ticket shop"
msgstr "Hapni dyqanin e biletave"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Checkout"
msgstr "Pagesa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr "Shporta juaj nuk mund të krijohej. Ju lutemi, provoni përsëri më vonë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgctxt "widget"
@@ -989,14 +976,11 @@ 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 ""
"Nuk mund të krijonim shportën tuaj sepse ka më tepër përdorës në këtë dyqan "
"të biletave. Ju lutemi, shtypni \"Vazhdoni\" të provoni përsëri në një skedë "
"të re."
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgctxt "widget"
msgid "Waiting list"
msgstr "Lista e pritjes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgctxt "widget"
@@ -1004,98 +988,96 @@ msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
"Ju keni tashmë një shportë aktive për këtë ngjarje. Nëse zgjidhni më shumë "
"produkte, do të shtohen në shportën tuaj aktuale."
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgctxt "widget"
msgid "Resume checkout"
msgstr "Vazhdoni pagesën"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgctxt "widget"
msgid "Redeem a voucher"
msgstr "Përdorni një kupon"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgctxt "widget"
msgid "Redeem"
msgstr "Përdorni"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgctxt "widget"
msgid "Voucher code"
msgstr "Kodi i kuponit"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgctxt "widget"
msgid "Close"
msgstr "Mbyllni"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgctxt "widget"
msgid "Close checkout"
msgstr "Kthehuni nga pagesa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgctxt "widget"
msgid "You cannot cancel this operation. Please wait for loading to finish."
msgstr "Ju nuk mund t'a anuloni këtë veprim. Ju lutemi, prisni për ngarkimin."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgctxt "widget"
msgid "Continue"
msgstr "Vazhdoni"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgctxt "widget"
msgid "Show variants"
msgstr "Tregoni variantet"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgctxt "widget"
msgid "Hide variants"
msgstr "Fshehni variantet"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgctxt "widget"
msgid "Choose a different event"
msgstr "Zgjidhni një ngjarje tjetër"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgctxt "widget"
msgid "Choose a different date"
msgstr "Zgjidhni një datë tjetër"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgctxt "widget"
msgid "Back"
msgstr "Kthehuni"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:70
msgctxt "widget"
msgid "Next month"
msgstr "Muaji tjetër"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:71
msgctxt "widget"
msgid "Previous month"
msgstr "Muaji i fundit"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:72
msgctxt "widget"
msgid "Next week"
msgstr "Java tjetër"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:73
msgctxt "widget"
msgid "Previous week"
msgstr "Java e fundit"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:74
msgctxt "widget"
msgid "Open seat selection"
msgstr "Hapni zgjedhjen e sendileve"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:75
msgctxt "widget"
@@ -1104,115 +1086,112 @@ msgid ""
"add yourself to the waiting list. We will then notify if seats are available "
"again."
msgstr ""
"Disa ose të gjitha kategoritë e biletave janë të shitura. Nëse dëshironi, "
"mund të shtoheni në listën e pritjes. Ne do t'ju njoftojmë nëse sendile "
"bëhen të disponueshme përsëri."
#: pretix/static/pretixpresale/js/widget/widget.js:76
msgctxt "widget"
msgid "Load more"
msgstr "Ngarkoni më shumë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "Mo"
msgstr ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:79
msgid "Tu"
msgstr "Ma"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:80
msgid "We"
msgstr ""
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:81
msgid "Th"
msgstr "En"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:82
msgid "Fr"
msgstr "Pr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:83
msgid "Sa"
msgstr "Sh"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:84
msgid "Su"
msgstr "Di"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:85
msgid "Monday"
msgstr "E Hënë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:86
msgid "Tuesday"
msgstr "E Martë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:87
msgid "Wednesday"
msgstr "E Mërkurë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:88
msgid "Thursday"
msgstr "E Enjte"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:89
msgid "Friday"
msgstr "E Premte"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:90
msgid "Saturday"
msgstr "E Shtunë"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:91
msgid "Sunday"
msgstr "E Diel"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:94
msgid "January"
msgstr "Janar"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:95
msgid "February"
msgstr "Shkurt"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:96
msgid "March"
msgstr "Mars"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:97
msgid "April"
msgstr "Prill"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:98
msgid "May"
msgstr "Maj"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:99
msgid "June"
msgstr "Qershor"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:100
msgid "July"
msgstr "Korrik"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:101
msgid "August"
msgstr "Gusht"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:102
msgid "September"
msgstr "Shtator"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:103
msgid "October"
msgstr "Tetor"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:104
msgid "November"
msgstr "Nëntor"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:105
msgid "December"
msgstr "Dhjetor"
msgstr ""

View File

@@ -1,4 +1,3 @@
{% load html_time %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
@@ -22,18 +21,20 @@
{% if event.settings.show_times %}
<br>
<span data-time="{{ ev.date_from.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% html_time ev.date_from "TIME_FORMAT" attr_fmt="H:i" as time%}
{% blocktrans trimmed with time=time %}
Begin: {{ time }}
{% endblocktrans %}
{% with time_human=ev.date_from|date:"TIME_FORMAT" time_24=ev.date_from|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Begin: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% if event.settings.show_date_to and ev.date_to %}
<br>
<span data-time="{{ ev.date_to.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% html_time ev.date_to "TIME_FORMAT" attr_fmt="H:i" as time%}
{% blocktrans trimmed with time=time %}
End: {{ time }}
{% endblocktrans %}
{% with time_human=ev.date_to|date:"TIME_FORMAT" time_24=ev.date_to|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
End: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% endif %}
{% endif %}
@@ -41,17 +42,19 @@
<br>
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
<span data-time="{{ ev.date_admission.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% html_time ev.date_admission "TIME_FORMAT" attr_fmt="H:i" as time%}
{% blocktrans trimmed with time=time %}
Admission: {{ time }}
{% endblocktrans %}
{% with time_human=ev.date_admission|date:"TIME_FORMAT" time_24=ev.date_admission|time:"H:i" %}
{% blocktrans trimmed with time='<time datetime="'|add:time_24|add:'">'|add:time_human|add:"</time>"|safe %}
Admission: {{ time }}
{% endblocktrans %}
{% endwith %}
</span>
{% else %}
<span data-time="{{ ev.date_admission.isoformat }}" data-timezone="{{ request.event.timezone }}">
{% html_time ev.date_admission "SHORT_DATETIME_FORMAT" attr_fmt="Y-m-d H:i" as datetime%}
{% blocktrans trimmed with datetime=datetime %}
Admission: {{ datetime }}
{% endblocktrans %}
{% with datetime_human=ev.date_admission|date:"SHORT_DATETIME_FORMAT" datetime_iso=ev.date_admission|time:"Y-m-d H:i" %}
{% blocktrans trimmed with datetime='<time datetime="'|add:datetime_iso|add:'">'|add:datetime_human|add:"</time>"|safe %}
Admission: {{ datetime }}
{% endblocktrans %}
{% endwith %}
</span>
{% endif %}
{% endif %}

View File

@@ -1,5 +1,4 @@
{% extends "pretixpresale/event/base.html" %}
{% load html_time %}
{% load i18n %}
{% load bootstrap3 %}
{% load eventsignal %}
@@ -93,10 +92,11 @@
A payment of {{ total }} is still pending for this order.
{% endblocktrans %}</strong>
<strong>
{% html_time order.expires "format_expires" as date %}
{% blocktrans trimmed with date=date %}
{% with date_human=order|format_expires|safe date_iso=order.expires|date:"c" %}
{% blocktrans trimmed with date='<time datetime="'|add:date_iso|add:'">'|add:date_human|add:"</time>"|safe %}
Please complete your payment before {{ date }}
{% endblocktrans %}
{% endwith %}
</strong>
</p>
{% if last_payment %}

View File

@@ -25,7 +25,6 @@
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ cart_redirect|urlencode }}">
{% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}"/>
<input type="hidden" name="_voucher_code" value="{{ voucher.code|default_if_none:"" }}">
{% if event.has_subevents %}
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request subevent=subevent voucher=voucher %}
{% else %}

View File

@@ -1,4 +1,3 @@
{% load html_time %}
{% load i18n %}
{% load date_fast %}
{% load calendarhead %}
@@ -56,7 +55,7 @@
running
{% elif event.event.presale_has_ended %}
over
{% elif event.event.settings.presale_start_show_date and event.event.effective_presale_start %}
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
soon
{% else %}
soon
@@ -109,12 +108,13 @@
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% elif event.event.presale_has_ended %}
{% trans "Sale over" %}
{% elif event.event.settings.presale_start_show_date and event.event.effective_presale_start %}
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
<span class="fa fa-ticket" aria-hidden="true"></span>
{% html_time event.event.effective_presale_start "SHORT_DATE_FORMAT" as start_date %}
{% blocktrans with start_date=start_date %}
{% with date_human=event.event.presale_start|date_fast:"SHORT_DATE_FORMAT" date_iso=event.event.presale_start|date_fast:"c" %}
{% blocktrans with start_date="<time datetime='"|add:date_iso|add:"'>"|add:date_human|add:"</time>"|safe %}
from {{ start_date }}
{% endblocktrans %}
{% endwith %}
{% else %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Soon" %}
{% endif %}

View File

@@ -1,4 +1,3 @@
{% load html_time %}
{% load i18n %}
{% load eventurl %}
<div class="day-calendar cal-size-{{ raster_to_shortest_ratio }}{% if no_headlines %} no-headlines{% endif %}"
@@ -53,7 +52,7 @@
running
{% elif event.event.presale_has_ended %}
over
{% elif event.event.settings.presale_start_show_date and event.event.effective_presale_start %}
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
soon
{% else %}
soon
@@ -115,10 +114,9 @@
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% elif event.event.presale_has_ended %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sale over" %}
{% elif event.event.settings.presale_start_show_date and event.event.effective_presale_start %}
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
<span class="fa fa-ticket" aria-hidden="true"></span>
{% html_time event.event.effective_presale_start "SHORT_DATE_FORMAT" as start_date %}
{% blocktrans with start_date=start_date %}
{% blocktrans with start_date=event.event.presale_start|date:"SHORT_DATE_FORMAT" %}
from {{ start_date }}
{% endblocktrans %}
{% else %}

View File

@@ -1,4 +1,3 @@
{% load html_time %}
{% load i18n %}
{% load icon %}
{% load textbubble %}
@@ -53,10 +52,11 @@
{% endtextbubble %}
{% if event.settings.presale_start_show_date %}
<br><span class="text-muted">
{% html_time event.event.effective_presale_start "SHORT_DATE_FORMAT" as date %}
{% blocktrans trimmed with date=date %}
{% with date_iso=event.effective_presale_start.isoformat date_human=event.effective_presale_start|date:"SHORT_DATE_FORMAT" %}
{% blocktrans trimmed with date='<time datetime="'|add:date_iso|add:'">'|add:date_human|add:"</time>"|safe %}
Sale starts {{ date }}
{% endblocktrans %}
{% endwith %}
</span>
{% endif %}
{% endif %}

View File

@@ -1,4 +1,3 @@
{% load html_time %}
{% load i18n %}
{% load date_fast %}
<div class="week-calendar">
@@ -25,7 +24,7 @@
running
{% elif event.event.presale_has_ended %}
over
{% elif event.event.settings.presale_start_show_date and event.event.effective_presale_start %}
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
soon
{% else %}
soon
@@ -78,10 +77,9 @@
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% elif event.event.presale_has_ended %}
{% trans "Sale over" %}
{% elif event.event.settings.presale_start_show_date and event.event.effective_presale_start %}
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
<span class="fa fa-ticket" aria-hidden="true"></span>
{% html_time event.event.effective_presale_start "SHORT_DATE_FORMAT" as start_date %}
{% blocktrans with start_date=start_date %}
{% blocktrans with start_date=event.event.presale_start|date_fast:"SHORT_DATE_FORMAT" %}
from {{ start_date }}
{% endblocktrans %}
{% else %}

View File

@@ -435,7 +435,7 @@ def cart_session(request):
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartApplyVoucher(EventViewMixin, CartActionMixin, AsyncAction, View):
task = apply_voucher
known_errortypes = ['CartError', 'CartPositionError']
known_errortypes = ['CartError']
def get_success_message(self, value):
return _('We applied the voucher to as many products in your cart as we could.')
@@ -513,7 +513,7 @@ class CartApplyVoucher(EventViewMixin, CartActionMixin, AsyncAction, View):
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
task = remove_cart_position
known_errortypes = ['CartError', 'CartPositionError']
known_errortypes = ['CartError']
def get_success_message(self, value):
if CartPosition.objects.filter(cart_id=get_or_create_cart_id(self.request)).exists():
@@ -542,7 +542,7 @@ class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
task = clear_cart
known_errortypes = ['CartError', 'CartPositionError']
known_errortypes = ['CartError']
def get_success_message(self, value):
create_empty_cart_id(self.request)
@@ -556,7 +556,7 @@ class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartExtendReservation(EventViewMixin, CartActionMixin, AsyncAction, View):
task = extend_cart_reservation
known_errortypes = ['CartError', 'CartPositionError']
known_errortypes = ['CartError']
def _ajax_response_data(self, value):
if isinstance(value, dict):
@@ -566,11 +566,7 @@ class CartExtendReservation(EventViewMixin, CartActionMixin, AsyncAction, View):
def get_success_message(self, value):
if value['success'] > 0:
if value.get('price_changed'):
return _('Your cart timeout was extended. Please note that some of the prices in your cart '
'changed.')
else:
return _('Your cart timeout was extended.')
return _('Your cart timeout was extended.')
def post(self, request, *args, **kwargs):
return self.do(self.request.event.id, get_or_create_cart_id(self.request), translation.get_language(),
@@ -582,7 +578,7 @@ class CartExtendReservation(EventViewMixin, CartActionMixin, AsyncAction, View):
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
task = add_items_to_cart
known_errortypes = ['CartError', 'CartPositionError']
known_errortypes = ['CartError']
def get_success_message(self, value):
return _('The products have been successfully added to your cart.')

View File

@@ -108,8 +108,6 @@ def indent(s):
def widget_css_etag(request, version, **kwargs):
if version > version_max:
return None
if version < version_min:
version = version_min
# This makes sure a new version of the theme is loaded whenever settings or the source files have changed
@@ -473,11 +471,10 @@ class WidgetAPIProductList(EventListMixin, View):
availability['color'] = 'red'
availability['text'] = gettext('Sale over')
availability['reason'] = 'over'
elif event.settings.presale_start_show_date and ev.effective_presale_start:
elif event.settings.presale_start_show_date and ev.presale_start:
availability['color'] = 'orange'
availability['text'] = gettext('from %(start_date)s') % {
'start_date': date_format(ev.effective_presale_start.astimezone(tz or event.timezone),
"SHORT_DATE_FORMAT")
'start_date': date_format(ev.presale_start.astimezone(tz or event.timezone), "SHORT_DATE_FORMAT")
}
availability['reason'] = 'soon'
else:

View File

@@ -848,9 +848,6 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
outline: 2px solid $brand-primary;
outline-offset: 2px;
}
&:not([open]) {
display: none;
}
}
.pretix-widget-frame-isloading:focus {
outline: none;

View File

@@ -1,7 +1,7 @@
[flake8]
ignore = N802,W503,E402,C901,E722,W504,E252,N812,N806,N818,E741
max-line-length = 160
exclude = data/*,migrations,.ropeproject,static,mt940.py,_static,build,make_testdata.py,*/testutils/settings.py,tests/settings.py,pretix/base/models/__init__.py,pretix/base/secretgenerators/pretix_sig1_pb2.py,.eggs/*
exclude = migrations,.ropeproject,static,mt940.py,_static,build,make_testdata.py,*/testutils/settings.py,tests/settings.py,pretix/base/models/__init__.py,pretix/base/secretgenerators/pretix_sig1_pb2.py,.eggs/*
max-complexity = 11
[isort]
@@ -13,7 +13,7 @@ extra_standard_library = typing,enum,mimetypes
multi_line_output = 5
line_length = 79
honor_noqa = true
skip_glob = data/**,make_testdata.py,wsgi.py,bootstrap,celery_app.py,pretix/settings.py,tests/settings.py,pretix/testutils/settings.py,.eggs/**
skip_glob = make_testdata.py,wsgi.py,bootstrap,celery_app.py,pretix/settings.py,tests/settings.py,pretix/testutils/settings.py,.eggs/**
[tool:pytest]
DJANGO_SETTINGS_MODULE = tests.settings

View File

@@ -745,8 +745,6 @@ def test_use_membership(event, customer, membership, requiring_ticket):
item=requiring_ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123",
used_membership=membership
)
q = event.quotas.create(size=None, name="foo")
q.items.add(requiring_ticket)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=now(),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
@@ -769,8 +767,6 @@ def test_use_membership_invalid(event, customer, membership, requiring_ticket):
membership.date_start -= timedelta(days=100)
membership.date_end -= timedelta(days=100)
membership.save()
q = event.quotas.create(size=None, name="foo")
q.items.add(requiring_ticket)
cp1 = CartPosition.objects.create(
item=requiring_ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123",
used_membership=membership

View File

@@ -42,8 +42,10 @@ from django.contrib.auth.tokens import (
)
from django.core import mail as djmail
from django.test import RequestFactory, TestCase, override_settings
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django_otp.oath import TOTP
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from webauthn.authentication.verify_authentication_response import (
VerifiedAuthentication,
@@ -492,6 +494,20 @@ class Login2FAFormTest(TestCase):
m.undo()
def test_recovery_code_valid(self):
djmail.outbox = []
d, __ = StaticDevice.objects.get_or_create(user=self.user, name='emergency')
token = d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
response = self.client.get('/control/login/2fa')
assert 'token' in response.content.decode()
response = self.client.post('/control/login/2fa', {
'token': token.token,
})
self.assertEqual(response.status_code, 302)
self.assertIn('/control/', response['Location'])
assert "recovery code" in djmail.outbox[0].body
class FakeRedis(object):
def get_redis_connection(self, connection_string):

View File

@@ -1428,27 +1428,6 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(cp2.expires, now() + self.cart_reservation_time)
self.assertEqual(cp2.max_extend, now() + 11 * self.cart_reservation_time)
def test_expired_cart_extend_price_change_note(self):
start_time = datetime.datetime(2024, 1, 1, 10, 00, 00, tzinfo=datetime.timezone.utc)
max_extend = start_time + 11 * self.cart_reservation_time
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=max_extend, max_extend=max_extend
)
cp1.update_listed_price_and_voucher()
self.ticket.default_price = Decimal("25.00")
self.ticket.save()
with freezegun.freeze_time(max_extend + timedelta(hours=1)):
response = self.client.post('/%s/%s/cart/extend' % (self.orga.slug, self.event.slug), {
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('some of the prices in your cart changed', doc.select('.alert-success')[0].text)
with scopes_disabled():
cp1.refresh_from_db()
self.assertEqual(cp1.price, Decimal("25.00"))
self.assertEqual(cp1.expires, now() + self.cart_reservation_time)
def test_expired_cart_extend_fails_partially_on_bundled(self):
start_time = datetime.datetime(2024, 1, 1, 10, 00, 00, tzinfo=datetime.timezone.utc)
max_extend = start_time + 11 * self.cart_reservation_time
@@ -2730,6 +2709,7 @@ class CartAddonTest(CartTestMixin, TestCase):
item=self.workshop1, price=Decimal('0.00'),
event=self.event, cart_id=self.session_key, addon_to=cp1
)
self.cm.extend_expired_positions()
self.cm.commit()
cp2.refresh_from_db()
assert cp2.expires > now()
@@ -2752,6 +2732,7 @@ class CartAddonTest(CartTestMixin, TestCase):
item=self.workshop1, price=Decimal('0.00'),
event=self.event, cart_id=self.session_key, addon_to=cp1
)
self.cm.extend_expired_positions()
with self.assertRaises(CartError):
self.cm.commit()
assert CartPosition.objects.count() == 0
@@ -3419,6 +3400,7 @@ class CartAddonTest(CartTestMixin, TestCase):
item=self.workshop1, price=Decimal('12.00'),
event=self.event, cart_id=self.session_key, addon_to=cp1
)
self.cm.extend_expired_positions()
self.cm.commit()
cp1.refresh_from_db()
cp2.refresh_from_db()
@@ -3426,30 +3408,16 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp2.expires > now()
assert cp2.addon_to_id == cp1.pk
@classscope(attr='orga')
def test_expand_expired_price_change(self):
cp1 = CartPosition.objects.create(
expires=now() - timedelta(minutes=10), max_extend=now() + 10 * self.cart_reservation_time,
item=self.ticket, price=Decimal('23.00'),
event=self.event, cart_id=self.session_key
)
self.ticket.default_price = Decimal("25.00")
self.ticket.save()
self.cm.commit()
cp1.refresh_from_db()
assert cp1.expires > now()
assert cp1.listed_price == Decimal("25.00")
assert cp1.price == Decimal("25.00")
@classscope(attr='orga')
def test_expand_expired_refresh_voucher(self):
v = Voucher.objects.create(item=self.ticket, value=Decimal('20.00'), event=self.event, price_mode='set',
valid_until=now() + timedelta(days=2), max_usages=1, redeemed=0)
valid_until=now() + timedelta(days=2), max_usages=999, redeemed=0)
cp1 = CartPosition.objects.create(
expires=now() - timedelta(minutes=10), max_extend=now() + 10 * self.cart_reservation_time,
item=self.ticket, price=Decimal('21.50'),
event=self.event, cart_id=self.session_key, voucher=v
)
self.cm.extend_expired_positions()
self.cm.commit()
cp1.refresh_from_db()
assert cp1.expires > now()
@@ -4112,8 +4080,6 @@ class CartBundleTest(CartTestMixin, TestCase):
@classscope(attr='orga')
def test_extend_bundled_and_addon(self):
self.trans.require_bundling = False
self.trans.save()
cp = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=21.5, expires=now() - timedelta(minutes=10), max_extend=now() + 10 * self.cart_reservation_time

View File

@@ -2669,7 +2669,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
cr1 = CartPosition.objects.create(
@@ -2839,8 +2839,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se2 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
se2 = self.event.subevents.create(name='Foo', date_from=now())
self.quota_tickets.size = 0
self.quota_tickets.subevent = se2
self.quota_tickets.save()
@@ -2880,7 +2880,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
SubEventItem.objects.create(subevent=se, item=self.ticket, price=24)
@@ -2901,7 +2901,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
SubEventItem.objects.create(subevent=se, item=self.ticket, price=24, disabled=True)
@@ -2919,7 +2919,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.workshop2)
q.variations.add(self.workshop2b)
@@ -2938,7 +2938,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
SubEventItem.objects.create(subevent=se, item=self.ticket, price=24, available_until=now() - timedelta(days=1))
@@ -2956,7 +2956,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.workshop2)
q.variations.add(self.workshop2b)
@@ -3735,8 +3735,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se2 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
se2 = self.event.subevents.create(name='Foo', date_from=now())
self.quota_tickets.size = 10
self.quota_tickets.subevent = se2
self.quota_tickets.save()
@@ -4165,7 +4165,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se = self.event.subevents.create(name='Foo', date_from=now())
self.workshopquota.size = 1
self.workshopquota.subevent = se
self.workshopquota.save()
@@ -4214,10 +4214,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.settings.display_net_prices = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), presale_start=now() + datetime.timedelta(days=1),
active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
se = self.event.subevents.create(name='Foo', date_from=now(), presale_start=now() + datetime.timedelta(days=1))
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4227,7 +4224,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'booking period for this event has not yet started.' in response.content.decode()
assert 'booking period for one of the events in your cart has not yet started.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
@@ -4236,7 +4233,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.settings.display_net_prices = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() - datetime.timedelta(days=1), active=True)
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() - datetime.timedelta(days=1))
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4246,7 +4243,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'The booking period for this event has ended.' in response.content.decode()
assert 'booking period for one of the events in your cart has ended.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
@@ -4256,9 +4253,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.settings.display_net_prices = True
self.event.save()
self.event.settings.payment_term_last = 'RELDATE/1/23:59:59/date_from/'
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
se = self.event.subevents.create(name='Foo', date_from=now())
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4268,7 +4263,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'All payments for this event need to be confirmed already, so no new orders can be created.' in response.content.decode()
assert 'booking period for one of the events in your cart has ended.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
@@ -4277,10 +4272,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.date_to = now() - datetime.timedelta(days=1)
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() + datetime.timedelta(days=1),
active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() + datetime.timedelta(days=1))
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4291,25 +4283,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select(".thank-you")), 1)
def test_confirm_subevent_disabled(self):
with scopes_disabled():
self.event.has_subevents = True
self.event.settings.display_net_prices = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), active=False)
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
)
self._set_payment()
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'selected event date is not active.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
def test_before_presale_timemachine(self):
self._login_with_permission(self.orga)
self._enable_test_mode()
@@ -4524,8 +4497,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.assertEqual(Order.objects.first().locale, 'de')
def test_variation_require_approval(self):
self.workshop2.category = None
self.workshop2.save()
self.workshop2a.require_approval = True
self.workshop2a.save()
with scopes_disabled():
@@ -4547,7 +4518,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
def test_item_with_variations_require_approval(self):
self.workshop2.require_approval = True
self.workshop2.category = None
self.workshop2.save()
with scopes_disabled():
cr1 = CartPosition.objects.create(