Просмотр исходного кода

Distinguish between pre- and skip-conditions for checkout views

Terminology:

- A *pre*-condition is a condition that must be met in order for a view
  to be available. If it isn't then the customer will be redirected to
  a view *earlier* in the chain.

- A *skip*-condition is a condition that must NOT be met in order for a
  view to be available. If the condition is met, this means the view can
  be skipped and the customer should be redirected to a view *later* in
  the chain.

This change implements the above by introducing skip conditions for
checkout views. They allow the previous payment pre-conditions to be
cleanly separated.

An existing basket pre-condition is also changed to be a skip-condition.
master
David Winterbottom 11 лет назад
Родитель
Сommit
cd566b34c8
3 измененных файлов: 75 добавлений и 34 удалений
  1. 9
    0
      oscar/apps/checkout/exceptions.py
  2. 53
    27
      oscar/apps/checkout/session.py
  3. 13
    7
      oscar/apps/checkout/views.py

+ 9
- 0
oscar/apps/checkout/exceptions.py Просмотреть файл

@@ -8,3 +8,12 @@ class FailedPreCondition(Exception):
8 8
             self.messages = messages
9 9
         else:
10 10
             self.messages = []
11
+
12
+
13
+class PassedSkipCondition(Exception):
14
+    """
15
+    To be raised when a skip condition has been passed and the current view
16
+    should be skipped. The passed URL dictates where to.
17
+    """
18
+    def __init__(self, url):
19
+        self.url = url

+ 53
- 27
oscar/apps/checkout/session.py Просмотреть файл

@@ -1,3 +1,5 @@
1
+from decimal import Decimal as D
2
+
1 3
 from django.contrib import messages
2 4
 from django import http
3 5
 from django.core.exceptions import ImproperlyConfigured
@@ -25,9 +27,15 @@ class CheckoutSessionMixin(object):
25 27
     checkout information is available in the template context.
26 28
     """
27 29
     # This should be list of method names that get executed before the normal
28
-    # flow of the view.
30
+    # flow of the view. Each method should check some condition has been met.
31
+    # If not, then an exception is raised that indicates the URL the customer
32
+    # should be redirect to.
29 33
     pre_conditions = None
30 34
 
35
+    # Skip conditions check whether the view should be skipped. They work in
36
+    # the same way as pre-conditions.
37
+    skip_conditions = None
38
+
31 39
     def dispatch(self, request, *args, **kwargs):
32 40
         # Assign the checkout session manager so it's available in all checkout
33 41
         # views.
@@ -41,6 +49,12 @@ class CheckoutSessionMixin(object):
41 49
                 messages.warning(request, message)
42 50
             return http.HttpResponseRedirect(e.url)
43 51
 
52
+        # Check if this view should be skipped
53
+        try:
54
+            self.check_skip_conditions(request)
55
+        except exceptions.PassedSkipCondition as e:
56
+            return http.HttpResponseRedirect(e.url)
57
+
44 58
         return super(CheckoutSessionMixin, self).dispatch(
45 59
             request, *args, **kwargs)
46 60
 
@@ -61,6 +75,23 @@ class CheckoutSessionMixin(object):
61 75
             return []
62 76
         return self.pre_conditions
63 77
 
78
+    def check_skip_conditions(self, request):
79
+        skip_conditions = self.get_skip_conditions(request)
80
+        for method_name in skip_conditions:
81
+            if not hasattr(self, method_name):
82
+                raise ImproperlyConfigured(
83
+                    "There is no method '%s' to call as a skip-condition" % (
84
+                        method_name))
85
+            getattr(self, method_name)(request)
86
+
87
+    def get_skip_conditions(self, request):
88
+        """
89
+        Return the skip-condition method names to run for this view
90
+        """
91
+        if self.skip_conditions is None:
92
+            return []
93
+        return self.skip_conditions
94
+
64 95
     # Re-usable pre-condition validators
65 96
 
66 97
     def check_basket_is_not_empty(self, request):
@@ -107,14 +138,6 @@ class CheckoutSessionMixin(object):
107 138
                     "Please either sign in or enter your email address")
108 139
             )
109 140
 
110
-    def check_basket_requires_shipping(self, request):
111
-        # Check to see that a shipping address is actually required.  It may
112
-        # not be if the basket is purely downloads
113
-        if not request.basket.is_shipping_required():
114
-            raise exceptions.FailedPreCondition(
115
-                url=reverse('checkout:shipping-method')
116
-            )
117
-
118 141
     def check_shipping_data_is_captured(self, request):
119 142
         if not request.basket.is_shipping_required():
120 143
             return
@@ -134,32 +157,35 @@ class CheckoutSessionMixin(object):
134 157
                 message=_("Please choose a shipping method")
135 158
             )
136 159
 
137
-    def check_payment_is_necessary(self, request):
138
-        shipping_address = self.get_shipping_address(request.basket)
139
-        shipping_method = self.get_shipping_method(
140
-            request.basket, shipping_address)
141
-        total = self.get_order_totals(request.basket, shipping_method)
142
-        if total.excl_tax == 0:
143
-            raise exceptions.FailedPreCondition(
144
-                url=reverse('checkout:preview'),
145
-                message=_("Payment is not necessary for this order")
146
-            )
147
-
148 160
     def check_payment_data_is_captured(self, request):
149 161
         # We don't collect payment data by default so we don't have anything to
150 162
         # validate here. If your shop requires forms to be submitted on the
151 163
         # payment details page, then override this method to check that the
152 164
         # relevant data is available. Often just enforcing that the preview
153 165
         # view is only accessible from a POST request is sufficient.
154
-
155
-        # Skip check if payment isn't necessary
156
-        try:
157
-            self.check_payment_is_necessary(request)
158
-        except exceptions.FailedPreCondition:
159
-            return
160
-        # Validate payment data here
161 166
         pass
162 167
 
168
+    # Re-usable skip conditions
169
+
170
+    def skip_unless_basket_requires_shipping(self, request):
171
+        # Check to see that a shipping address is actually required.  It may
172
+        # not be if the basket is purely downloads
173
+        if not request.basket.is_shipping_required():
174
+            raise exceptions.PassedSkipCondition(
175
+                url=reverse('checkout:payment-method')
176
+            )
177
+
178
+    def skip_unless_payment_is_required(self, request):
179
+        # Check to see if payment is actually required for this order.
180
+        shipping_address = self.get_shipping_address(request.basket)
181
+        shipping_method = self.get_shipping_method(
182
+            request.basket, shipping_address)
183
+        total = self.get_order_totals(request.basket, shipping_method)
184
+        if total.excl_tax == D('0.00'):
185
+            raise exceptions.PassedSkipCondition(
186
+                url=reverse('checkout:preview')
187
+            )
188
+
163 189
     # Helpers
164 190
 
165 191
     def get_context_data(self, **kwargs):

+ 13
- 7
oscar/apps/checkout/views.py Просмотреть файл

@@ -134,8 +134,8 @@ class ShippingAddressView(CheckoutSessionMixin, generic.FormView):
134 134
     success_url = reverse_lazy('checkout:shipping-method')
135 135
     pre_conditions = ['check_basket_is_not_empty',
136 136
                       'check_basket_is_valid',
137
-                      'check_user_email_is_captured',
138
-                      'check_basket_requires_shipping']
137
+                      'check_user_email_is_captured']
138
+    skip_conditions = ['skip_unless_basket_requires_shipping']
139 139
 
140 140
     def get_initial(self):
141 141
         return self.checkout_session.new_shipping_address_fields()
@@ -244,7 +244,7 @@ class ShippingMethodView(CheckoutSessionMixin, generic.TemplateView):
244 244
     template_name = 'checkout/shipping_methods.html'
245 245
     pre_conditions = ['check_basket_is_not_empty',
246 246
                       'check_basket_is_valid',
247
-                      'check_user_email_is_captured', ]
247
+                      'check_user_email_is_captured',]
248 248
 
249 249
     def get(self, request, *args, **kwargs):
250 250
         # These pre-conditions can't easily be factored out into the normal
@@ -338,8 +338,8 @@ class PaymentMethodView(CheckoutSessionMixin, generic.TemplateView):
338 338
         'check_basket_is_not_empty',
339 339
         'check_basket_is_valid',
340 340
         'check_user_email_is_captured',
341
-        'check_shipping_data_is_captured',
342
-        'check_payment_is_necessary']
341
+        'check_shipping_data_is_captured']
342
+    skip_conditions = ['skip_unless_payment_is_required']
343 343
 
344 344
     def get(self, request, *args, **kwargs):
345 345
         # By default we redirect straight onto the payment details view. Shops
@@ -391,6 +391,8 @@ class PaymentDetailsView(OrderPlacementMixin, generic.TemplateView):
391 391
     template_name = 'checkout/payment_details.html'
392 392
     template_name_preview = 'checkout/preview.html'
393 393
 
394
+    # These conditions are extended at runtime depending on whether we are in
395
+    # 'preview' mode or not.
394 396
     pre_conditions = [
395 397
         'check_basket_is_not_empty',
396 398
         'check_basket_is_valid',
@@ -406,9 +408,13 @@ class PaymentDetailsView(OrderPlacementMixin, generic.TemplateView):
406 408
             # The preview view needs to ensure payment information has been
407 409
             # correctly captured.
408 410
             return self.pre_conditions + ['check_payment_data_is_captured']
409
-        else:
411
+        return super(PaymentDetailsView, self).get_pre_conditions(request)
412
+
413
+    def get_skip_conditions(self, request):
414
+        if not self.preview:
410 415
             # Payment details should only be collected if necessary
411
-            return self.pre_conditions + ['check_payment_is_necessary']
416
+            return ['skip_unless_payment_is_required']
417
+        return super(PaymentDetailsView, self).get_skip_conditions(request)
412 418
 
413 419
     def post(self, request, *args, **kwargs):
414 420
         # Posting to payment-details isn't the right thing to do.  Form

Загрузка…
Отмена
Сохранить