diff --git a/src/make_testdata.py b/src/make_testdata.py index d7faa545b..bf997683a 100644 --- a/src/make_testdata.py +++ b/src/make_testdata.py @@ -8,7 +8,7 @@ import django django.setup() -from pretix.base.models import * +from pretix.base.models import * # NOQA if Organizer.objects.exists(): print("There already is data in your DB!") diff --git a/src/pretix/base/forms/user.py b/src/pretix/base/forms/user.py index dcaaad2e0..5eba17b7b 100644 --- a/src/pretix/base/forms/user.py +++ b/src/pretix/base/forms/user.py @@ -2,7 +2,7 @@ from django import forms from django.contrib.auth.hashers import check_password from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from pytz import common_timezones +# from pytz import common_timezones from pretix.base.models import User diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index ca7818c29..e081e4c47 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -9,7 +9,6 @@ from django.forms import Form from django.http import HttpRequest from django.template.loader import get_template from django.utils.translation import ugettext_lazy as _ -from pretix.base.forms import SettingsForm from pretix.base.models import Order, CartPosition from pretix.base.services.orders import mark_order_paid diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index b586f0232..1bf1685ed 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -65,7 +65,7 @@ class OrderError(Exception): pass -def perform_order(event, user, payment_provider, positions): +def check_positions(event, dt, positions, quotas_locked): error_messages = { 'unavailable': _('Some of the products you selected were no longer available. ' 'Please see below for details.'), @@ -73,71 +73,78 @@ def perform_order(event, user, payment_provider, positions): '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 ' 'meantime. Please see below for details.'), + 'max_items': _("You cannot select more than %s items per order"), + } + err = None + + for i, cp in enumerate(positions): + quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all()) + if cp.expires >= dt: + # Other checks are not necessary + continue + price = cp.item.check_restrictions() if cp.variation is None else cp.variation.check_restrictions() + if price is False or len(quotas) == 0: + err = err or error_messages['unavailable'] + continue + if price != cp.price: + cp = cp.clone() + positions[i] = cp + cp.price = price + cp.save() + err = err or error_messages['price_changed'] + continue + quota_ok = True + for quota in quotas: + # Lock the quota, so no other thread is allowed to perform sales covered by this + # quota while we're doing so. + if quota not in quotas_locked: + quota.lock() + quotas_locked.add(quota) + avail = quota.availability() + if avail[0] != Quota.AVAILABILITY_OK: + # This quota is sold out/currently unavailable, so do not sell this at all + err = err or error_messages['unavailable'] + quota_ok = False + break + if quota_ok and not event.presale_end or now() < event.presale_end: + cp = cp.clone() + positions[i] = cp + cp.expires = now() + timedelta( + minutes=event.settings.get('reservation_time', as_type=int)) + cp.save() + elif not quota_ok: + cp.delete() # Sorry! + if err: + raise OrderError(err) + + +def perform_order(event, user, payment_provider, positions): + error_messages = { 'busy': _('We were not able to process your request completely as the ' 'server was too busy. Please try again.'), - 'max_items': _("You cannot select more than %s items per order"), } dt = now() quotas_locked = set() - err = None try: - for i, cp in enumerate(positions): - quotas = list(cp.item.quotas.all()) if cp.variation is None else list(cp.variation.quotas.all()) - if cp.expires < dt: - price = cp.item.check_restrictions() if cp.variation is None else cp.variation.check_restrictions() - if price is False or len(quotas) == 0: - err = err or error_messages['unavailable'] - continue - if price != cp.price: - cp = cp.clone() - positions[i] = cp - cp.price = price - cp.save() - err = err or error_messages['price_changed'] - continue - quota_ok = True - for quota in quotas: - # Lock the quota, so no other thread is allowed to perform sales covered by this - # quota while we're doing so. - if quota not in quotas_locked: - quota.lock() - quotas_locked.add(quota) - avail = quota.availability() - if avail[0] != Quota.AVAILABILITY_OK: - # This quota is sold out/currently unavailable, so do not sell this at all - err = err or error_messages['unavailable'] - quota_ok = False - break - if quota_ok: - if not event.presale_end or now() < event.presale_end: - cp = cp.clone() - positions[i] = cp - cp.expires = now() + timedelta( - minutes=event.settings.get('reservation_time', as_type=int)) - cp.save() - else: - cp.delete() # Sorry! - if err: - raise OrderError(err) - else: # Everything went well - order = place_order(event, user, positions, dt, payment_provider) - mail( - user, _('Your order: %(code)s') % {'code': order.code}, - 'pretixpresale/email/order_placed.txt', - { - 'user': user, 'order': order, - 'event': event, - 'url': build_absolute_uri('presale:event.order', kwargs={ - 'event': event.slug, - 'organizer': event.organizer.slug, - 'order': order.code, - }), - 'payment': payment_provider.order_pending_mail_render(order) - }, - event - ) - return order + check_positions(event, dt, positions, quotas_locked) + order = place_order(event, user, positions, dt, payment_provider) + mail( + user, _('Your order: %(code)s') % {'code': order.code}, + 'pretixpresale/email/order_placed.txt', + { + 'user': user, 'order': order, + 'event': event, + 'url': build_absolute_uri('presale:event.order', kwargs={ + 'event': event.slug, + 'organizer': event.organizer.slug, + 'order': order.code, + }), + 'payment': payment_provider.order_pending_mail_render(order) + }, + event + ) + return order except Quota.LockTimeoutException: # Is raised when there are too many threads asking for quota locks and we were # unaible to get one @@ -165,6 +172,6 @@ def place_order(event, user, positions, dt, payment_provider): total=total, payment_fee=payment_fee, payment_provider=payment_provider.identifier, - ) + ) OrderPosition.transform_cart_positions(positions, order) return order diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index b2713f134..be463e614 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -3,7 +3,7 @@ from django import forms from django.contrib import messages from django.db.models import Sum -from django.forms import inlineformset_factory, formset_factory, modelformset_factory, BaseInlineFormSet +from django.forms import modelformset_factory from django.shortcuts import render, redirect from django.utils.functional import cached_property from django.views.generic import FormView diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index e5b3f892d..34e961e4a 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -62,56 +62,60 @@ class ImportView(EventPermissionRequiredMixin, TemplateView): def process_mt940(self): return self.confirm_view(mt940import.parse(self.request.FILES.get('file'))) + def process_csv_file(self): + try: + data = csvimport.get_rows_from_file(self.request.FILES['file']) + except csv.Error as e: # TODO: narrow down + logger.error('Import failed: ' + str(e)) + messages.error(self.request, _('I\'m sorry, but we were unable to import this CSV file. Please ' + 'contact support for help.')) + return self.redirect_back() + + if len(data) == 0: + messages.error(self.request, _('I\'m sorry, but we detected this file as empty. Please ' + 'contact support for help.')) + + if self.request.event.settings.get('banktransfer_csvhint') is not None: + hint = self.request.event.settings.get('banktransfer_csvhint', as_type=dict) + try: + parsed = csvimport.parse(data, hint) + except csvimport.HintMismatchError as e: # TODO: narrow down + logger.error('Import using stored hint failed: ' + str(e)) + else: + return self.confirm_view(parsed) + + return self.assign_view(data) + + def process_csv_hint(self): + data = [] + for i in range(int(self.request.POST.get('rows'))): + data.append([ + self.request.POST.get('col[%d][%d]' % (i, j)) + for j in range(int(self.request.POST.get('cols'))) + ]) + if 'reference' not in self.request.POST: + messages.error(self.request, _('You need to select the column containing the payment reference.')) + return self.assign_view(data) + try: + hint = csvimport.new_hint(self.request.POST) + except Exception as e: + logger.error('Parsing hint failed: ' + str(e)) + messages.error(self.request, _('We were unable to process your input.')) + return self.assign_view(data) + try: + self.request.event.settings.set('banktransfer_csvhint', hint) + except Exception as e: # TODO: narrow down + logger.error('Import using stored hint failed: ' + str(e)) + pass + else: + parsed = csvimport.parse(data, hint) + return self.confirm_view(parsed) + def process_csv(self): if 'file' in self.request.FILES: - # if file is csv file - try: - data = csvimport.get_rows_from_file(self.request.FILES['file']) - except csv.Error as e: # TODO: narrow down - logger.error('Import failed: ' + str(e)) - messages.error(self.request, _('I\'m sorry, but we were unable to import this CSV file. Please ' - 'contact support for help.')) - return self.redirect_back() - - if len(data) == 0: - messages.error(self.request, _('I\'m sorry, but we detected this file as empty. Please ' - 'contact support for help.')) - - if self.request.event.settings.get('banktransfer_csvhint') is not None: - hint = self.request.event.settings.get('banktransfer_csvhint', as_type=dict) - try: - parsed = csvimport.parse(data, hint) - except csvimport.HintMismatchError as e: # TODO: narrow down - logger.error('Import using stored hint failed: ' + str(e)) - else: - return self.confirm_view(parsed) - - return self.assign_view(data) - - elif 'amount' in self.request.POST: # CSV hint given - data = [] - for i in range(int(self.request.POST.get('rows'))): - data.append([ - self.request.POST.get('col[%d][%d]' % (i, j)) - for j in range(int(self.request.POST.get('cols'))) - ]) - if 'reference' not in self.request.POST: - messages.error(self.request, _('You need to select the column containing the payment reference.')) - return self.assign_view(data) - try: - hint = csvimport.new_hint(self.request.POST) - except Exception as e: - logger.error('Parsing hint failed: ' + str(e)) - messages.error(self.request, _('We were unable to process your input.')) - return self.assign_view(data) - try: - self.request.event.settings.set('banktransfer_csvhint', hint) - except Exception as e: # TODO: narrow down - logger.error('Import using stored hint failed: ' + str(e)) - pass - else: - parsed = csvimport.parse(data, hint) - return self.confirm_view(parsed) + return self.process_csv_file() + elif 'amount' in self.request.POST: + return self.process_csv_hint() return super().get(self.request) def confirm_view(self, parsed): diff --git a/src/pretix/plugins/paypal/views.py b/src/pretix/plugins/paypal/views.py index 273987b83..80cf422a2 100644 --- a/src/pretix/plugins/paypal/views.py +++ b/src/pretix/plugins/paypal/views.py @@ -1,7 +1,6 @@ import logging from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.core.urlresolvers import reverse from django.shortcuts import redirect import paypalrestsdk from pretix.base.models import Event, Order diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 7369eb69e..06a36515d 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -108,8 +108,11 @@ class CartAdd(EventViewMixin, CartActionMixin, View): self.items = self._items_from_post_data() + if not self.items: + return redirect(self.get_failure_url()) + # We do not use EventLoginRequiredMixin here, as we want to store stuff into the - # session beforehand + # session before redirecting to login if not request.user.is_authenticated() or \ (request.user.event is not None and request.user.event != request.event): request.session['cart_tmp'] = json.dumps(self.items) @@ -119,6 +122,13 @@ class CartAdd(EventViewMixin, CartActionMixin, View): 'event': request.event.slug, }), 'next' ) + + existing = CartPosition.objects.current.filter(user=self.request.user, event=self.request.event).count() + if sum(i[2] for i in self.items) + existing > int(self.request.event.settings.max_items_per_order): + # TODO: i18n plurals + self.error_message(self.error_messages['max_items'] % self.request.event.settings.max_items_per_order) + return redirect(self.get_failure_url()) + return self.process() def error_message(self, msg, important=False): @@ -129,7 +139,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): def _re_add_position(self, position): self.items.insert(0, (position.item_id, position.variation_id, 1, position)) - def _expired_positions(self): + def _re_add_expired_positions(self): positions = set() # For items that are already expired, we have to delete and re-add them, as they might # be no longer available or prices might have changed. Sorry! @@ -140,25 +150,24 @@ class CartAdd(EventViewMixin, CartActionMixin, View): positions.add(cp) return positions - def process(self): + def _extend_existing(self, expiry): # Extend this user's cart session to 30 minutes from now to ensure all items in the # cart expire at the same time # We can extend the reservation of items which are not yet expired without risk - expiry = now() + timedelta(minutes=self.request.event.settings.get('reservation_time', as_type=int)) CartPosition.objects.current.filter( Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__gt=now()) ).update(expires=expiry) - _expired = self._expired_positions() + def _delete_expired(self, expired): + for cp in expired: + if cp.version_end_date is None: + cp.delete() - if not self.items: - return redirect(self.get_failure_url()) + def process(self): + expiry = now() + timedelta(minutes=self.request.event.settings.get('reservation_time', as_type=int)) + self._extend_existing(expiry) - existing = CartPosition.objects.current.filter(user=self.request.user, event=self.request.event).count() - if sum(i[2] for i in self.items) + existing > int(self.request.event.settings.max_items_per_order): - # TODO: i18n plurals - self.error_message(self.error_messages['max_items'] % self.request.event.settings.max_items_per_order) - return redirect(self.get_failure_url()) + _expired = self._re_add_expired_positions() # Fetch items from the database items_cache = { @@ -244,9 +253,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): for quota in quotas: quota.release() - for cp in _expired: - if cp.version_end_date is None: - cp.delete() + self._delete_expired(_expired) if not self.msg_some_unavailable: messages.success(self.request, _('The products have been successfully added to your cart.')) diff --git a/src/tests/control/test_user.py b/src/tests/control/test_user.py index 33ea31d7a..9c46429bd 100644 --- a/src/tests/control/test_user.py +++ b/src/tests/control/test_user.py @@ -1,4 +1,3 @@ -from django.utils.timezone import now from pretix.base.models import User from tests.base import BrowserTest diff --git a/src/tests/presale/test_account.py b/src/tests/presale/test_account.py index 85612cec5..eb84d6890 100644 --- a/src/tests/presale/test_account.py +++ b/src/tests/presale/test_account.py @@ -1,4 +1,3 @@ -import datetime import time from pretix.base.models import User from tests.base import BrowserTest diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py index d02281bb5..b4392af6c 100644 --- a/src/tests/presale/test_orders.py +++ b/src/tests/presale/test_orders.py @@ -268,7 +268,7 @@ class OrdersTest(TestCase): self.order.save() response = self.client.get( '/%s/%s/order/%s/download/testdummy' % (self.orga.slug, self.event.slug, self.order.code), - ) + ) assert response.status_code == 200 assert response.content.strip().decode() == self.order.identity diff --git a/src/tests/settings.py b/src/tests/settings.py index dbb063ad7..9181c0646 100644 --- a/src/tests/settings.py +++ b/src/tests/settings.py @@ -1,4 +1,4 @@ -from pretix.settings import * +from pretix.settings import * # NOQA TEST_DIR = os.path.dirname(__file__) diff --git a/src/tests/testdummy/__init__.py b/src/tests/testdummy/__init__.py index 466f761ae..f7826d906 100644 --- a/src/tests/testdummy/__init__.py +++ b/src/tests/testdummy/__init__.py @@ -12,7 +12,7 @@ class TestDummyApp(AppConfig): version = '1.0.0' def ready(self): - from tests.testdummy import signals + from tests.testdummy import signals # noqa default_app_config = 'tests.testdummy.TestDummyApp'