diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 2876a2f6a2..eb8e47526f 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -21,6 +21,7 @@ from pretix.base.reldate import RelativeDateField, RelativeDateWrapper from pretix.base.settings import SettingsSandbox from pretix.base.signals import register_payment_providers from pretix.presale.views import get_cart_total +from pretix.presale.views.cart import get_or_create_cart_id logger = logging.getLogger(__name__) @@ -275,7 +276,7 @@ class BasePaymentProvider: The default implementation checks for the _availability_date setting to be either unset or in the future. """ - return self._is_still_available(cart_id=request.session.session_key) + return self._is_still_available(cart_id=get_or_create_cart_id(request)) def payment_form_render(self, request: HttpRequest) -> str: """ diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index b4cdb2f671..77389ba4f7 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -25,6 +25,9 @@ from pretix.presale.signals import ( ) from pretix.presale.views import CartMixin, get_cart, get_cart_total from pretix.presale.views.async import AsyncAction +from pretix.presale.views.cart import ( + cart_session, create_empty_cart_id, get_or_create_cart_id, +) from pretix.presale.views.questions import QuestionsViewMixin @@ -82,9 +85,13 @@ class BaseCheckoutFlowStep: if n: return n.get_step_url() + @cached_property + def cart_session(self): + return cart_session(self.request) + @cached_property def invoice_address(self): - iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk)) + iapk = self.cart_session.get('invoice_address') if not iapk: return InvoiceAddress() @@ -257,7 +264,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep): if not is_valid: return self.get(request, *args, **kwargs) - return self.do(self.request.event.id, data, self.request.session.session_key, + return self.do(self.request.event.id, data, get_or_create_cart_id(self.request), invoice_address=self.invoice_address.pk) @@ -272,9 +279,9 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): @cached_property def contact_form(self): initial = { - 'email': self.request.session.get('email', '') + 'email': self.cart_session.get('email', '') } - initial.update(self.request.session.get('contact_form_data', {})) + initial.update(self.cart_session.get('contact_form_data', {})) return ContactForm(data=self.request.POST if self.request.method == "POST" else None, event=self.request.event, initial=initial) @@ -301,15 +308,15 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): messages.error(request, _("We had difficulties processing your input. Please review the errors below.")) return self.render() - request.session['email'] = self.contact_form.cleaned_data['email'] + self.cart_session['email'] = self.contact_form.cleaned_data['email'] if request.event.settings.invoice_address_asked: addr = self.invoice_form.save() - request.session['invoice_address_{}'.format(request.event.pk)] = addr.pk - request.session['contact_form_data'] = self.contact_form.cleaned_data + self.cart_session['invoice_address'] = addr.pk + self.cart_session['contact_form_data'] = self.contact_form.cleaned_data update_tax_rates( event=request.event, - cart_id=request.session.session_key, + cart_id=get_or_create_cart_id(request), invoice_address=self.invoice_form.instance ) @@ -319,11 +326,11 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): self.request = request try: emailval = EmailValidator() - if 'email' not in request.session: + if 'email' not in self.cart_session: if warn: messages.warning(request, _('Please enter a valid email address.')) return False - emailval(request.session.get('email')) + emailval(self.cart_session.get('email')) except ValidationError: if warn: messages.warning(request, _('Please enter a valid email address.')) @@ -404,7 +411,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): self.request = request for p in self.provider_forms: if p['provider'].identifier == request.POST.get('payment', ''): - request.session['payment'] = p['provider'].identifier + self.cart_session['payment'] = p['provider'].identifier resp = p['provider'].checkout_prepare( request, self.get_cart() @@ -422,16 +429,16 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): ctx = super().get_context_data(**kwargs) ctx['providers'] = self.provider_forms ctx['show_fees'] = any(p['fee'] for p in self.provider_forms) - ctx['selected'] = self.request.POST.get('payment', self.request.session.get('payment', '')) + ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', '')) return ctx @cached_property def payment_provider(self): - return self.request.event.get_payment_providers().get(self.request.session['payment']) + return self.request.event.get_payment_providers().get(self.cart_session['payment']) def is_completed(self, request, warn=False): self.request = request - if 'payment' not in request.session or not self.payment_provider: + if 'payment' not in self.cart_session or not self.payment_provider: if warn: messages.error(request, _('The payment information you entered was incomplete.')) return False @@ -446,7 +453,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): def is_applicable(self, request): self.request = request if self._total_order_value == 0: - request.session['payment'] = 'free' + self.cart_session['payment'] = 'free' return False return True @@ -476,7 +483,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): responses = contact_form_fields.send(self.event) for r, response in sorted(responses, key=lambda r: str(r[0])): for key, value in response.items(): - v = self.request.session.get('contact_form_data', {}).get(key) + v = self.cart_session.get('contact_form_data', {}).get(key) if v is True: v = _('Yes') elif v is False: @@ -495,7 +502,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): @cached_property def payment_provider(self): - return self.request.event.get_payment_providers().get(self.request.session['payment']) + return self.request.event.get_payment_providers().get(self.cart_session['payment']) def get(self, request): self.request = request @@ -520,16 +527,17 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): return redirect(self.get_error_url()) meta_info = { - 'contact_form_data': self.request.session.get('contact_form_data', {}) + 'contact_form_data': self.cart_session.get('contact_form_data', {}) } for receiver, response in order_meta_from_request.send(sender=request.event, request=request): meta_info.update(response) return self.do(self.request.event.id, self.payment_provider.identifier, - [p.id for p in self.positions], request.session.get('email'), + [p.id for p in self.positions], self.cart_session.get('email'), translation.get_language(), self.invoice_address.pk, meta_info) def get_success_message(self, value): + create_empty_cart_id(self.request) return None def get_success_url(self, value): diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index 5261894b69..57a3b2a18d 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -19,6 +19,11 @@ class CartMixin: """ return list(get_cart(self.request)) + @cached_property + def cart_session(self): + from pretix.presale.views.cart import cart_session + return cart_session(self.request) + def get_cart(self, answers=False, queryset=None, order=None, downloads=False): if queryset: prefetch = [] @@ -102,7 +107,7 @@ class CartMixin: if order: fees = order.fees.all() else: - iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk)) + iapk = self.cart_session.get('invoice_address') ia = None if iapk: try: @@ -110,7 +115,7 @@ class CartMixin: except InvoiceAddress.DoesNotExist: pass - fees = get_fees(self.request.event, self.request, total, ia, self.request.session.get('payment')) + fees = get_fees(self.request.event, self.request, total, ia, self.cart_session.get('payment')) total += sum([f.value for f in fees]) net_total += sum([f.net_value for f in fees]) @@ -137,9 +142,11 @@ class CartMixin: def get_cart(request): + from pretix.presale.views.cart import get_or_create_cart_id + if not hasattr(request, '_cart_cache'): request._cart_cache = CartPosition.objects.filter( - cart_id=request.session.session_key, event=request.event + cart_id=get_or_create_cart_id(request), event=request.event ).order_by( 'item', 'variation' ).select_related( @@ -152,12 +159,14 @@ def get_cart(request): def get_cart_total(request): + from pretix.presale.views.cart import get_or_create_cart_id + if not hasattr(request, '_cart_total_cache'): if hasattr(request, '_cart_cache'): request._cart_total_cache = sum(i.price for i in request._cart_cache) else: request._cart_total_cache = CartPosition.objects.filter( - cart_id=request.session.session_key, event=request.event + cart_id=get_or_create_cart_id(request), event=request.event ).aggregate(sum=Sum('price'))['sum'] or 0 return request._cart_total_cache diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index c947a3005b..07bafe8ec0 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -6,6 +6,7 @@ from django.db.models import Count, Prefetch, Q from django.http import FileResponse, Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.utils import translation +from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext as _ @@ -39,9 +40,13 @@ class CartActionMixin: def get_error_url(self): return self.get_next_url() + @cached_property + def cart_session(self): + return cart_session(self.request) + @cached_property def invoice_address(self): - iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk)) + iapk = self.cart_session.get('invoice_address') if not iapk: return InvoiceAddress() @@ -124,19 +129,49 @@ class CartActionMixin: return items +def create_empty_cart_id(request): + current_id = request.session.get('current_cart_event_{}'.format(request.event.pk)) + if current_id and current_id in request.session.get('carts', {}): + del request.session['carts'][current_id] + del request.session['current_cart_event_{}'.format(request.event.pk)] + return get_or_create_cart_id(request) + + +def get_or_create_cart_id(request): + current_id = request.session.get('current_cart_event_{}'.format(request.event.pk)) + if current_id and current_id in request.session.get('carts', {}): + return current_id + else: + while True: + new_id = get_random_string(length=32) + if not CartPosition.objects.filter(cart_id=new_id).exists(): + break + + if 'carts' not in request.session: + request.session['carts'] = {} + request.session['carts'][new_id] = {} + request.session['current_cart_event_{}'.format(request.event.pk)] = new_id + return new_id + + +def cart_session(request): + request.session.modified = True + return request.session['carts'][get_or_create_cart_id(request)] + + class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View): task = remove_cart_position known_errortypes = ['CartError'] def get_success_message(self, value): - if CartPosition.objects.filter(cart_id=self.request.session.session_key).exists(): + if CartPosition.objects.filter(cart_id=get_or_create_cart_id(self.request)).exists(): return _('Your cart has been updated.') else: return _('Your cart is now empty.') def post(self, request, *args, **kwargs): if 'id' in request.POST: - return self.do(self.request.event.id, request.POST.get('id'), self.request.session.session_key, translation.get_language()) + return self.do(self.request.event.id, request.POST.get('id'), get_or_create_cart_id(self.request), translation.get_language()) else: if 'ajax' in self.request.GET or 'ajax' in self.request.POST: return JsonResponse({ @@ -154,7 +189,7 @@ class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View): return _('Your cart is now empty.') def post(self, request, *args, **kwargs): - return self.do(self.request.event.id, self.request.session.session_key, translation.get_language()) + return self.do(self.request.event.id, get_or_create_cart_id(self.request), translation.get_language()) class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View): @@ -167,7 +202,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View): def post(self, request, *args, **kwargs): items = self._items_from_post_data() if items: - return self.do(self.request.event.id, items, self.request.session.session_key, translation.get_language(), + return self.do(self.request.event.id, items, get_or_create_cart_id(self.request), translation.get_language(), self.invoice_address.pk) else: if 'ajax' in self.request.GET or 'ajax' in self.request.POST: @@ -299,7 +334,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView): redeemed_in_carts = CartPosition.objects.filter( Q(voucher=self.voucher) & Q(event=request.event) & - (Q(expires__gte=now()) | Q(cart_id=request.session.session_key)) + (Q(expires__gte=now()) | Q(cart_id=get_or_create_cart_id(request))) ) v_avail = self.voucher.max_usages - self.voucher.redeemed - redeemed_in_carts.count() if v_avail < 1: @@ -337,7 +372,7 @@ class AnswerDownload(EventViewMixin, View): answid = kwargs.get('answer') answer = get_object_or_404( QuestionAnswer, - cartposition__cart_id=self.request.session.session_key, + cartposition__cart_id=get_or_create_cart_id(self.request), id=answid ) if not answer.file: diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py index 18ed1757d8..d6443ec898 100644 --- a/src/pretix/presale/views/checkout.py +++ b/src/pretix/presale/views/checkout.py @@ -9,13 +9,15 @@ from pretix.base.services.cart import CartError from pretix.base.signals import validate_cart from pretix.multidomain.urlreverse import eventreverse from pretix.presale.checkoutflow import get_checkout_flow +from pretix.presale.views.cart import get_or_create_cart_id class CheckoutView(View): def dispatch(self, request, *args, **kwargs): + self.request = request cart_pos = CartPosition.objects.filter( - cart_id=self.request.session.session_key, event=self.request.event + cart_id=get_or_create_cart_id(request), event=self.request.event ) if not cart_pos.exists() and "async_id" not in request.GET: diff --git a/src/pretix/testutils/sessions.py b/src/pretix/testutils/sessions.py new file mode 100644 index 0000000000..0448d5a70d --- /dev/null +++ b/src/pretix/testutils/sessions.py @@ -0,0 +1,20 @@ +from django.utils.crypto import get_random_string + + +def add_cart_session(client, event, data): + new_id = get_random_string(length=32) + session = client.session + session['current_cart_event_{}'.format(event.pk)] = new_id + if 'carts' not in session: + session['carts'] = {} + session['carts'][new_id] = data + session.save() + return new_id + + +def get_cart_session_key(client, event): + cart_id = client.session.get('current_cart_event_{}'.format(event.pk)) + if cart_id: + return cart_id + else: + return add_cart_session(client, event, {}) diff --git a/src/tests/plugins/paypal/test_checkout.py b/src/tests/plugins/paypal/test_checkout.py index 53c9923a41..21472c0e74 100644 --- a/src/tests/plugins/paypal/test_checkout.py +++ b/src/tests/plugins/paypal/test_checkout.py @@ -1,12 +1,12 @@ import datetime import pytest -from django.conf import settings from django.utils.timezone import now from pretix.base.models import ( CartPosition, Event, Item, ItemCategory, Organizer, Quota, ) +from pretix.testutils.sessions import add_cart_session, get_cart_session_key @pytest.fixture @@ -29,7 +29,7 @@ def env(client): event.settings.set('payment_paypal_endpoint', 'sandbox') event.settings.set('payment_paypal_client_id', '12345') event.settings.set('payment_paypal_secret', '12345') - client.session.email = 'admin@localhost' + add_cart_session(client, event, {'email': 'admin@localhost'}) return client, ticket @@ -44,7 +44,7 @@ def test_payment(env, monkeypatch): monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal._create_payment", create_payment) client, ticket = env - session_key = client.cookies.get(settings.SESSION_COOKIE_NAME).value + session_key = get_cart_session_key(client, ticket.event) CartPosition.objects.create( event=ticket.event, cart_id=session_key, item=ticket, price=23, expires=now() + datetime.timedelta(minutes=10) diff --git a/src/tests/plugins/stripe/test_checkout.py b/src/tests/plugins/stripe/test_checkout.py index bd3cc0e5f5..8aaf6f6e97 100644 --- a/src/tests/plugins/stripe/test_checkout.py +++ b/src/tests/plugins/stripe/test_checkout.py @@ -1,12 +1,12 @@ import datetime import pytest -from django.conf import settings from django.utils.timezone import now from pretix.base.models import ( CartPosition, Event, Item, ItemCategory, Organizer, Quota, ) +from pretix.testutils.sessions import add_cart_session, get_cart_session_key class MockedCharge(): @@ -35,7 +35,7 @@ def env(client): quota_tickets.items.add(ticket) event.settings.set('attendee_names_asked', False) event.settings.set('payment_stripe__enabled', True) - client.session.email = 'admin@localhost' + add_cart_session(client, event, {'email': 'admin@localhost'}) return client, ticket @@ -53,7 +53,7 @@ def test_payment(env, monkeypatch): monkeypatch.setattr("stripe.Charge.create", charge_create) client, ticket = env - session_key = client.cookies.get(settings.SESSION_COOKIE_NAME).value + session_key = get_cart_session_key(client, ticket.event) CartPosition.objects.create( event=ticket.event, cart_id=session_key, item=ticket, price=13.37, expires=now() + datetime.timedelta(minutes=10) diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 8a3e90ed5c..91f3824a31 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -3,7 +3,6 @@ from datetime import timedelta from decimal import Decimal from bs4 import BeautifulSoup -from django.conf import settings from django.test import TestCase from django.utils.timezone import now from django_countries.fields import Country @@ -17,6 +16,7 @@ from pretix.base.models.items import ( ItemAddOn, SubEventItem, SubEventItemVariation, ) from pretix.base.services.cart import CartError, CartManager +from pretix.testutils.sessions import get_cart_session_key class CartTestMixin: @@ -51,7 +51,7 @@ class CartTestMixin: self.quota_all.variations.add(self.shirt_red) self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)) - self.session_key = self.client.cookies.get(settings.SESSION_COOKIE_NAME).value + self.session_key = get_cart_session_key(self.client, self.event) class CartTest(CartTestMixin, TestCase): @@ -97,7 +97,7 @@ class CartTest(CartTestMixin, TestCase): def _set_session(self, key, value): session = self.client.session - session[key] = value + session['carts'][get_cart_session_key(self.client, self.event)][key] = value session.save() def _enable_reverse_charge(self): @@ -108,7 +108,7 @@ class CartTest(CartTestMixin, TestCase): is_business=True, vat_id='ATU1234567', vat_id_validated=True, country=Country('AT') ) - self._set_session('invoice_address_{}'.format(self.event.pk), ia.pk) + self._set_session('invoice_address', ia.pk) return ia def test_reverse_charge(self): diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index e8ccb58177..af37d8929b 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -17,6 +17,7 @@ from pretix.base.models import ( OrderPosition, Organizer, Question, Quota, Voucher, ) from pretix.base.models.items import ItemAddOn, ItemVariation, SubEventItem +from pretix.testutils.sessions import get_cart_session_key class CheckoutTestCase(TestCase): @@ -40,7 +41,7 @@ class CheckoutTestCase(TestCase): self.event.settings.set('payment_banktransfer__enabled', True) self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)) - self.session_key = self.client.cookies.get(settings.SESSION_COOKIE_NAME).value + self.session_key = get_cart_session_key(self.client, self.event) self._set_session('email', 'admin@localhost') self.workshopcat = ItemCategory.objects.create(name="Workshops", is_addon=True, event=self.event) @@ -64,7 +65,7 @@ class CheckoutTestCase(TestCase): is_business=True, vat_id='ATU1234567', vat_id_validated=True, country=Country('AT') ) - self._set_session('invoice_address_{}'.format(self.event.pk), ia.pk) + self._set_session('invoice_address', ia.pk) return ia def test_empty_cart(self): @@ -156,7 +157,7 @@ class CheckoutTestCase(TestCase): cr1.refresh_from_db() assert cr1.price == round_decimal(Decimal('23.00') / Decimal('1.19')) - ia = InvoiceAddress.objects.get(pk=self.client.session.get('invoice_address_{}'.format(self.event.pk))) + ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address')) assert ia.vat_id_validated def test_reverse_charge_enable_then_disable(self): @@ -178,7 +179,7 @@ class CheckoutTestCase(TestCase): cr = CartPosition.objects.get(cart_id=self.session_key) assert cr.price == Decimal('23.00') - ia = InvoiceAddress.objects.get(pk=self.client.session.get('invoice_address_{}'.format(self.event.pk))) + ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address')) assert not ia.vat_id_validated def test_reverse_charge_invalid_vatid(self): @@ -242,7 +243,7 @@ class CheckoutTestCase(TestCase): cr1.refresh_from_db() assert cr1.price == round_decimal(Decimal('23.00') / Decimal('1.19')) - ia = InvoiceAddress.objects.get(pk=self.client.session.get('invoice_address_{}'.format(self.event.pk))) + ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address')) assert not ia.vat_id_validated def test_reverse_charge_vatid_same_country(self): @@ -273,7 +274,7 @@ class CheckoutTestCase(TestCase): cr1.refresh_from_db() assert cr1.price == Decimal('23.00') - ia = InvoiceAddress.objects.get(pk=self.client.session.get('invoice_address_{}'.format(self.event.pk))) + ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address')) assert ia.vat_id_validated def test_reverse_charge_vatid_check_invalid_country(self): @@ -337,7 +338,7 @@ class CheckoutTestCase(TestCase): cr1.refresh_from_db() assert cr1.price == Decimal('23.00') - ia = InvoiceAddress.objects.get(pk=self.client.session.get('invoice_address_{}'.format(self.event.pk))) + ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address')) assert not ia.vat_id_validated def test_question_file_upload(self): @@ -525,7 +526,7 @@ class CheckoutTestCase(TestCase): def _set_session(self, key, value): session = self.client.session - session[key] = value + session['carts'][get_cart_session_key(self.client, self.event)][key] = value session.save() def test_subevent(self):