diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 2e7a154b89..afc6cde066 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -31,6 +31,7 @@ # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. +import re import uuid from collections import Counter, defaultdict, namedtuple from datetime import datetime, time, timedelta @@ -38,6 +39,7 @@ from decimal import Decimal from typing import List, Optional from celery.exceptions import MaxRetriesExceededError +from django import forms from django.core.exceptions import ValidationError from django.db import DatabaseError, transaction from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value @@ -135,6 +137,7 @@ error_messages = { '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.'), + 'price_not_a_number': gettext_lazy('The entered price is not a number.'), 'price_too_high': gettext_lazy('The entered price is to high.'), 'voucher_invalid': gettext_lazy('This voucher code is not known in our database.'), 'voucher_min_usages': gettext_lazy( @@ -725,9 +728,18 @@ class CartManager: price_after_voucher = listed_price custom_price = None if item.free_price and i.get('price'): - custom_price = Decimal(str(i.get('price')).replace(",", ".")) + custom_price = re.sub('[^0-9.,]', '', str(i.get('price'))) + if not custom_price: + raise CartError(error_messages['price_not_a_number']) + try: + custom_price = forms.DecimalField(localize=True).to_python(custom_price) + except: + try: + custom_price = Decimal(custom_price) + except: + raise CartError(error_messages['price_not_a_number']) if custom_price > 99_999_999_999: - raise ValueError('price_too_high') + raise CartError(error_messages['price_too_high']) op = self.AddOperation( count=i['count'], @@ -840,9 +852,18 @@ class CartManager: listed_price = get_listed_price(item, variation, cp.subevent) custom_price = None if item.free_price and a.get('price'): - custom_price = Decimal(str(a.get('price')).replace(",", ".")) + custom_price = re.sub('[^0-9.,]', '', a.get('price')) + if not custom_price: + raise CartError(error_messages['price_not_a_number']) + try: + custom_price = forms.DecimalField(localize=True).to_python(custom_price) + except: + try: + custom_price = Decimal(custom_price) + except: + raise CartError(error_messages['price_not_a_number']) if custom_price > 99_999_999_999: - raise ValueError('price_too_high') + raise CartError(error_messages['price_too_high']) # Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky) for ca in current_addons[cp][a['item'], a['variation']]: diff --git a/src/pretix/base/services/pricing.py b/src/pretix/base/services/pricing.py index f1f242d150..d335827135 100644 --- a/src/pretix/base/services/pricing.py +++ b/src/pretix/base/services/pricing.py @@ -19,9 +19,11 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import re from decimal import Decimal from typing import List, Optional, Tuple +from django import forms from django.db.models import Q from django.utils.timezone import now @@ -69,7 +71,16 @@ def get_price(item: Item, variation: ItemVariation = None, subtract_from_gross=bundled_sum) elif item.free_price and custom_price is not None and custom_price != "": if not isinstance(custom_price, Decimal): - custom_price = Decimal(str(custom_price).replace(",", ".")) + custom_price = re.sub('[^0-9.,]', '', str(custom_price)) + if not custom_price: + raise ValueError('price_not_a_number') + try: + custom_price = forms.DecimalField(localize=True).to_python(custom_price) + except: + try: + custom_price = Decimal(custom_price) + except: + raise ValueError('price_not_a_number') if custom_price > 99_999_999_999: raise ValueError('price_too_high') diff --git a/src/tests/base/test_pricing.py b/src/tests/base/test_pricing.py index fc6593befa..92491a83ff 100644 --- a/src/tests/base/test_pricing.py +++ b/src/tests/base/test_pricing.py @@ -23,6 +23,7 @@ import json from decimal import Decimal import pytest +from django.utils import translation from django.utils.timezone import now from django_countries.fields import Country @@ -196,7 +197,8 @@ def test_free_price_accepted(item): @pytest.mark.django_db def test_free_price_string(item): item.free_price = True - assert get_price(item, custom_price='42,00').gross == Decimal('42.00') + with translation.override('de'): + assert get_price(item, custom_price='42,00').gross == Decimal('42.00') @pytest.mark.django_db @@ -209,7 +211,7 @@ def test_free_price_float(item): def test_free_price_limit(item): item.free_price = True with pytest.raises(ValueError): - get_price(item, custom_price=Decimal('200000000')) + get_price(item, custom_price=Decimal('200000000000')) @pytest.mark.django_db diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 7b043d897e..dab0d8d420 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -635,6 +635,21 @@ class CartTest(CartTestMixin, TestCase): self.assertIsNone(objs[0].variation) self.assertEqual(objs[0].price, 24) + def test_free_price_numeric(self): + self.ticket.free_price = True + self.ticket.save() + response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'item_%d' % self.ticket.id: '1', + 'price_%d' % self.ticket.id: 'abcde' + }, follow=True) + self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug), + target_status_code=200) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertIn('not a number', doc.select('#error-message')[0].text) + with scopes_disabled(): + objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) + self.assertEqual(len(objs), 0) + def test_free_price_only_if_allowed(self): self.ticket.free_price = False self.ticket.save() diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 551eb1d327..1730c0b8f1 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -3138,6 +3138,9 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): assert cp1.addons.last().item == self.workshop1 def test_set_addon_free_price(self): + self.event.settings.locales = ['de'] + self.event.settings.locale = 'de' + with scopes_disabled(): self.workshop1.free_price = True self.workshop1.save()