From cd1ae33bec1ee7d20acc45db5e52f52c81e62f4c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 12 Sep 2019 15:29:08 -0400 Subject: [PATCH 1/4] Update awscli from 1.16.237 to 1.16.238 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 01b58ef8..6377e8b5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ pycodestyle==2.5.0 coveralls==1.8.2 responses==0.10.6 six==1.12.0 -awscli==1.16.237 +awscli==1.16.238 From baed44932fccb3fcd1cf57d419edbc2c0d782eb8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 13 Sep 2019 12:08:44 -0400 Subject: [PATCH 2/4] Update factory-boy from 2.11.1 to 2.12.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0c33f6b7..2c396bdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ djangorestframework==3.10.3 django-cors-headers==3.1.0 django-filter==2.2.0 coreapi==2.3.3 -factory-boy==2.11.1 +factory-boy==2.12.0 Markdown==2.6.11 Pygments==2.3.1 django-anymail==6.1.0 From eaf5fa2e16f78d23eee8bed2cfec7a7cf1dae6d8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 13 Sep 2019 12:08:52 -0400 Subject: [PATCH 3/4] Update pygments from 2.3.1 to 2.4.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0c33f6b7..a4f46d83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ django-filter==2.2.0 coreapi==2.3.3 factory-boy==2.11.1 Markdown==2.6.11 -Pygments==2.3.1 +Pygments==2.4.2 django-anymail==6.1.0 Pillow==6.1.0 django-simple-history==2.7.3 From e5eb58b84bd8cae27c4cd5fe1010bbebd7680294 Mon Sep 17 00:00:00 2001 From: Jerome Celle Date: Sat, 14 Sep 2019 16:44:37 +0200 Subject: [PATCH 4/4] Add rollback for coupon validate call --- store/tests/tests_viewset_Order.py | 18 +++++ store/views.py | 118 +++++++++++++++++------------ 2 files changed, 88 insertions(+), 48 deletions(-) diff --git a/store/tests/tests_viewset_Order.py b/store/tests/tests_viewset_Order.py index 9b791e56..4a35c640 100644 --- a/store/tests/tests_viewset_Order.py +++ b/store/tests/tests_viewset_Order.py @@ -184,6 +184,17 @@ def setUp(self): reserved_seats=1, has_shared_rooms=True, ) + + self.option_retreat: OptionProduct = OptionProduct.objects.create( + name="Vegan", + details="Vegan details", + available=True, + price=50, + max_quantity=10 + ) + self.option_retreat.available_on_products.add(self.retreat) + self.option_retreat.save() + self.retreat_no_seats = Retreat.objects.create( name="no_place_left_retreat", seats=0, @@ -2489,10 +2500,17 @@ def test_validate_coupon(self): 'content_type': 'timeslot', 'object_id': self.time_slot.id, 'quantity': 1, + 'options': [] }, { 'content_type': 'retreat', 'object_id': self.retreat.id, 'quantity': 1, + 'options': [ + { + 'id': self.option_retreat.id, + 'quantity': 2 + } + ] }], 'coupon': "ABCD1234", } diff --git a/store/views.py b/store/views.py index 9a8b262b..192e2fa8 100644 --- a/store/views.py +++ b/store/views.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.db import transaction from django.http import Http404, HttpResponse, HttpRequest from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -20,7 +21,7 @@ from .exceptions import PaymentAPIError from .models import (Package, Membership, Order, OrderLine, PaymentProfile, CustomPayment, Coupon, CouponUser, Refund, BaseProduct, - OptionProduct) + OptionProduct, OrderLineBaseProduct) from .permissions import IsOwner from .resources import (MembershipResource, PackageResource, OrderResource, OrderLineResource, CustomPaymentResource, @@ -259,6 +260,7 @@ class OrderViewSet(ExportMixin, viewsets.ModelViewSet): export_resource = OrderResource() + @transaction.atomic @action( methods=['post'], detail=False, permission_classes=[IsAuthenticated]) def validate_coupon(self, request, pk=None): @@ -269,57 +271,77 @@ def validate_coupon(self, request, pk=None): This is not the best way to do it and it could be improved. """ - serializer = serializers.OrderSerializer( - data=request.data, - context={'request': request} - ) - serializer.is_valid(raise_exception=True) - # Coupon is necessary for this view - if not serializer.validated_data.get('coupon'): - error = { - 'coupon': [_("This field is required.")] - } - return Response(error, status=status.HTTP_400_BAD_REQUEST) - orderlines = serializer.validated_data.pop('order_lines', None) - coupon = serializer.validated_data.pop('coupon', None) - serializer.validated_data.pop('payment_token', None) - serializer.validated_data.pop('single_use_token', None) - order = Order( - **serializer.validated_data, - transaction_date=timezone.now(), - user=request.user, - ) - order.save() - - orderline_list = [] - for idx, orderline in enumerate(orderlines): - orderline_list.append( - OrderLine( - **orderline, - order=order, - ) + try: + sid = transaction.savepoint() + serializer = serializers.OrderSerializer( + data=request.data, + context={'request': request} ) - orderline_list[idx].save() - - response = validate_coupon_for_order(coupon, order) - response['orderline'] = serializers.OrderLineSerializerNoOrder( - response['orderline'], - context={'request': request} - ).data - response['orderline'].pop('url', None) - response['orderline'].pop('id', None) - response['orderline'].pop('order', None) - response['orderline'].pop('coupon', None) - response['orderline'].pop('coupon_real_value', None) - response['orderline'].pop('cost', None) - order.delete() - for orderline in orderline_list: - orderline.delete() - if response['valid_use']: + serializer.is_valid(raise_exception=True) + # Coupon is necessary for this view + if not serializer.validated_data.get('coupon'): + error = { + 'coupon': [_("This field is required.")] + } + return Response(error, status=status.HTTP_400_BAD_REQUEST) + orderlines = serializer.validated_data.pop('order_lines', None) + coupon = serializer.validated_data.pop('coupon', None) + serializer.validated_data.pop('payment_token', None) + serializer.validated_data.pop('single_use_token', None) + order = Order( + **serializer.validated_data, + transaction_date=timezone.now(), + user=request.user, + ) + order.save() + + orderline_list = [] + for idx, orderline in enumerate(orderlines): + if 'options' in orderline.keys(): + options = orderline.pop('options') + orderline_list.append( + OrderLine( + **orderline, + order=order, + ) + ) + + orderline_list[idx].save() + + if 'options' in orderline.keys(): + for option in options: + OrderLineBaseProduct.objects.create( + order_line=orderline_list[idx], + option_id=option.get('id'), + quantity=option.get('quantity'), + ) + orderline_list[idx].save() + + response = validate_coupon_for_order(coupon, order) + response['orderline'] = serializers.OrderLineSerializerNoOrder( + response['orderline'], + context={'request': request} + ).data + + except Exception as err: + raise err + finally: + # add rollback in finally, so it always rollback the data + transaction.savepoint_rollback(sid) + + if response.get('valid_use', False): + response['orderline'].pop('url', None) + response['orderline'].pop('id', None) + response['orderline'].pop('order', None) + response['orderline'].pop('coupon', None) + response['orderline'].pop('coupon_real_value', None) + response['orderline'].pop('cost', None) response.pop('valid_use', None) response.pop('error', None) return Response(response) - return Response(response['error'], status=status.HTTP_400_BAD_REQUEST) + + return Response(response['error'], + status=status.HTTP_400_BAD_REQUEST) def get_queryset(self): """