Browse Source

Major tidy up of checkout view classes.

Pulled template variables into a class context variable
Updated shipping method loading
master
David Winterbottom 14 years ago
parent
commit
aeccbd0499

+ 10
- 1
docs/source/contributing.rst View File

1
 Contributing
1
 Contributing
2
 ============
2
 ============
3
 
3
 
4
-Guidelines:
4
+Make sure you're read http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html
5
+
6
+Guidelines
7
+----------
5
 
8
 
6
 * New features should be discussed on the mailing list (or in the meetings) before serious work starts
9
 * New features should be discussed on the mailing list (or in the meetings) before serious work starts
7
 * Pull requests will be rejected if sufficient tests aren't provided
10
 * Pull requests will be rejected if sufficient tests aren't provided
8
 * Please updated the documentation when altering behaviour or introducing new features 
11
 * Please updated the documentation when altering behaviour or introducing new features 
9
 
12
 
13
+Coding conventions
14
+------------------
15
+
16
+* PEP8 (http://www.python.org/dev/peps/pep-0008/)
17
+* PEP257 (http://www.python.org/dev/peps/pep-0257/)
18
+
10
 Contents:
19
 Contents:
11
 
20
 
12
 .. toctree::
21
 .. toctree::

+ 1
- 0
docs/source/index.rst View File

13
 
13
 
14
    introduction
14
    introduction
15
    getting_started
15
    getting_started
16
+   web_services
16
    recipes
17
    recipes
17
    contributing
18
    contributing
18
 
19
 

+ 47
- 0
docs/source/recipes/custom_shipping_logic.rst View File

1
+Shipping
2
+========
3
+
4
+By default, you can configure shipping by using the built-in ShippingMethod models.  These
5
+support shipping charges that are calculated using an order- and item-level charge.
6
+
7
+Custom shipping calculators
8
+---------------------------
9
+
10
+To use a custom shipping calculator, you need to subclass the core shipping Repository class and
11
+override two methods in provide the calculator of your domain.
12
+
13
+First create a ``myshop.shipping`` app and include it in your ``settings.py`` file (removing the ``oscar.shipping``
14
+app in the process.
15
+
16
+Next, create ``methods.py`` and create a new ``Repository`` class that subclasses the core ``Repository`` class but
17
+provides the custom behaviour that you need.
18
+
19
+Here is an example ``methods.py``::
20
+
21
+    from decimal import Decimal
22
+
23
+    from oscar.shipping.methods import Repository as CoreRepository
24
+    from oscar.shipping.abstract_models import ShippingMethod
25
+    
26
+    class FixedChargeMethod(ShippingMethod):
27
+        
28
+        name = 'Fixed charge'
29
+        
30
+        def basket_charge_incl_tax(self):
31
+            return Decimal('12.50')
32
+        
33
+        def basket_charge_excl_tax(self):
34
+            return Decimal('12.50')
35
+    
36
+    class Repository(CoreRepository):
37
+        
38
+        def __init__(self):
39
+            self.method = FixedChargeMethod()
40
+        
41
+        def get_shipping_methods(self, user, basket):
42
+            return [self.method] 
43
+    
44
+        def find_by_code(self, code):
45
+            return self.method
46
+
47
+Here we are using a plain Python object (not a Django model) as the shipping calculator.

+ 12
- 0
docs/source/web_services.rst View File

1
+Web services
2
+============
3
+
4
+Django-oscar exposes three different RESTful webservices for administration:
5
+
6
+.. toctree::
7
+   :maxdepth: 2
8
+
9
+   web_services/order_management
10
+   web_services/stock_management
11
+   web_services/inventory_management
12
+

+ 3
- 0
docs/source/web_services/inventory_management.rst View File

1
+Inventory management REST services
2
+==============================
3
+

+ 62
- 0
docs/source/web_services/order_management.rst View File

1
+Order management REST services
2
+==============================
3
+
4
+Supported methods and resources:
5
+
6
+**Retrieve list of orders:**::
7
+
8
+    GET /orders/
9
+
10
+Filters:
11
+
12
+* ``after=2010-10-01`` - Return all orders placed after 2010-10-01
13
+* ``before=2010-10-31`` - Return all orders placed before 2010-10-31
14
+
15
+**Retrieve a summary of an order with number 123 (not id)**::
16
+
17
+    GET /order/123/
18
+
19
+**Retrieve a list of batches**::
20
+
21
+    GET /order/123/batches/
22
+
23
+**Retrieve a summary of batch**::
24
+
25
+    GET /order/123/batch/34/
26
+
27
+**Retrieve a list of lines**::
28
+
29
+    GET /order/123/batch/34/lines/ [just lines within batch 34, part of order 123]
30
+    
31
+    GET /order/123/lines/ [all lines within order 123]
32
+    
33
+    GET /lines/ [all lines]
34
+
35
+Filters:
36
+
37
+* ``at_shipping_status`` - Returns lines at the specified shipping status (use the code)
38
+
39
+* ``at_payment_status`` - Returns lines at the specified payment status (use the code)
40
+
41
+* ``partner`` - Returns lines fulfilled by a particular partner
42
+
43
+**Retrieve a summary of a lines with ids 100,101,102**::
44
+
45
+    GET /order/123/batch/34/line/100;101;102`` 
46
+
47
+    GET /order/123/line/100;101;102`` 
48
+
49
+**Update shipping status of an order line**::
50
+
51
+    POST /order/123/batch/34/line/100/ 
52
+
53
+    POST /order/123/lines/ 
54
+
55
+Request:
56
+
57
+``{'shipping_status': 'acknowledged'}`` - Update every item in line
58
+
59
+``{'shipping_status': {'acknowledged': 10, 'cancelled': 1}}`` - Fine-grained control
60
+
61
+
62
+

+ 3
- 0
docs/source/web_services/stock_management.rst View File

1
+Stock management REST services
2
+==============================
3
+

+ 6
- 7
examples/defaultshop/selenium/save-delivery-address View File

13
 </thead><tbody>
13
 </thead><tbody>
14
 <tr>
14
 <tr>
15
 	<td>open</td>
15
 	<td>open</td>
16
-	<td>/shop/checkout/delivery_address/</td>
16
+	<td>/shop/checkout/shipping-address/</td>
17
 	<td></td>
17
 	<td></td>
18
 </tr>
18
 </tr>
19
 <tr>
19
 <tr>
24
 <tr>
24
 <tr>
25
 	<td>type</td>
25
 	<td>type</td>
26
 	<td>id_last_name</td>
26
 	<td>id_last_name</td>
27
-	<td>Winterbottom</td>
27
+	<td>Winerbottom</td>
28
 </tr>
28
 </tr>
29
 <tr>
29
 <tr>
30
 	<td>type</td>
30
 	<td>type</td>
39
 <tr>
39
 <tr>
40
 	<td>type</td>
40
 	<td>type</td>
41
 	<td>id_postcode</td>
41
 	<td>id_postcode</td>
42
-	<td>N12 9ET</td>
42
+	<td>N12 9et</td>
43
 </tr>
43
 </tr>
44
 <tr>
44
 <tr>
45
-	<td>type</td>
45
+	<td>select</td>
46
 	<td>id_country</td>
46
 	<td>id_country</td>
47
-	<td>UK</td>
47
+	<td>label=United Kingdom of Great Britain and Northern Ireland</td>
48
 </tr>
48
 </tr>
49
 <tr>
49
 <tr>
50
 	<td>clickAndWait</td>
50
 	<td>clickAndWait</td>
51
-	<td>//input[@value='Save delivery address']</td>
51
+	<td>//input[@value='Save shipping address']</td>
52
 	<td></td>
52
 	<td></td>
53
 </tr>
53
 </tr>
54
-
55
 </tbody></table>
54
 </tbody></table>
56
 </body>
55
 </body>
57
 </html>
56
 </html>

+ 5
- 10
oscar/checkout/utils.py View File

2
 
2
 
3
 from oscar.services import import_module
3
 from oscar.services import import_module
4
 
4
 
5
-shipping_models = import_module('shipping.models', ['Method'])
5
+shipping_methods = import_module('shipping.repository', ['Repository'])
6
 
6
 
7
 
7
 
8
 class ProgressChecker(object):
8
 class ProgressChecker(object):
75
 class CheckoutSessionData(object):
75
 class CheckoutSessionData(object):
76
     u"""Class responsible for marshalling all the checkout session data."""
76
     u"""Class responsible for marshalling all the checkout session data."""
77
     SESSION_KEY = 'checkout_data'
77
     SESSION_KEY = 'checkout_data'
78
-    FREE_SHIPPING = '__free__'
79
     
78
     
80
     def __init__(self, request):
79
     def __init__(self, request):
81
         self.request = request
80
         self.request = request
122
     def user_address_id(self):
121
     def user_address_id(self):
123
         return self._get('shipping', 'user_address_id')
122
         return self._get('shipping', 'user_address_id')
124
     
123
     
125
-    def use_free_shipping(self):
126
-        self._set('shipping', 'method_code', '__free__')
127
-    
128
     def use_shipping_method(self, code):
124
     def use_shipping_method(self, code):
129
         self._set('shipping', 'method_code', code)
125
         self._set('shipping', 'method_code', code)
130
         
126
         
134
         data stored in the session.
130
         data stored in the session.
135
         """
131
         """
136
         code = self._get('shipping', 'method_code')
132
         code = self._get('shipping', 'method_code')
137
-        if code == self.FREE_SHIPPING:
138
-            method = shipping_models.Method(name="Standard shipping (Free)", code=self.FREE_SHIPPING)
139
-        else:
140
-            method = shipping_models.Method.objects.get(code=code)
141
-        return method
133
+        if not code:
134
+            return None
135
+        repo = shipping_methods.Repository()
136
+        return repo.find_by_code(code)

+ 48
- 61
oscar/checkout/views.py View File

1
+from decimal import Decimal
2
+
1
 from django.conf import settings
3
 from django.conf import settings
2
 from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseBadRequest
4
 from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseBadRequest
3
 from django.template import RequestContext
5
 from django.template import RequestContext
19
 order_models = import_module('order.models', ['Order', 'ShippingAddress'])
21
 order_models = import_module('order.models', ['Order', 'ShippingAddress'])
20
 order_utils = import_module('order.utils', ['OrderCreator'])
22
 order_utils = import_module('order.utils', ['OrderCreator'])
21
 address_models = import_module('address.models', ['UserAddress'])
23
 address_models = import_module('address.models', ['UserAddress'])
22
-shipping_models = import_module('shipping.models', ['Method'])
24
+shipping_repository = import_module('shipping.repository', ['Repository'])
23
 
25
 
24
 def prev_steps_must_be_complete(view_fn):
26
 def prev_steps_must_be_complete(view_fn):
25
     u"""
27
     u"""
78
         # Set up the instance variables that are needed to place an order
80
         # Set up the instance variables that are needed to place an order
79
         self.request = request
81
         self.request = request
80
         self.co_data = checkout_utils.CheckoutSessionData(request)
82
         self.co_data = checkout_utils.CheckoutSessionData(request)
83
+        self.basket = basket_factory.BasketFactory().get_open_basket(self.request)
84
+        self.context = {'basket': self.basket,
85
+                        'order_total': self.get_order_total(),
86
+                        'shipping_addr': self.get_shipping_address()}
87
+        self.set_shipping_context()
81
         
88
         
82
         if request.method == 'POST':
89
         if request.method == 'POST':
83
             response = self.handle_POST()
90
             response = self.handle_POST()
87
             response = HttpResponseBadRequest()
94
             response = HttpResponseBadRequest()
88
         return response
95
         return response
89
     
96
     
97
+    def set_shipping_context(self):
98
+        method = self.co_data.shipping_method()
99
+        if method:
100
+            method.set_basket(self.basket)
101
+            self.context['method'] = method
102
+            self.context['shipping_total_excl_tax'] = method.basket_charge_excl_tax()
103
+            self.context['shipping_total_incl_tax'] = method.basket_charge_incl_tax()
104
+    
90
     def handle_GET(self):
105
     def handle_GET(self):
91
         u"""
106
         u"""
92
         Default behaviour is to set step as complete and redirect
107
         Default behaviour is to set step as complete and redirect
94
         """ 
109
         """ 
95
         return self.get_success_response()
110
         return self.get_success_response()
96
     
111
     
112
+    def get_order_total(self):
113
+        calc = checkout_calculators.OrderTotalCalculator(self.request)
114
+        return calc.order_total_incl_tax(self.basket)
115
+    
116
+    def get_shipping_address(self):
117
+        # Load address data into a blank address model
118
+        addr_data = self.co_data.new_address_fields()
119
+        if addr_data:
120
+            return order_models.ShippingAddress(**addr_data)
121
+        addr_id = self.co_data.user_address_id()
122
+        if addr_id:
123
+            return address_models.UserAddress.objects.get(pk=addr_id)
124
+        return None
125
+    
97
     def get_success_response(self):
126
     def get_success_response(self):
98
         u"""
127
         u"""
99
         Returns the appropriate redirect response if a checkout
128
         Returns the appropriate redirect response if a checkout
146
                 form = checkout_forms.ShippingAddressForm(addr_fields)
175
                 form = checkout_forms.ShippingAddressForm(addr_fields)
147
             else:
176
             else:
148
                 form = checkout_forms.ShippingAddressForm()
177
                 form = checkout_forms.ShippingAddressForm()
178
+        self.context['form'] = form
149
     
179
     
150
-        # Add in extra template bindings
151
-        basket = basket_factory.BasketFactory().get_open_basket(self.request)
152
-        calc = checkout_calculators.OrderTotalCalculator(self.request)
153
-        order_total = calc.order_total_incl_tax(basket)
154
-        shipping_total_excl_tax = 0
155
-        shipping_total_incl_tax = 0
156
-        
157
         # Look up address book data
180
         # Look up address book data
158
         if self.request.user.is_authenticated():
181
         if self.request.user.is_authenticated():
159
-            addresses = address_models.UserAddress.objects.filter(user=self.request.user)
182
+            self.context['addresses'] = address_models.UserAddress.objects.filter(user=self.request.user)
160
         
183
         
161
-        return render(self.request, self.template_file, locals())
184
+        return render(self.request, self.template_file, self.context)
162
     
185
     
163
     
186
     
164
 class ShippingMethodView(CheckoutView):
187
 class ShippingMethodView(CheckoutView):
169
     template_file = 'checkout/shipping_methods.html';
192
     template_file = 'checkout/shipping_methods.html';
170
     
193
     
171
     def handle_GET(self):
194
     def handle_GET(self):
172
-        basket = basket_factory.BasketFactory().get_open_basket(self.request)
173
-        methods = self.get_shipping_methods_for_basket(basket)
174
-        
175
-        if not methods.count():
176
-            # No defined methods - assume delivery is free
177
-            self.co_data.use_free_shipping()
178
-            return self.get_success_response()
179
-        
180
-        if methods.count() == 1:
181
-            # Only one method - set this
195
+        methods = self.get_available_shipping_methods()
196
+        if len(methods) == 1:
197
+            # Only one method - set this and redirect onto the next step
182
             self.co_data.use_shipping_method(methods[0].code)
198
             self.co_data.use_shipping_method(methods[0].code)
183
             return self.get_success_response()
199
             return self.get_success_response()
184
         
200
         
185
-        for method in methods:
186
-            method.set_basket(basket)
187
-        
188
-        # Load address data into a blank address model
189
-        addr_data = self.co_data.new_address_fields()
190
-        if addr_data:
191
-            shipping_addr = order_models.ShippingAddress(**addr_data)
192
-        addr_id = self.co_data.user_address_id()
193
-        if addr_id:
194
-            shipping_addr = address_models.UserAddress.objects.get(pk=addr_id)
195
-        
196
-        calc = checkout_calculators.OrderTotalCalculator(self.request)
197
-        order_total = calc.order_total_incl_tax(basket)
198
-        
199
-        return render(self.request, self.template_file, locals())
201
+        self.context['methods'] = methods
202
+        return render(self.request, self.template_file, self.context)
200
     
203
     
201
-    def get_shipping_methods_for_basket(self, basket):
202
-        return shipping_models.Method.objects.all()
204
+    def get_available_shipping_methods(self):
205
+        u"""
206
+        Returns all applicable shipping method objects
207
+        for a given basket.
208
+        """ 
209
+        repo = shipping_repository.Repository()
210
+        return repo.get_shipping_methods(self.request.user, self.basket, self.get_shipping_address())
203
     
211
     
204
     def handle_POST(self):
212
     def handle_POST(self):
205
         method_code = self.request.POST['method_code']
213
         method_code = self.request.POST['method_code']
224
     template_file = 'checkout/preview.html'
232
     template_file = 'checkout/preview.html'
225
     
233
     
226
     def handle_GET(self):
234
     def handle_GET(self):
227
-        basket = basket_factory.BasketFactory().get_open_basket(self.request)
228
-        
229
-        # Load address data into a blank address model
230
-        addr_data = self.co_data.new_address_fields()
231
-        if addr_data:
232
-            shipping_addr = order_models.ShippingAddress(**addr_data)
233
-        addr_id = self.co_data.user_address_id()
234
-        if addr_id:
235
-            shipping_addr = address_models.UserAddress.objects.get(pk=addr_id)
236
-        
237
-        # Shipping method
238
-        method = self.co_data.shipping_method()
239
-        method.set_basket(basket)
240
-
241
-        shipping_total_excl_tax = method.basket_charge_excl_tax()
242
-        shipping_total_incl_tax = method.basket_charge_incl_tax()
243
-        
244
-        # Calculate order total
245
-        calc = checkout_calculators.OrderTotalCalculator(self.request)
246
-        order_total = calc.order_total_incl_tax(basket, method)
247
         
235
         
248
         mark_step_as_complete(self.request)
236
         mark_step_as_complete(self.request)
249
-        return render(self.request, self.template_file, locals())
237
+        return render(self.request, self.template_file, self.context)
250
 
238
 
251
 
239
 
252
 class PaymentDetailsView(CheckoutView):
240
 class PaymentDetailsView(CheckoutView):
277
     
265
     
278
     def handle_POST(self):
266
     def handle_POST(self):
279
         
267
         
280
-        basket = basket_factory.BasketFactory().get_open_basket(self.request)
281
-        self._handle_payment(basket)
282
-        order = self._place_order(basket)
268
+        self._handle_payment(self.basket)
269
+        order = self._place_order(self.basket)
283
         self._reset_checkout()
270
         self._reset_checkout()
284
         
271
         
285
         # Send signal
272
         # Send signal

+ 14
- 3
oscar/shipping/abstract_models.py View File

1
-import zlib
2
 from decimal import Decimal
1
 from decimal import Decimal
3
 
2
 
4
 from django.db import models
3
 from django.db import models
5
 from django.utils.translation import ugettext_lazy as _
4
 from django.utils.translation import ugettext_lazy as _
6
 from django.template.defaultfilters import slugify
5
 from django.template.defaultfilters import slugify
7
 
6
 
7
+from oscar.shipping.methods import ShippingMethod
8
 
8
 
9
-class AbstractMethod(models.Model):
10
-    u"""Shipping method"""
9
+
10
+class AbstractMethod(models.Model, ShippingMethod):
11
+    u"""
12
+    Standard shipping method
13
+    
14
+    This method has two components: 
15
+    * a charge per order
16
+    * a charge per item
17
+    
18
+    Many sites use shipping logic which fits into this system.  However, for more
19
+    complex shipping logic, a custom shipping method object will need to be provided
20
+    that subclasses ShippingMethod.
21
+    """
11
     code = models.CharField(max_length=128, unique=True)
22
     code = models.CharField(max_length=128, unique=True)
12
     name = models.CharField(_("Name"), max_length=128)
23
     name = models.CharField(_("Name"), max_length=128)
13
     description = models.TextField(_("Description"), blank=True)
24
     description = models.TextField(_("Description"), blank=True)

+ 31
- 0
oscar/shipping/methods.py View File

1
+class ShippingMethod(object):
2
+    u"""
3
+    Superclass for all shipping method objects
4
+    """
5
+    code = '__default__'
6
+    name = 'Default shipping'
7
+    description = ''
8
+    
9
+    def set_basket(self, basket):
10
+        self.basket = basket
11
+    
12
+    def basket_charge_incl_tax(self):
13
+        pass
14
+    
15
+    def basket_charge_excl_tax(self):
16
+        pass
17
+    
18
+    
19
+class FreeShipping(ShippingMethod):
20
+    u"""
21
+    Simple method for free shipping
22
+    """
23
+    code = 'free-shipping'
24
+    name = 'Free shipping'
25
+    
26
+    def basket_charge_incl_tax(self):
27
+        return Decimal('0.00')
28
+    
29
+    def basket_charge_excl_tax(self):
30
+        return Decimal('0.00')
31
+       

+ 36
- 0
oscar/shipping/repository.py View File

1
+from oscar.shipping.methods import FreeShipping
2
+from oscar.services import import_module
3
+
4
+shipping_models = import_module('shipping.models', ['Method'])
5
+
6
+
7
+class Repository(object):
8
+    u"""
9
+    Repository class responsible for returning ShippingMethod
10
+    objects
11
+    """
12
+    
13
+    def get_shipping_methods(self, user, basket, shipping_addr):
14
+        u"""
15
+        Returns all applicable shipping method objects
16
+        for a given basket.
17
+        
18
+        We default to returning the Method models that have been defined but
19
+        this behaviour can easily be overridden by subclassing this class
20
+        and overriding this method.
21
+        """ 
22
+        methods = shipping_models.Method.objects.all()
23
+        if not methods.count():
24
+            return [FreeShipping()]
25
+        
26
+        for method in methods:
27
+            method.set_basket(basket)
28
+        return methods
29
+
30
+    def find_by_code(self, code):
31
+        u"""
32
+        Returns the appropriate Method object for the given code
33
+        """
34
+        if code == FreeShipping.code:
35
+            return FreeShipping()
36
+        return shipping_models.Method.objects.get(code=code)          

+ 3
- 1
oscar/templates/checkout/shipping_address.html View File

77
         <td>{{ basket.total_excl_tax|currency }}</td>
77
         <td>{{ basket.total_excl_tax|currency }}</td>
78
         <td>{{ basket.total_incl_tax|currency }}</td>
78
         <td>{{ basket.total_incl_tax|currency }}</td>
79
     </tr>
79
     </tr>
80
+    {% if shipping_total_excl_tax %}
80
     <tr>
81
     <tr>
81
         <td colspan="5">shipping charge</td>
82
         <td colspan="5">shipping charge</td>
82
         <td>{{ shipping_total_excl_tax|currency }}</td>
83
         <td>{{ shipping_total_excl_tax|currency }}</td>
83
         <td>{{ shipping_total_incl_tax|currency }}</td>
84
         <td>{{ shipping_total_incl_tax|currency }}</td>
84
     </tr>
85
     </tr>
86
+    {% endif %}
85
     <tr>
87
     <tr>
86
-        <td colspan="6">Order total</td>
88
+        <td colspan="6">Order total (before shipping)</td>
87
         <td>{{ order_total|currency }}</td>
89
         <td>{{ order_total|currency }}</td>
88
     </tr>
90
     </tr>
89
 </table>
91
 </table>

Loading…
Cancel
Save