Clean up localization or error messages in cart (#3049)

This commit is contained in:
Richard Schreiber
2023-01-30 17:24:09 +01:00
committed by GitHub
parent 59f409b1c6
commit a7f9e100d2
5 changed files with 245 additions and 186 deletions

View File

@@ -43,7 +43,9 @@ from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _, pgettext_lazy from django.utils.translation import (
gettext as _, gettext_lazy, ngettext_lazy, pgettext_lazy,
)
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
@@ -81,74 +83,119 @@ class CartError(Exception):
if msgargs: if msgargs:
msg = _(msg) % msgargs msg = _(msg) % msgargs
else: else:
msg = _(msg) # force msg to string to make sure lazy-translation is done in current locale-context
# otherwise translation might happen in celery-context, which uses default-locale
# also translate with _/gettext to keep it backwards compatible
msg = _(str(msg))
super().__init__(msg) super().__init__(msg)
error_messages = { error_messages = {
'busy': _('We were not able to process your request completely as the ' 'busy': gettext_lazy(
'server was too busy. Please try again.'), 'We were not able to process your request completely as the '
'empty': _('You did not select any products.'), 'server was too busy. Please try again.'
'unknown_position': _('Unknown cart position.'), ),
'empty': gettext_lazy('You did not select any products.'),
'unknown_position': gettext_lazy('Unknown cart position.'),
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'), 'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
'not_for_sale': _('You selected a product which is not available for sale.'), 'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
'unavailable': _('Some of the products you selected are no longer available. ' 'unavailable': gettext_lazy(
'Please see below for details.'), 'Some of the products you selected are no longer available. '
'in_part': _('Some of the products you selected are no longer available in ' 'Please see below for details.'
'the quantity you selected. Please see below for details.'), ),
'max_items': _("You cannot select more than %s items per order."), 'in_part': gettext_lazy(
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s."), 'Some of the products you selected are no longer available in '
'min_items_per_product': _("You need to select at least %(min)s items of the product %(product)s."), 'the quantity you selected. Please see below for details.'
'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than " ),
"%(min)s items of it."), 'max_items': ngettext_lazy(
'not_started': _('The booking period for this event has not yet started.'), "You cannot select more than %s item per order.",
'ended': _('The booking period for this event has ended.'), "You cannot select more than %s items per order."
'payment_ended': _('All payments for this event need to be confirmed already, so no new orders can be created.'), ),
'some_subevent_not_started': _('The booking period for this event has not yet started. The affected positions ' 'max_items_per_product': ngettext_lazy(
'have been removed from your cart.'), "You cannot select more than %(max)s item of the product %(product)s.",
'some_subevent_ended': _('The booking period for one of the events in your cart has ended. The affected ' "You cannot select more than %(max)s items of the product %(product)s.",
'positions have been removed from your cart.'), "max"
'price_too_high': _('The entered price is to high.'), ),
'voucher_invalid': _('This voucher code is not known in our database.'), 'min_items_per_product': ngettext_lazy(
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s ' "You need to select at least %(min)s item of the product %(product)s.",
'matching products.'), "You need to select at least %(min)s items of the product %(product)s.",
'voucher_min_usages_removed': _('The voucher code "%(voucher)s" can only be used if you select at least ' "min"
'%(number)s matching products. We have therefore removed some positions from ' ),
'your cart that can no longer be purchased like this.'), 'min_items_per_product_removed': ngettext_lazy(
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'), "We removed %(product)s from your cart as you can not buy less than %(min)s item of it.",
'voucher_redeemed_cart': _('This voucher code is currently locked since it is already contained in a cart. This ' "We removed %(product)s from your cart as you can not buy less than %(min)s items of it.",
'might mean that someone else is redeeming this voucher right now, or that you tried ' "min"
'to redeem it before but did not complete the checkout process. You can try to use it ' ),
'again in %d minutes.'), 'not_started': gettext_lazy('The booking period for this event has not yet started.'),
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'), 'ended': gettext_lazy('The booking period for this event has ended.'),
'voucher_double': _('You already used this voucher code. Remove the associated line from your ' 'payment_ended': gettext_lazy('All payments for this event need to be confirmed already, so no new orders can be created.'),
'cart if you want to use it for a different product.'), 'some_subevent_not_started': gettext_lazy(
'voucher_expired': _('This voucher is expired.'), 'The booking period for this event has not yet started. The affected positions '
'voucher_invalid_item': _('This voucher is not valid for this product.'), 'have been removed from your cart.'),
'voucher_invalid_seat': _('This voucher is not valid for this seat.'), 'some_subevent_ended': gettext_lazy(
'voucher_no_match': _('We did not find any position in your cart that we could use this voucher for. If you want ' 'The booking period for one of the events in your cart has ended. The affected '
'to add something new to your cart using that voucher, you can do so with the voucher ' 'positions have been removed from your cart.'),
'redemption option on the bottom of the page.'), 'price_too_high': gettext_lazy('The entered price is to high.'),
'voucher_item_not_available': _( 'voucher_invalid': gettext_lazy('This voucher code is not known in our database.'),
'voucher_min_usages': gettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'
),
'voucher_min_usages_removed': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
'We have therefore removed some positions from your cart that can no longer be purchased like this.',
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
'We have therefore removed some positions from your cart that can no longer be purchased like this.',
'number'
),
'voucher_redeemed': gettext_lazy('This voucher code has already been used the maximum number of times allowed.'),
'voucher_redeemed_cart': gettext_lazy(
'This voucher code is currently locked since it is already contained in a cart. This '
'might mean that someone else is redeeming this voucher right now, or that you tried '
'to redeem it before but did not complete the checkout process. You can try to use it '
'again in %d minutes.'
),
'voucher_redeemed_partial': gettext_lazy('This voucher code can only be redeemed %d more times.'),
'voucher_whole_cart_not_combined': gettext_lazy('Applying a voucher to the whole cart should not be combined with other operations.'),
'voucher_double': gettext_lazy(
'You already used this voucher code. Remove the associated line from your '
'cart if you want to use it for a different product.'
),
'voucher_expired': gettext_lazy('This voucher is expired.'),
'voucher_invalid_item': gettext_lazy('This voucher is not valid for this product.'),
'voucher_invalid_seat': gettext_lazy('This voucher is not valid for this seat.'),
'voucher_no_match': gettext_lazy(
'We did not find any position in your cart that we could use this voucher for. If you want '
'to add something new to your cart using that voucher, you can do so with the voucher '
'redemption option on the bottom of the page.'
),
'voucher_item_not_available': gettext_lazy(
'Your voucher is valid for a product that is currently not for sale.'), 'Your voucher is valid for a product that is currently not for sale.'),
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'), 'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
'voucher_required': _('You need a valid voucher code to order this product.'), 'voucher_required': gettext_lazy('You need a valid voucher code to order this product.'),
'inactive_subevent': pgettext_lazy('subevent', 'The selected event date is not active.'), 'inactive_subevent': pgettext_lazy('subevent', 'The selected event date is not active.'),
'addon_invalid_base': _('You can not select an add-on for the selected product.'), 'addon_invalid_base': gettext_lazy('You can not select an add-on for the selected product.'),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'), 'addon_duplicate_item': gettext_lazy('You can not select two variations of the same add-on product.'),
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'), 'addon_max_count': ngettext_lazy(
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the ' 'You can select at most %(max)s add-on from the category %(cat)s for the product %(base)s.',
'product %(base)s.'), 'You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.',
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'), 'max'
'addon_only': _('One of the products you selected can only be bought as an add-on to another product.'), ),
'bundled_only': _('One of the products you selected can only be bought part of a bundle.'), 'addon_min_count': ngettext_lazy(
'seat_required': _('You need to select a specific seat.'), 'You need to select at least %(min)s add-on from the category %(cat)s for the product %(base)s.',
'seat_invalid': _('Please select a valid seat.'), 'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'seat_forbidden': _('You can not select a seat for this position.'), 'min'
'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'), ),
'seat_multiple': _('You can not select the same seat multiple times.'), 'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."), 'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'),
'country_blocked': _('One of the selected products is not available in the selected country.'), 'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'),
'seat_required': gettext_lazy('You need to select a specific seat.'),
'seat_invalid': gettext_lazy('Please select a valid seat.'),
'seat_forbidden': gettext_lazy('You can not select a seat for this position.'),
'seat_unavailable': gettext_lazy('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': gettext_lazy('You can not select the same seat multiple times.'),
'gift_card': gettext_lazy("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
} }
@@ -320,8 +367,7 @@ class CartManager:
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
not op.position.addon_to_id]) not op.position.addon_to_id])
if cartsize > int(self.event.settings.max_items_per_order): if cartsize > int(self.event.settings.max_items_per_order):
# TODO: i18n plurals raise CartError(error_messages['max_items'] % self.event.settings.max_items_per_order)
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
def _check_item_constraints(self, op, current_ops=[]): def _check_item_constraints(self, op, current_ops=[]):
if isinstance(op, (self.AddOperation, self.ExtendOperation)): if isinstance(op, (self.AddOperation, self.ExtendOperation)):
@@ -494,7 +540,7 @@ class CartManager:
def apply_voucher(self, voucher_code: str): def apply_voucher(self, voucher_code: str):
if self._operations: if self._operations:
raise CartError('Applying a voucher to the whole cart should not be combined with other operations.') raise CartError(error_messages['voucher_whole_cart_not_combined'])
try: try:
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip()) voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
except Voucher.DoesNotExist: except Voucher.DoesNotExist:
@@ -539,7 +585,7 @@ class CartManager:
for voucher, cnt in list(voucher_use_diff.items()): for voucher, cnt in list(voucher_use_diff.items()):
if 0 < cnt < voucher.min_usages_remaining: if 0 < cnt < voucher.min_usages_remaining:
raise CartError( raise CartError(
_(error_messages['voucher_min_usages']) % { error_messages['voucher_min_usages'] % {
'voucher': voucher.code, 'voucher': voucher.code,
'number': voucher.min_usages_remaining, 'number': voucher.min_usages_remaining,
} }
@@ -843,22 +889,16 @@ class CartManager:
for (i, v), c in selected.items(): for (i, v), c in selected.items():
n_per_i[i] += c n_per_i[i] += c
if sum(selected.values()) > iao.max_count: if sum(selected.values()) > iao.max_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise CartError( raise CartError(
error_messages['addon_max_count'], error_messages['addon_max_count'] % {
{
'base': str(item.name), 'base': str(item.name),
'max': iao.max_count, 'max': iao.max_count,
'cat': str(iao.addon_category.name), 'cat': str(iao.addon_category.name),
} }
) )
elif sum(selected.values()) < iao.min_count: elif sum(selected.values()) < iao.min_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise CartError( raise CartError(
error_messages['addon_min_count'], error_messages['addon_min_count'] % {
{
'base': str(item.name), 'base': str(item.name),
'min': iao.min_count, 'min': iao.min_count,
'cat': str(iao.addon_category.name), 'cat': str(iao.addon_category.name),
@@ -866,8 +906,7 @@ class CartManager:
) )
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed: elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise CartError( raise CartError(
error_messages['addon_no_multi'], error_messages['addon_no_multi'] % {
{
'base': str(item.name), 'base': str(item.name),
'cat': str(iao.addon_category.name), 'cat': str(iao.addon_category.name),
} }
@@ -931,7 +970,7 @@ class CartManager:
if item.max_per_order and count > item.max_per_order: if item.max_per_order and count > item.max_per_order:
raise CartError( raise CartError(
_(error_messages['max_items_per_product']) % { error_messages['max_items_per_product'] % {
'max': item.max_per_order, 'max': item.max_per_order,
'product': item.name 'product': item.name
} }
@@ -945,13 +984,13 @@ class CartManager:
for p in self.positions: for p in self.positions:
if p.item_id == item.pk and p.pk not in removals: if p.item_id == item.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p)) self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['min_items_per_product_removed']) % { err = error_messages['min_items_per_product_removed'] % {
'min': item.min_per_order, 'min': item.min_per_order,
'product': item.name 'product': item.name
} }
if not err: if not err:
raise CartError( raise CartError(
_(error_messages['min_items_per_product']) % { error_messages['min_items_per_product'] % {
'min': item.min_per_order, 'min': item.min_per_order,
'product': item.name 'product': item.name
} }
@@ -980,13 +1019,13 @@ class CartManager:
for p in self.positions: for p in self.positions:
if p.voucher_id == voucher.pk and p.pk not in removals: if p.voucher_id == voucher.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p)) self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['voucher_min_usages_removed']) % { err = error_messages['voucher_min_usages_removed'] % {
'voucher': voucher.code, 'voucher': voucher.code,
'number': voucher.min_usages_remaining, 'number': voucher.min_usages_remaining,
} }
if not err: if not err:
raise CartError( raise CartError(
_(error_messages['voucher_min_usages']) % { error_messages['voucher_min_usages'] % {
'voucher': voucher.code, 'voucher': voucher.code,
'number': voucher.min_usages_remaining, 'number': voucher.min_usages_remaining,
} }

View File

@@ -54,15 +54,13 @@ from django.db.transaction import get_connection
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication from pretix.api.models import OAuthApplication
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import ( from pretix.base.i18n import get_language_without_region, language
LazyLocaleException, get_language_without_region, language,
)
from pretix.base.models import ( from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership, CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User, Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
@@ -101,47 +99,94 @@ from pretix.helpers import OF_SELF
from pretix.helpers.models import modelcopy from pretix.helpers.models import modelcopy
from pretix.helpers.periodic import minimum_interval from pretix.helpers.periodic import minimum_interval
class OrderError(Exception):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
# force msg to string to make sure lazy-translation is done in current locale-context
# otherwise translation might happen in celery-context, which uses default-locale
# also translate with _/gettext to keep it backwards compatible
msg = _(str(msg))
super().__init__(msg)
error_messages = { error_messages = {
'unavailable': _('Some of the products you selected were no longer available. ' 'unavailable': gettext_lazy(
'Please see below for details.'), 'Some of the products you selected were no longer available. '
'in_part': _('Some of the products you selected were no longer available in ' 'Please see below for details.'
'the quantity you selected. Please see below for details.'), ),
'price_changed': _('The price of some of the items in your cart has changed in the ' 'in_part': gettext_lazy(
'meantime. Please see below for details.'), 'Some of the products you selected were no longer available in '
'internal': _("An internal error occurred, please try again."), 'the quantity you selected. Please see below for details.'
'empty': _("Your cart is empty."), ),
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s. We removed the " 'price_changed': gettext_lazy(
"surplus items from your cart."), 'The price of some of the items in your cart has changed in the '
'busy': _('We were not able to process your request completely as the ' 'meantime. Please see below for details.'
'server was too busy. Please try again.'), ),
'not_started': _('The booking period for this event has not yet started.'), 'internal': gettext_lazy("An internal error occurred, please try again."),
'ended': _('The booking period has ended.'), 'empty': gettext_lazy("Your cart is empty."),
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s ' 'max_items_per_product': ngettext_lazy(
'matching products.'), "You cannot select more than %(max)s item of the product %(product)s. We removed the surplus items from your cart.",
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'), "You cannot select more than %(max)s items of the product %(product)s. We removed the surplus items from your cart.",
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum ' "max"
'number of times allowed. We removed this item from your cart.'), ),
'voucher_budget_used': _('The voucher code used for one of the items in your cart has already been too often. We ' 'busy': gettext_lazy(
'adjusted the price of the item in your cart.'), 'We were not able to process your request completely as the '
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item ' 'server was too busy. Please try again.'
'from your cart.'), ),
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We ' 'not_started': gettext_lazy('The booking period for this event has not yet started.'),
'removed this item from your cart.'), 'ended': gettext_lazy('The booking period has ended.'),
'voucher_required': _('You need a valid voucher code to order one of the products.'), 'voucher_min_usages': ngettext_lazy(
'some_subevent_not_started': _('The booking period for one of the events in your cart has not yet started. The ' 'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'affected positions have been removed from your cart.'), 'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'some_subevent_ended': _('The booking period for one of the events in your cart has ended. The affected ' 'number'
'positions have been removed from your cart.'), ),
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'), 'voucher_invalid': gettext_lazy('The voucher code used for one of the items in your cart is not known in our database.'),
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'), 'voucher_redeemed': gettext_lazy(
'country_blocked': _('One of the selected products is not available in the selected country.'), 'The voucher code used for one of the items in your cart has already been used the maximum '
'not_for_sale': _('You selected a product which is not available for sale.'), 'number of times allowed. We removed this item from your cart.'
'addon_invalid_base': _('You can not select an add-on for the selected product.'), ),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'), 'voucher_budget_used': gettext_lazy(
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'), 'The voucher code used for one of the items in your cart has already been too often. We '
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the ' 'adjusted the price of the item in your cart.'
'product %(base)s.'), ),
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'), 'voucher_expired': gettext_lazy(
'The voucher code used for one of the items in your cart is expired. We removed this item from your cart.'
),
'voucher_invalid_item': gettext_lazy(
'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.'),
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
'addon_invalid_base': gettext_lazy('You can not select an add-on for the selected product.'),
'addon_duplicate_item': gettext_lazy('You can not select two variations of the same add-on product.'),
'addon_max_count': ngettext_lazy(
'You can select at most %(max)s add-on from the category %(cat)s for the product %(base)s.',
'You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.',
'max'
),
'addon_min_count': ngettext_lazy(
'You need to select at least %(min)s add-on from the category %(cat)s for the product %(base)s.',
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
} }
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -157,7 +202,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
enough quota. enough quota.
""" """
if order.status != Order.STATUS_CANCELED: if order.status != Order.STATUS_CANCELED:
raise OrderError('The order was not canceled.') raise OrderError(_('The order was not canceled.'))
with order.event.lock() as now_dt: with order.event.lock() as now_dt:
is_available = order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True, is_available = order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True,
@@ -538,18 +583,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
return order.pk return order.pk
class OrderError(LazyLocaleException):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def _check_date(event: Event, now_dt: datetime): def _check_date(event: Event, now_dt: datetime):
if event.presale_start and now_dt < event.presale_start: if event.presale_start and now_dt < event.presale_start:
raise OrderError(error_messages['not_started']) raise OrderError(error_messages['not_started'])
@@ -570,7 +603,6 @@ def _check_date(event: Event, now_dt: datetime):
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None, def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None,
sales_channel='web', customer=None): sales_channel='web', customer=None):
err = None err = None
errargs = None
_check_date(event, now_dt) _check_date(event, now_dt)
products_seen = Counter() products_seen = Counter()
@@ -607,9 +639,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
products_seen[cp.item] += 1 products_seen[cp.item] += 1
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order: if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
err = error_messages['max_items_per_product'] err = error_messages['max_items_per_product'] % {
errargs = {'max': cp.item.max_per_order, 'max': cp.item.max_per_order,
'product': cp.item.name} 'product': cp.item.name
}
delete(cp) delete(cp)
break break
@@ -728,7 +761,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
for voucher, cnt in v_usages.items(): for voucher, cnt in v_usages.items():
if 0 < cnt < voucher.min_usages_remaining: if 0 < cnt < voucher.min_usages_remaining:
raise OrderError(error_messages['voucher_min_usages'], { raise OrderError(error_messages['voucher_min_usages'] % {
'voucher': voucher.code, 'voucher': voucher.code,
'number': voucher.min_usages_remaining, 'number': voucher.min_usages_remaining,
}) })
@@ -791,7 +824,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
cp.save() cp.save()
if err: if err:
raise OrderError(err, errargs) raise OrderError(err)
def _get_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress, def _get_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
@@ -1344,22 +1377,26 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
class OrderChangeManager: class OrderChangeManager:
error_messages = { error_messages = {
'product_without_variation': _('You need to select a variation of the product.'), 'product_without_variation': gettext_lazy('You need to select a variation of the product.'),
'quota': _('The quota {name} does not have enough capacity left to perform the operation.'), 'quota': gettext_lazy('The quota {name} does not have enough capacity left to perform the operation.'),
'quota_missing': _('There is no quota defined that allows this operation.'), 'quota_missing': gettext_lazy('There is no quota defined that allows this operation.'),
'product_invalid': _('The selected product is not active or has no price set.'), 'product_invalid': gettext_lazy('The selected product is not active or has no price set.'),
'complete_cancel': _('This operation would leave the order empty. Please cancel the order itself instead.'), 'complete_cancel': gettext_lazy('This operation would leave the order empty. Please cancel the order itself instead.'),
'paid_to_free_exceeded': _('This operation would make the order free and therefore immediately paid, however ' 'paid_to_free_exceeded': gettext_lazy(
'no quota is available.'), 'This operation would make the order free and therefore immediately paid, however '
'addon_to_required': _('This is an add-on product, please select the base position it should be added to.'), 'no quota is available.'
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'), ),
'subevent_required': _('You need to choose a subevent for the new position.'), 'addon_to_required': gettext_lazy('This is an add-on product, please select the base position it should be added to.'),
'seat_unavailable': _('The selected seat "{seat}" is not available.'), 'addon_invalid': gettext_lazy('The selected base position does not allow you to add this product as an add-on.'),
'seat_subevent_mismatch': _('You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'), 'subevent_required': gettext_lazy('You need to choose a subevent for the new position.'),
'seat_required': _('The selected product requires you to select a seat.'), 'seat_unavailable': gettext_lazy('The selected seat "{seat}" is not available.'),
'seat_forbidden': _('The selected product does not allow to select a seat.'), 'seat_subevent_mismatch': gettext_lazy(
'tax_rule_country_blocked': _('The selected country is blocked by your tax rule.'), 'You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'
'gift_card_change': _('You cannot change the price of a position that has been used to issue a gift card.'), ),
'seat_required': gettext_lazy('The selected product requires you to select a seat.'),
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
} }
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation')) ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent')) SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
@@ -1766,22 +1803,16 @@ class OrderChangeManager:
for (i, v), c in selected.items(): for (i, v), c in selected.items():
n_per_i[i] += c n_per_i[i] += c
if sum(selected.values()) > iao.max_count: if sum(selected.values()) > iao.max_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError( raise OrderError(
error_messages['addon_max_count'], error_messages['addon_max_count'] % {
{
'base': str(item.name), 'base': str(item.name),
'max': iao.max_count, 'max': iao.max_count,
'cat': str(iao.addon_category.name), 'cat': str(iao.addon_category.name),
} }
) )
elif sum(selected.values()) < iao.min_count: elif sum(selected.values()) < iao.min_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError( raise OrderError(
error_messages['addon_min_count'], error_messages['addon_min_count'] % {
{
'base': str(item.name), 'base': str(item.name),
'min': iao.min_count, 'min': iao.min_count,
'cat': str(iao.addon_category.name), 'cat': str(iao.addon_category.name),
@@ -1789,8 +1820,7 @@ class OrderChangeManager:
) )
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed: elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise OrderError( raise OrderError(
error_messages['addon_no_multi'], error_messages['addon_no_multi'] % {
{
'base': str(item.name), 'base': str(item.name),
'cat': str(iao.addon_category.name), 'cat': str(iao.addon_category.name),
} }
@@ -2471,7 +2501,7 @@ def perform_order(self, event: Event, payments: List[dict], positions: List[str]
except LockTimeoutException: except LockTimeoutException:
self.retry() self.retry()
except (MaxRetriesExceededError, LockTimeoutException): except (MaxRetriesExceededError, LockTimeoutException):
raise OrderError(str(error_messages['busy'])) raise OrderError(error_messages['busy'])
_unset = object() _unset = object()

View File

@@ -525,7 +525,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
return JsonResponse({ return JsonResponse({
'redirect': self.get_error_url(), 'redirect': self.get_error_url(),
'success': False, 'success': False,
'message': _(error_messages['empty']) 'message': str(error_messages['empty'])
}) })
else: else:
return redirect(self.get_error_url()) return redirect(self.get_error_url())
@@ -597,8 +597,6 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
return context return context
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
from pretix.base.services.cart import error_messages
err = None err = None
v = request.GET.get('voucher') v = request.GET.get('voucher')
@@ -652,7 +650,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
pass pass
if err: if err:
messages.error(request, _(err)) messages.error(request, str(err))
return redirect_to_url(self.get_next_url() + "?voucher_invalid") return redirect_to_url(self.get_next_url() + "?voucher_invalid")
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)

View File

@@ -1389,22 +1389,16 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
selected[i, None] = val, price selected[i, None] = val, price
if sum(a[0] for a in selected.values()) > category['max_count']: if sum(a[0] for a in selected.values()) > category['max_count']:
# TODO: Proper pluralization
raise ValidationError( raise ValidationError(
_(error_messages['addon_max_count']), error_messages['addon_max_count'] % {
'addon_max_count',
{
'base': str(form['pos'].item.name), 'base': str(form['pos'].item.name),
'max': category['max_count'], 'max': category['max_count'],
'cat': str(category['category'].name), 'cat': str(category['category'].name),
} }
) )
elif sum(a[0] for a in selected.values()) < category['min_count']: elif sum(a[0] for a in selected.values()) < category['min_count']:
# TODO: Proper pluralization
raise ValidationError( raise ValidationError(
_(error_messages['addon_min_count']), error_messages['addon_min_count'] % {
'addon_min_count',
{
'base': str(form['pos'].item.name), 'base': str(form['pos'].item.name),
'min': category['min_count'], 'min': category['min_count'],
'cat': str(category['category'].name), 'cat': str(category['category'].name),
@@ -1412,9 +1406,7 @@ class OrderChange(EventViewMixin, OrderDetailMixin, TemplateView):
) )
elif any(sum(v[0] for k, v in selected.items() if k[0] == i) > 1 for i in category['items']) and not category['multi_allowed']: elif any(sum(v[0] for k, v in selected.items() if k[0] == i) > 1 for i in category['items']) and not category['multi_allowed']:
raise ValidationError( raise ValidationError(
_(error_messages['addon_no_multi']), error_messages['addon_no_multi'] % {
'addon_no_multi',
{
'base': str(form['pos'].item.name), 'base': str(form['pos'].item.name),
'cat': str(category['category'].name), 'cat': str(category['category'].name),
} }

View File

@@ -1529,7 +1529,7 @@ class CartTest(CartTestMixin, TestCase):
response = self.client.get('/%s/%s/redeem' % (self.orga.slug, self.event.slug), response = self.client.get('/%s/%s/redeem' % (self.orga.slug, self.event.slug),
{'voucher': v.code}, {'voucher': v.code},
follow=True) follow=True)
assert error_messages['voucher_item_not_available'] in response.rendered_content assert str(error_messages['voucher_item_not_available']) in response.rendered_content
def test_voucher_price(self): def test_voucher_price(self):
with scopes_disabled(): with scopes_disabled():