瀏覽代碼

Merge branch 'releases/0.2'

Conflicts:
	oscar/__init__.py
	oscar/apps/basket/abstract_models.py
	oscar/apps/checkout/views.py
	tests/functional/basket_tests.py
master
David Winterbottom 13 年之前
父節點
當前提交
4d5b02c7cb

+ 9
- 6
README.rst 查看文件

@@ -10,12 +10,13 @@ core functionality can be customised to suit the needs of your project.  This
10 10
 allows it to handle a wide range of e-commerce requirements, from large-scale B2C
11 11
 sites to complex B2B sites rich in domain-specific business logic.
12 12
 
13
-This README is just a stub - see the following links for more details
14
-information:
13
+More information:
15 14
 
16 15
 * `Official homepage`_ 
17
-* `Demo site`_ (experimental) 
18
-* `Documentation`_ on `readthedocs.org`_
16
+* `Sandbox site`_ (an hourly build of the unstable master branch - it's
17
+  experimental but feel free to explore and get a feel for the base Oscar
18
+  install) 
19
+* `Documentation`_ on the excellent `readthedocs.org`_
19 20
 * `Google Group`_ - the mailing list is django-oscar@googlegroups.com
20 21
 * `Continuous integration homepage`_ on `travis-ci.org`_
21 22
 * `Twitter account for news and updates`_
@@ -24,7 +25,7 @@ information:
24 25
 .. image:: https://secure.travis-ci.org/tangentlabs/django-oscar.png
25 26
 
26 27
 .. _`Official homepage`: http://oscarcommerce.com
27
-.. _`Demo site`: http://sandbox.oscar.tangentlabs.co.uk
28
+.. _`Sandbox site`: http://sandbox.oscar.tangentlabs.co.uk
28 29
 .. _`Documentation`: http://django-oscar.readthedocs.org/en/latest/
29 30
 .. _`readthedocs.org`: http://readthedocs.org
30 31
 .. _`Continuous integration homepage`: http://travis-ci.org/#!/tangentlabs/django-oscar 
@@ -34,7 +35,8 @@ information:
34 35
 .. _`Google Group`: https://groups.google.com/forum/?fromgroups#!forum/django-oscar
35 36
 
36 37
 Oscar was written by `David Winterbottom`_ (`@codeinthehole`_) and is developed
37
-and maintained by `Tangent Labs`_, a London-based digital agency.
38
+and maintained by `Tangent Labs`_, a London-based digital agency, with help from
39
+`Mirumee`.
38 40
 
39 41
 Oscar is released under the permissive `New BSD license`_.
40 42
 
@@ -42,6 +44,7 @@ Oscar is released under the permissive `New BSD license`_.
42 44
 .. _`@codeinthehole`: https://twitter.com/codeinthehole
43 45
 .. _`Tangent Labs`: http://www.tangentlabs.co.uk
44 46
 .. _`New BSD license`: https://github.com/tangentlabs/django-oscar/blob/master/LICENSE
47
+.. _`Mirumee`: http://mirumee.com/
45 48
 
46 49
 Case studies
47 50
 ------------

+ 7
- 5
docs/source/conf.py 查看文件

@@ -11,14 +11,16 @@
11 11
 # All configuration values have a default; values that are commented out
12 12
 # serve to show the default.
13 13
 
14
-import sys, os
15
-oscar_folder = os.path.realpath(os.path.join(os.path.dirname(__file__), '../..'))
16
-sys.path.append(oscar_folder)
17
-
18 14
 # If extensions (or modules to document with autodoc) are in another directory,
19 15
 # add these directories to sys.path here. If the directory is relative to the
20 16
 # documentation root, use os.path.abspath to make it absolute, like shown here.
21 17
 #sys.path.insert(0, os.path.abspath('.'))
18
+import sys, os
19
+oscar_folder = os.path.realpath(os.path.join(os.path.dirname(__file__), '../..'))
20
+sys.path.append(oscar_folder)
21
+
22
+# Get django settings
23
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sandbox.settings')
22 24
 
23 25
 # -- General configuration -----------------------------------------------------
24 26
 
@@ -43,7 +45,7 @@ master_doc = 'index'
43 45
 
44 46
 # General information about the project.
45 47
 project = u'django-oscar'
46
-copyright = u'2011, David Winterbottom'
48
+copyright = u'Tangent Labs'
47 49
 
48 50
 # The version info for the project you're documenting, acts as replacement for
49 51
 # |version| and |release|, also used in various other places throughout the

+ 17
- 1
docs/source/index.rst 查看文件

@@ -39,9 +39,25 @@ Features:
39 39
 
40 40
 * Extension libraries available for PayPal, GoCardless, DataCash and more
41 41
 
42
+Oscar is a good choice if your domain has non-trivial business logic.  Oscar's
43
+flexibility means it's straightforward to implement business rules that would be
44
+difficult to apply in other frameworks.  
45
+
46
+Example requirements that Oscar applications already handle:
47
+
48
+* Paying for an order with multiple payment sources (eg using a bankcard,
49
+  voucher, giftcard and business account).
50
+
51
+* Complex access control rules governing who can view and order what.
52
+
53
+* Supporting a hierarchy of customers, sales reps and sales directors - each
54
+  being able to "masquerade" as their subordinate users.
55
+
56
+* Multi-lingual products and categories
57
+
42 58
 Oscar is developed by `Tangent Labs`_, a London-based digital agency.  It is
43 59
 used in production in several applications to sell everything from beer mats to
44
-ipads.  The `source is on Github`_. 
60
+ipads.  The `source is on Github`_ - contributions welcome.
45 61
 
46 62
 .. _`Tangent Labs`: http://www.tangentlabs.co.uk
47 63
 .. _`source is on Github`: https://github.com/tangentlabs/django-oscar

+ 40
- 0
docs/source/recipes/how_to_configure_stock_messaging.rst 查看文件

@@ -1,3 +1,43 @@
1 1
 ================================
2 2
 How to configure stock messaging
3 3
 ================================
4
+
5
+Stock messaging is controlled on a per-partner basis.  A product's stockrecord
6
+has the following methods for messaging:
7
+
8
+.. autoclass:: oscar.apps.partner.abstract_models.AbstractStockRecord
9
+    :members: availability, availability_code
10
+
11
+Both these methods delegate to a "partner wrapper" instance.  These are defined
12
+in the ``OSCAR_PARTNER_WRAPPERS`` setting which is a dict mapping from partner
13
+name to a class path, for instance::
14
+
15
+    # settings.py
16
+    OSCAR_PARTNER_WRAPPERS = {
17
+        'Partner A': 'myproject.wrappers.PartnerAWrapper',
18
+    }
19
+
20
+The default wrapper is :class:`oscar.apps.partner.wrappers.DefaultWrapper`,
21
+which provides methods of the same name.
22
+
23
+.. autoclass:: oscar.apps.partner.wrappers.DefaultWrapper
24
+    :members: availability, availability_code
25
+
26
+Custom wrappers should subclass this class and override the appropriate methods.
27
+Here's an example wrapper that provides custom availability messaging::
28
+
29
+    # myproject/wrappers.py
30
+    from oscar.apps.partner import wrappers
31
+
32
+
33
+    class PartnerAWrapper(wrappers.DefaultWrapper):
34
+        
35
+        def availability(self, stockrecord):
36
+            if stockrecord.net_stock_level > 0:
37
+                return "Available to buy now!"
38
+            return "Sorry, not available"
39
+
40
+        def availability_code(self, stockrecord):
41
+            if stockrecord.net_stock_level > 0:
42
+                return "icon_tick"
43
+            return "icon_cross"

+ 31
- 4
docs/source/recipes/how_to_disable_an_app.rst 查看文件

@@ -1,6 +1,33 @@
1
-=====================
2
-How to disable an app
3
-=====================
1
+============================
2
+How to disable an app's URLs
3
+============================
4 4
 
5
-...
5
+Suppose you don't want to use Oscar's dashboard but use your own.  The way to do
6
+this is to modify the URLs config to exclude the URLs from the app in question.
6 7
 
8
+You need to use your own root 'application' instance which gives you control
9
+over the URLs structure.  So your root ``urls.py`` should have::
10
+
11
+    # urls.py
12
+    from myproject.app import application
13
+
14
+    urlpatterns = patterns('',
15
+        ...
16
+        (r'', include(application.urls)),
17
+    )
18
+
19
+where ``application`` is a subclass of ``oscar.app.Shop`` which overrides the 
20
+link to the dashboard app::
21
+
22
+    # myproject/app.py
23
+    from oscar.app import Shop
24
+    from oscar.core.application import Application
25
+
26
+    class MyShop(Shop):
27
+
28
+        # Override the core dashboard_app instance to use a blank application 
29
+        # instance.  This means no dashboard URLs are included.
30
+        dashboard_app = Application()
31
+
32
+The only remaining task is to ensure your templates don't reference any
33
+dashboard URLs. 

+ 99
- 3
docs/source/reference/signals.rst 查看文件

@@ -2,16 +2,112 @@
2 2
 Signals
3 3
 =======
4 4
 
5
-Oscar defined a number of custom signals that provide useful hook-points for
5
+Oscar implements a number of custom signals that provide useful hook-points for
6 6
 adding functionality.
7 7
 
8
+product_viewed
9
+--------------
10
+
11
+.. data:: oscar.apps.catalogue.signals.product_viewed
12
+    :class:
13
+
14
+Raised when a product detail page is viewed.  
15
+
16
+Arguments sent with this signal:
17
+
18
+``product``
19
+    The product being viewed
20
+
21
+``user``
22
+    The user in question
23
+
24
+``request``
25
+    The request instance
26
+
27
+``response``
28
+    The response instance
29
+
30
+product_search
31
+--------------
32
+
33
+.. data:: oscar.apps.catalogue.signals.product_search
34
+    :class:
35
+
36
+Raised when a search is performed.
37
+
38
+Arguments sent with this signal:
39
+
40
+``query``
41
+    The search term
42
+
43
+``user``
44
+    The user in question
45
+
46
+basket_addition
47
+---------------
48
+
49
+.. data:: oscar.apps.basket.signals.basket_addition
50
+    :class:
51
+
52
+Raised when a product is added to a basket
53
+
54
+Arguments sent with this signal:
55
+
56
+``product``
57
+    The product being added
58
+
59
+``user``
60
+    The user in question
61
+
62
+voucher_addition
63
+----------------
64
+
65
+.. data:: oscar.apps.basket.signals.voucher_addition
66
+    :class:
67
+
68
+Raised when a valid voucher is added to a basket
69
+
70
+Arguments sent with this signal:
71
+
72
+``basket``
73
+    The basket in question
74
+
75
+``voucher``
76
+    The voucher in question
77
+
78
+pre_payment
79
+-----------
80
+
81
+.. data:: oscar.apps.checkout.signals.pre_payment
82
+    :class:
83
+
84
+Raised immediately before attempting to take payment in the checkout.
85
+
86
+Arguments sent with this signal:
87
+
88
+``view``
89
+    The view class instance
90
+
91
+post_payment
92
+------------
93
+
94
+.. data:: oscar.apps.checkout.signals.post_payment
95
+    :class:
96
+
97
+Raised immediately after payment has been taken.
98
+
99
+Arguments sent with this signal:
100
+
101
+``view``
102
+    The view class instance
103
+
8 104
 order_placed
9 105
 ------------
10 106
 
11
-.. data:: oscar.apps.order.order_placed
107
+.. data:: oscar.apps.order.signals.order_placed
12 108
     :class:
13 109
 
14
-Raised by the :class:`oscar.apps.order.OrderCreator` class when creating an order.
110
+Raised by the :class:`oscar.apps.order.utils.OrderCreator` class when creating an order.
15 111
 
16 112
 Arguments sent with this signal:
17 113
 

+ 1
- 1
oscar/__init__.py 查看文件

@@ -4,7 +4,7 @@ import os
4 4
 # a full release
5 5
 
6 6
 VERSION = (0, 3, 0, 'alpha', 0)
7
-
7
+    
8 8
 def get_short_version():
9 9
     return '%s.%s' % (VERSION[0], VERSION[1])
10 10
 

+ 2
- 2
oscar/app.py 查看文件

@@ -46,5 +46,5 @@ class Shop(Application):
46 46
         )
47 47
         return urlpatterns
48 48
     
49
-
50
-shop = Shop()
49
+# 'shop' kept for legacy projects - 'application' is a better name
50
+shop = application = Shop()

+ 12
- 6
oscar/apps/basket/abstract_models.py 查看文件

@@ -129,7 +129,7 @@ class AbstractBasket(models.Model):
129 129
         self.discounts = []
130 130
         self._lines = None
131 131
 
132
-    def merge_line(self, line):
132
+    def merge_line(self, line, add_quantities=True):
133 133
         """
134 134
         For transferring a line from another basket to this one.
135 135
 
@@ -142,17 +142,23 @@ class AbstractBasket(models.Model):
142 142
             line.basket = self
143 143
             line.save()
144 144
         else:
145
-            # Line already exists - bump its quantity and delete the old
146
-            existing_line.quantity += line.quantity
145
+            # Line already exists - assume the max quantity is correct and delete the old
146
+            if add_quantities:
147
+                existing_line.quantity += line.quantity
148
+            else:
149
+                existing_line.quantity = max(existing_line.quantity, line.quantity)
147 150
             existing_line.save()
148 151
             line.delete()
149 152
 
150
-    def merge(self, basket):
153
+    def merge(self, basket, add_quantities=True):
151 154
         """
152 155
         Merges another basket with this one.
156
+
157
+        :basket: The basket to merge into this one
158
+        :add_quantities: Whether to add line quantities when they are merged.
153 159
         """
154 160
         for line_to_merge in basket.all_lines():
155
-            self.merge_line(line_to_merge)
161
+            self.merge_line(line_to_merge, add_quantities)
156 162
         basket.status = MERGED
157 163
         basket.date_merged = datetime.datetime.now()
158 164
         basket.save()
@@ -189,7 +195,7 @@ class AbstractBasket(models.Model):
189 195
         shipping.
190 196
         """
191 197
         for line in self.all_lines():
192
-            if line.product.os_shipping_required:
198
+            if line.product.is_shipping_required:
193 199
                 return True
194 200
         return False
195 201
 

+ 1
- 1
oscar/apps/basket/middleware.py 查看文件

@@ -55,7 +55,7 @@ class BasketMiddleware(object):
55 55
         
56 56
         This is its own method to allow it to be overridden
57 57
         """
58
-        master.merge(slave)    
58
+        master.merge(slave, add_quantities=False)
59 59
         
60 60
     def process_response(self, request, response):
61 61
         # Delete any surplus cookies

+ 1
- 1
oscar/apps/basket/signals.py 查看文件

@@ -1,4 +1,4 @@
1 1
 import django.dispatch
2 2
 
3 3
 basket_addition = django.dispatch.Signal(providing_args=["product", "user"])
4
-basket_voucher = django.dispatch.Signal(providing_args=["basket", "voucher"])
4
+voucher_addition = django.dispatch.Signal(providing_args=["basket", "voucher"])

+ 7
- 1
oscar/apps/basket/views.py 查看文件

@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
7 7
 from django.core.exceptions import ObjectDoesNotExist
8 8
 
9 9
 from extra_views import ModelFormSetView
10
-from oscar.apps.basket.signals import basket_addition
10
+from oscar.apps.basket.signals import basket_addition, voucher_addition
11 11
 from oscar.core.loading import get_class, get_classes
12 12
 Applicator = get_class('offer.utils', 'Applicator')
13 13
 BasketLineForm, AddToBasketForm, BasketVoucherForm, \
@@ -169,6 +169,7 @@ class BasketAddView(FormView):
169 169
 class VoucherAddView(FormView):
170 170
     form_class = BasketVoucherForm
171 171
     voucher_model = get_model('voucher', 'voucher')
172
+    add_signal = voucher_addition
172 173
 
173 174
     def get(self, request, *args, **kwargs):
174 175
         return HttpResponseRedirect(reverse('basket:summary'))
@@ -185,6 +186,11 @@ class VoucherAddView(FormView):
185 186
 
186 187
         self.request.basket.vouchers.add(voucher)
187 188
 
189
+        # Raise signal
190
+        self.add_signal.send(sender=self, 
191
+                             basket=self.request.basket, 
192
+                             voucher=voucher)
193
+
188 194
         # Recalculate discounts to see if the voucher gives any
189 195
         discounts_before = self.request.basket.get_discounts()
190 196
         self.request.basket.remove_discounts()

+ 2
- 0
oscar/apps/catalogue/signals.py 查看文件

@@ -1,4 +1,6 @@
1 1
 import django.dispatch
2 2
 
3 3
 product_viewed = django.dispatch.Signal(providing_args=["product", "user", "request", "response"])
4
+
5
+# This needs to be moved into the search app when it is refactored
4 6
 product_search = django.dispatch.Signal(providing_args=["query", '"user'])

+ 3
- 2
oscar/apps/checkout/forms.py 查看文件

@@ -9,8 +9,9 @@ class ShippingAddressForm(forms.ModelForm):
9 9
         super(ShippingAddressForm,self ).__init__(*args, **kwargs)
10 10
         self.set_country_queryset() 
11 11
         
12
-    def set_country_queryset(self):    
13
-        self.fields['country'].queryset = get_model('address', 'country')._default_manager.filter(is_shipping_country=True)
12
+    def set_country_queryset(self):
13
+        self.fields['country'].queryset = get_model('address', 'country')._default_manager.filter(
14
+            is_shipping_country=True)
14 15
     
15 16
     class Meta:
16 17
         model = get_model('order', 'shippingaddress')

+ 1
- 2
oscar/apps/checkout/signals.py 查看文件

@@ -1,5 +1,4 @@
1 1
 from django.dispatch import Signal
2 2
 
3 3
 pre_payment = Signal(providing_args=["view"])
4
-post_payment = Signal(providing_args=["view"])
5
-order_placed = Signal(providing_args=["order"])
4
+post_payment = Signal(providing_args=["view"])

+ 5
- 17
oscar/apps/checkout/utils.py 查看文件

@@ -71,20 +71,6 @@ class CheckoutSessionData(object):
71 71
     def reset_shipping_data(self):
72 72
         self._flush_namespace('shipping')
73 73
 
74
-    def no_shipping_required(self):
75
-        """
76
-        Record fact that basket doesn't require a shipping address or method
77
-        """
78
-        self.reset_shipping_data()
79
-        self._set('shipping', 'is_required', False)
80
-
81
-    def shipping_required(self):
82
-        """
83
-        Record fact that basket does require a shipping address or method
84
-        """
85
-        self.reset_shipping_data()
86
-        self._set('shipping', 'is_required', True)
87
-
88 74
     def ship_to_user_address(self, address):
89 75
         """
90 76
         Set existing shipping address id to session and unset address fields from session
@@ -112,10 +98,12 @@ class CheckoutSessionData(object):
112 98
         return self._get('shipping', 'user_address_id')
113 99
     user_address_id = shipping_user_address_id
114 100
 
115
-    def is_shipping_required(self):
116
-        return self._get('shipping', 'is_required', True)
117
-
118 101
     def is_shipping_address_set(self):
102
+        """
103
+        Test whether a shipping address has been stored in the session.
104
+
105
+        This can be from a new address or re-using an existing address.
106
+        """
119 107
         new_fields = self.new_shipping_address_fields()
120 108
         has_new_address = new_fields is not None
121 109
         has_old_address = self.user_address_id() > 0

+ 27
- 15
oscar/apps/checkout/views.py 查看文件

@@ -176,6 +176,11 @@ class ShippingAddressView(CheckoutSessionMixin, FormView):
176 176
     form_class = ShippingAddressForm
177 177
 
178 178
     def get(self, request, *args, **kwargs):
179
+        # Check that the user's basket is not empty
180
+        if request.basket.is_empty:
181
+            messages.error(request, _("You need to add some items to your basket to checkout"))
182
+            return HttpResponseRedirect(reverse('basket:summary'))
183
+
179 184
         # Check that guests have entered an email address
180 185
         if not request.user.is_authenticated() and not self.checkout_session.get_guest_email():
181 186
             messages.error(request, _("Please either sign in or enter your email address"))
@@ -183,21 +188,12 @@ class ShippingAddressView(CheckoutSessionMixin, FormView):
183 188
 
184 189
         # Check to see that a shipping address is actually required.  It may not be if
185 190
         # the basket is purely downloads
186
-        if not self.does_basket_require_shipping(request.basket):
191
+        if not request.basket.is_shipping_required():
187 192
             messages.info(request, _("Your basket does not require a shipping address to be submitted"))
188
-            self.checkout_session.no_shipping_required()
189 193
             return HttpResponseRedirect(self.get_success_url())
190
-        else:
191
-            self.checkout_session.shipping_required()
192 194
 
193 195
         return super(ShippingAddressView, self).get(request, *args, **kwargs)
194 196
 
195
-    def does_basket_require_shipping(self, basket):
196
-        """
197
-        Test whether the contents of the basket require shipping
198
-        """
199
-        return basket.is_shipping_required()
200
-
201 197
     def get_initial(self):
202 198
         return self.checkout_session.new_shipping_address_fields()
203 199
 
@@ -318,8 +314,13 @@ class ShippingMethodView(CheckoutSessionMixin, TemplateView):
318 314
     template_name = 'checkout/shipping_methods.html';
319 315
 
320 316
     def get(self, request, *args, **kwargs):
317
+        # Check that the user's basket is not empty
318
+        if request.basket.is_empty:
319
+            messages.error(request, _("You need to add some items to your basket to checkout"))
320
+            return HttpResponseRedirect(reverse('basket:summary'))
321
+
321 322
         # Check that shipping is required at all
322
-        if not self.checkout_session.is_shipping_required():
323
+        if not request.basket.is_shipping_required():
323 324
             self.checkout_session.use_shipping_method(NoShippingRequired().code)
324 325
             return self.get_success_response()
325 326
 
@@ -394,10 +395,16 @@ class PaymentMethodView(CheckoutSessionMixin, TemplateView):
394 395
     """
395 396
 
396 397
     def get(self, request, *args, **kwargs):
398
+        # Check that the user's basket is not empty
399
+        if request.basket.is_empty:
400
+            messages.error(request, _("You need to add some items to your basket to checkout"))
401
+            return HttpResponseRedirect(reverse('basket:summary'))
402
+
397 403
         # Check that shipping address has been completed
398
-        if self.checkout_session.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
404
+        if request.basket.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
399 405
             messages.error(request, _("Please choose a shipping address"))
400 406
             return HttpResponseRedirect(reverse('checkout:shipping-address'))
407
+
401 408
         # Check that shipping method has been set
402 409
         if not self.checkout_session.is_shipping_method_set():
403 410
             messages.error(request, _("Please choose a shipping method"))
@@ -513,7 +520,7 @@ class OrderPlacementMixin(CheckoutSessionMixin):
513 520
         If the shipping address was selected from the user's address book,
514 521
         then we convert the UserAddress to a ShippingAddress.
515 522
         """
516
-        if not self.checkout_session.is_shipping_required():
523
+        if not self.request.basket.is_shipping_required():
517 524
             return None
518 525
 
519 526
         addr_data = self.checkout_session.new_shipping_address_fields()
@@ -670,8 +677,12 @@ class PaymentDetailsView(OrderPlacementMixin, TemplateView):
670 677
         return [self.template_name_preview] if self.preview else [self.template_name]
671 678
 
672 679
     def get_error_response(self):
680
+        # Check that the user's basket is not empty
681
+        if self.request.basket.is_empty:
682
+            messages.error(self.request, _("You need to add some items to your basket to checkout"))
683
+            return HttpResponseRedirect(reverse('basket:summary'))
673 684
         # Check that shipping address has been completed
674
-        if self.checkout_session.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
685
+        if self.request.basket.is_shipping_required() and not self.checkout_session.is_shipping_address_set():
675 686
             messages.error(self.request, _("Please choose a shipping address"))
676 687
             return HttpResponseRedirect(reverse('checkout:shipping-address'))
677 688
         # Check that shipping method has been set
@@ -692,6 +703,7 @@ class PaymentDetailsView(OrderPlacementMixin, TemplateView):
692 703
         then the method can call submit()
693 704
         """
694 705
         error_response = self.get_error_response()
706
+        
695 707
         if error_response:
696 708
             return error_response
697 709
         if self.preview:
@@ -761,7 +773,7 @@ class PaymentDetailsView(OrderPlacementMixin, TemplateView):
761 773
         # Next, check that basket isn't empty
762 774
         if basket.is_empty:
763 775
             messages.error(self.request, _("This order cannot be submitted as the basket is empty"))
764
-            url = self.request.META.get('HTTP_REFERER', reverse('checkout:shipping-address'))
776
+            url = self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
765 777
             return HttpResponseRedirect(url)
766 778
 
767 779
         # Domain-specific checks on the basket

+ 2
- 2
oscar/apps/dashboard/catalogue/views.py 查看文件

@@ -118,7 +118,7 @@ class ProductCreateView(generic.CreateView):
118 118
         image_formset = ProductImageFormSet(self.request.POST,
119 119
                                             self.request.FILES,
120 120
                                             instance=product)
121
-        if stockrecord_form.is_valid() and category_formset.is_valid() and image_formset.is_valid():
121
+        if all([stockrecord_form.is_valid(), category_formset.is_valid(), image_formset.is_valid()]):
122 122
             # Save product
123 123
             product.save()
124 124
             # Save stock record
@@ -192,7 +192,7 @@ class ProductUpdateView(generic.UpdateView):
192 192
         image_formset = ProductImageFormSet(self.request.POST,
193 193
                                             self.request.FILES,
194 194
                                             instance=self.object)
195
-        if stockrecord_form.is_valid() and category_formset.is_valid() and image_formset.is_valid():
195
+        if all([stockrecord_form.is_valid(), category_formset.is_valid(), image_formset.is_valid()]):
196 196
             form.save()
197 197
             stockrecord_form.save()
198 198
             category_formset.save()

+ 9
- 4
oscar/apps/partner/abstract_models.py 查看文件

@@ -191,22 +191,27 @@ class AbstractStockRecord(models.Model):
191 191
     @property
192 192
     def availability_code(self):
193 193
         """
194
-        Return an item's availability as a code for use in CSS
194
+        Return an product's availability as a code for use in CSS to add icons
195
+        to the overall availability mark-up.  For example, "instock",
196
+        "unavailable".
195 197
         """
196 198
         return get_partner_wrapper(self.partner.name).availability_code(self)
197 199
     
198 200
     @property
199 201
     def availability(self):
200 202
         """
201
-        Return an item's availability as a string
203
+        Return a product's availability as a string that can be displayed to the
204
+        user.  For example, "In stock", "Unavailabl".
202 205
         """
203 206
         return get_partner_wrapper(self.partner.name).availability(self)
204 207
 
205
-    def max_purchase_quantity(self, user):
208
+    def max_purchase_quantity(self, user=None):
206 209
         """
207 210
         Return an item's availability as a string
211
+
212
+        :param user: (optional) The user who wants to purchase
208 213
         """
209
-        return get_partner_wrapper(self.partner.name).availability(self)
214
+        return get_partner_wrapper(self.partner.name).max_purchase_quantity(self, user)
210 215
     
211 216
     @property
212 217
     def dispatch_date(self):

+ 7
- 0
oscar/apps/partner/wrappers.py 查看文件

@@ -47,6 +47,8 @@ class DefaultWrapper(object):
47 47
         Return a code for the availability of this product.
48 48
 
49 49
         This is normally used within CSS to add icons to stock messages
50
+
51
+        :param oscar.apps.partner.models.StockRecord stockrecord: stockrecord instance
50 52
         """
51 53
         if stockrecord.net_stock_level > 0:
52 54
             return 'instock'
@@ -55,6 +57,11 @@ class DefaultWrapper(object):
55 57
         return 'outofstock'
56 58
     
57 59
     def availability(self, stockrecord):
60
+        """
61
+        Return an availability message for the passed stockrecord.
62
+
63
+        :param oscar.apps.partner.models.StockRecord stockrecord: stockrecord instance
64
+        """
58 65
         if stockrecord.net_stock_level > 0:
59 66
             return _("In stock (%d available)" % stockrecord.net_stock_level)
60 67
         if self.is_available_to_buy(stockrecord):

+ 4
- 4
oscar/apps/shipping/repository.py 查看文件

@@ -7,16 +7,16 @@ class Repository(object):
7 7
     Repository class responsible for returning ShippingMethod
8 8
     objects for a given user, basket etc
9 9
     """
10
-    
10
+
11 11
     def get_shipping_methods(self, user, basket, shipping_addr=None, **kwargs):
12 12
         """
13 13
         Return a list of all applicable shipping method objects
14 14
         for a given basket.
15
-        
15
+
16 16
         We default to returning the Method models that have been defined but
17 17
         this behaviour can easily be overridden by subclassing this class
18 18
         and overriding this method.
19
-        """ 
19
+        """
20 20
         methods = [Free()]
21 21
         return self.add_basket_to_methods(basket, methods)
22 22
 
@@ -24,7 +24,7 @@ class Repository(object):
24 24
         methods = self.get_shipping_methods(user, basket, shipping_addr, **kwargs)
25 25
         if len(methods) == 0:
26 26
             raise ImproperlyConfigured("You need to define some shipping methods")
27
-        return methods[0]
27
+        return min(methods, key=lambda method: method.basket_charge_incl_tax())
28 28
 
29 29
     def add_basket_to_methods(self, basket, methods):
30 30
         for method in methods:

+ 5
- 1
oscar/apps/voucher/abstract_models.py 查看文件

@@ -90,7 +90,10 @@ class AbstractVoucher(models.Model):
90 90
         """
91 91
         Records a usage of this voucher in an order.
92 92
         """
93
-        self.applications.create(voucher=self, order=order, user=user)
93
+        if user.is_authenticated():
94
+            self.applications.create(voucher=self, order=order, user=user)
95
+        else:
96
+            self.applications.create(voucher=self, order=order)
94 97
 
95 98
     @property
96 99
     def benefit(self):
@@ -102,6 +105,7 @@ class AbstractVoucherApplication(models.Model):
102 105
     For tracking how often a voucher has been used
103 106
     """
104 107
     voucher = models.ForeignKey('voucher.Voucher', related_name="applications")
108
+
105 109
     # It is possible for an anonymous user to apply a voucher so we need to allow
106 110
     # the user to be nullable
107 111
     user = models.ForeignKey('auth.User', blank=True, null=True)

+ 5
- 1
oscar/templates/customer/anon-order.html 查看文件

@@ -8,8 +8,12 @@
8 8
 </div>
9 9
 {% endblock header %}
10 10
 
11
-
12 11
 {% block content %}
12
+<div class="sub-header">
13
+    <h3>Status</h3>
14
+</div>
15
+<p>{{ order.status }}</p>
16
+
13 17
 <div class="sub-header">
14 18
     <h3>Shipping address</h3>
15 19
 </div>

+ 12
- 8
oscar/templates/dashboard/orders/order_detail.html 查看文件

@@ -31,7 +31,7 @@ Order {{ order.number }} | {{ block.super }}
31 31
 
32 32
     <h3>Customer Information</h3>
33 33
 	{% if order.user %}
34
-	
34
+
35 35
     <div class="row-fluid">
36 36
         <div class="span4">
37 37
             <div class="well well-info">
@@ -85,12 +85,14 @@ Order {{ order.number }} | {{ block.super }}
85 85
 
86 86
 <div class="tabbable dashboard">
87 87
 	<ul class="nav nav-tabs">
88
+	{% block nav_tabs %}
88 89
 		<li class="active"><a href="#lines" data-toggle="tab">Order contents</a></li>
89 90
 		<li><a href="#shipping" data-toggle="tab">Shipping</a></li>
90 91
 		<li><a href="#payment" data-toggle="tab">Payment</a></li>
91 92
 		<li><a href="#discounts" data-toggle="tab">Discounts</a></li>
92 93
 		<li><a href="#emails" data-toggle="tab">Emails</a></li>
93 94
 		<li><a href="#notes" data-toggle="tab">Notes</a></li>
95
+	{% endblock nav_tabs %}
94 96
 	</ul>
95 97
 
96 98
 	<div class="tab-content">
@@ -207,7 +209,7 @@ Order {{ order.number }} | {{ block.super }}
207 209
 							</select>
208 210
 						</label>
209 211
 						<label class="radio inline">
210
-							with amount <input type="text" name="amount" value="" /> 
212
+							with amount <input type="text" name="amount" value="" />
211 213
 						</label>
212 214
 					</div>
213 215
 				</div>
@@ -338,16 +340,16 @@ Order {{ order.number }} | {{ block.super }}
338 340
 				<h3 class="app-ico ico_mapmarker icon">Payment sources</h3>
339 341
 			</div>
340 342
 			{% if sources %}
341
-				<table class="table table-striped table-bordered">    
342
-					<thead>  
343
+				<table class="table table-striped table-bordered">
344
+					<thead>
343 345
 						<tr>
344 346
 							<th>Source</th>
345 347
 							<th>Allocation</th>
346 348
 							<th>Amount debited</th>
347 349
 							<th>Amount refunded</th>
348
-						</tr>  
349
-					</thead>	
350
-					<tbody> 
350
+						</tr>
351
+					</thead>
352
+					<tbody>
351 353
 						{% for source in sources %}
352 354
 						<tr>
353 355
 							<td>{{ source.source_type }}</td>
@@ -356,7 +358,7 @@ Order {{ order.number }} | {{ block.super }}
356 358
 							<td>{{ source.amount_refunded|currency }}</td>
357 359
 						</tr>
358 360
 						{% endfor %}
359
-					</tbody>	
361
+					</tbody>
360 362
 				</table>
361 363
 			{% else %}
362 364
 				<p>No payment sources</p>
@@ -463,6 +465,8 @@ Order {{ order.number }} | {{ block.super }}
463 465
 			</form>
464 466
 			{% endblock %}
465 467
 		</div>
468
+
469
+		{% block extra_tabs %}{% endblock %}
466 470
 	</div>
467 471
 </div>
468 472
 

+ 10
- 2
oscar/templates/dashboard/reviews/review_list.html 查看文件

@@ -50,10 +50,18 @@ Reviews | {{ block.super }}
50 50
         <tr>
51 51
             <td><input type="checkbox" name="selected_review" class="selected_review" value="{{ review.id }}"/>
52 52
                 <td>
53
-                    <a href="{% url catalogue:reviews-detail review.product.slug review.product.id review.id %}">{{ review.title }}</a>
53
+					{% if review.product %}
54
+						<a href="{% url catalogue:reviews-detail review.product.slug review.product.id review.id %}">{{ review.title }}</a>
55
+					{% else %}
56
+						{{ review.title }}
57
+					{% endif %}
54 58
                 </td>
55 59
             <td>
56
-                <a href='{% url catalogue:detail review.product.slug review.product.id %}'>{{ review.product.title }}</a> </td>
60
+				{% if review.product %}
61
+					<a href='{% url catalogue:detail review.product.slug review.product.id %}'>{{ review.product.title }}</a> </td>
62
+				{% else %}
63
+					[Product deleted]
64
+				{% endif %}
57 65
             <td>
58 66
 				{% if review.user %}
59 67
                 <a href="{% url dashboard:user-detail review.user.id %}">{{ review.get_reviewer_name }}</a>

+ 1
- 1
oscar/templates/dashboard/vouchers/voucher_delete.html 查看文件

@@ -43,7 +43,7 @@ Delete voucher '{{ voucher.name }}'? | Vouchers | {{ block.super }}
43 43
 		{% csrf_token %}
44 44
 		<div class="form-actions">
45 45
 			<button class="btn btn-danger btn-large" type="submit">Delete</button> or
46
-			<a href="{% url dashboard:range-list %}">cancel</a>
46
+			<a href="{% url dashboard:voucher-list %}">cancel</a>
47 47
 		</div>
48 48
 	</form>
49 49
 

+ 5
- 1
oscar/test/__init__.py 查看文件

@@ -4,6 +4,7 @@ from contextlib import contextmanager
4 4
 from django.test import TestCase
5 5
 from django.test.client import Client
6 6
 from django.contrib.auth.models import User
7
+from django.core.urlresolvers import reverse
7 8
 from purl import URL
8 9
 
9 10
 
@@ -51,7 +52,10 @@ class ClientTestCase(TestCase):
51 52
             location = URL.from_string(response['Location'])
52 53
             self.assertEqual(expected_url, location.path())
53 54
 
54
-
55
+    def assertRedirectUrlName(self, response, name):
56
+        self.assertIsRedirect(response)
57
+        location = response['Location'].replace('http://testserver', '')
58
+        self.assertEqual(location, reverse(name))
55 59
 
56 60
     def assertIsOk(self, response):
57 61
         self.assertEqual(httplib.OK, response.status_code)

+ 16
- 55
oscar/test/helpers.py 查看文件

@@ -1,11 +1,6 @@
1
-from StringIO import StringIO
2 1
 from decimal import Decimal as D
3 2
 import random
4
-
5
-from django.test import TestCase
6
-from django.core.servers.basehttp import AdminMediaHandler
7
-from django.core.handlers.wsgi import WSGIHandler
8
-from django.core.urlresolvers import reverse
3
+import datetime
9 4
 
10 5
 from oscar.apps.basket.models import Basket
11 6
 from oscar.apps.catalogue.models import ProductClass, Product, ProductAttribute, ProductAttributeValue
@@ -14,6 +9,7 @@ from oscar.apps.order.utils import OrderCreator
14 9
 from oscar.apps.partner.models import Partner, StockRecord
15 10
 from oscar.apps.shipping.methods import Free
16 11
 from oscar.apps.offer.models import Range, ConditionalOffer, Condition, Benefit
12
+from oscar.apps.voucher.models import Voucher
17 13
 
18 14
 
19 15
 def create_product(price=None, title="Dummy title", product_class="Dummy item class",
@@ -72,6 +68,9 @@ def create_order(number=None, basket=None, user=None, shipping_address=None, shi
72 68
 
73 69
 
74 70
 def create_offer():
71
+    """
72
+    Helper method for creating an offer
73
+    """
75 74
     range = Range.objects.create(name="All products range", includes_all_products=True)
76 75
     condition = Condition.objects.create(range=range,
77 76
                                          type=Condition.COUNT,
@@ -79,7 +78,7 @@ def create_offer():
79 78
     benefit = Benefit.objects.create(range=range,
80 79
                                      type=Benefit.PERCENTAGE,
81 80
                                      value=20)
82
-    offer= ConditionalOffer.objects.create(
81
+    offer = ConditionalOffer.objects.create(
83 82
         name='Dummy offer',
84 83
         offer_type='Site',
85 84
         condition=condition,
@@ -88,53 +87,15 @@ def create_offer():
88 87
     return offer
89 88
 
90 89
 
91
-
92
-
93
-class TwillTestCase(TestCase):
90
+def create_voucher():
94 91
     """
95
-    Simple wrapper around Twill to make writing TestCases easier.
96
-
97
-    Commands availabel through self.command are:
98
-    - go        -> visit a URL
99
-    - back      -> back to previous URL
100
-    - reload    -> reload URL
101
-    - follow    -> follow a given link
102
-    - code      -> assert the HTTP response code
103
-    - find      -> assert page contains some string
104
-    - notfind   -> assert page does not contain
105
-    - title     -> assert page title
92
+    Helper method for creating a voucher
106 93
     """
107
-
108
-    HOST = '127.0.0.1'
109
-    PORT = 8080
110
-
111
-    def setUp(self):
112
-        app = AdminMediaHandler(WSGIHandler())
113
-        twill.add_wsgi_intercept(self.HOST, self.PORT, lambda: app)
114
-        twill.set_output(StringIO())
115
-        self.command = twill.commands
116
-
117
-    def tearDown(self):
118
-        twill.remove_wsgi_intercept(self.HOST, self.PORT)
119
-
120
-    def reverse(self, url_name, *args, **kwargs):
121
-        """
122
-        Custom 'reverse' function that includes the protocol and host
123
-        """
124
-        return 'http://%s:%d%s' % (self.HOST, self.PORT, reverse(url_name, *args, **kwargs))
125
-
126
-    def visit(self, url_name, *args,**kwargs):
127
-        self.command.go(self.reverse(url_name, *args, **kwargs))
128
-
129
-    def assertResponseCodeIs(self, code):
130
-        self.command.code(code)
131
-
132
-    def assertPageContains(self, regexp):
133
-        self.command.find(regexp)
134
-
135
-    def assertPageDoesNotContain(self, regexp):
136
-        self.command.notfind(regexp)
137
-
138
-    def assertPageTitleMatches(self, regexp):
139
-        self.command.title(regexp)
140
-
94
+    voucher = Voucher.objects.create(
95
+        name="Test voucher",
96
+        code="test",
97
+        start_date=datetime.date.today(),
98
+        end_date=datetime.date.today() + datetime.timedelta(days=12)
99
+    )
100
+    voucher.offers.add(create_offer())
101
+    return voucher

+ 2
- 1
tests/config.py 查看文件

@@ -3,6 +3,7 @@ import os
3 3
 from django.conf import settings, global_settings
4 4
 from oscar import OSCAR_CORE_APPS
5 5
 
6
+
6 7
 def configure():
7 8
     if not settings.configured:
8 9
         from oscar.defaults import OSCAR_SETTINGS
@@ -55,6 +56,6 @@ def configure():
55 56
             HAYSTACK_SEARCH_ENGINE='dummy',
56 57
             HAYSTACK_SITECONF = 'oscar.search_sites',
57 58
             APPEND_SLASH=True,
58
-            NOSE_ARGS=['-s', '-x', '--with-spec'],
59
+            NOSE_ARGS=['-s', '-x'],
59 60
             **OSCAR_SETTINGS
60 61
         )

+ 44
- 0
tests/functional/basket_tests.py 查看文件

@@ -1,5 +1,6 @@
1 1
 from decimal import Decimal as D
2 2
 import httplib
3
+import datetime
3 4
 
4 5
 from django.contrib.auth.models import User
5 6
 from django.conf import settings
@@ -8,6 +9,28 @@ from django.core.urlresolvers import reverse
8 9
 
9 10
 from oscar.test.helpers import create_product
10 11
 from oscar.apps.basket.models import Basket
12
+from oscar.apps.basket import reports
13
+
14
+
15
+class BasketMergingTests(TestCase):
16
+
17
+    def setUp(self):
18
+        self.product = create_product()
19
+        self.user_basket = Basket()
20
+        self.user_basket.add_product(self.product)
21
+        self.cookie_basket = Basket()
22
+        self.cookie_basket.add_product(self.product, 2)
23
+        self.user_basket.merge(self.cookie_basket, add_quantities=False)
24
+
25
+    def test_cookie_basket_has_status_set(self):
26
+        self.assertEqual('Merged', self.cookie_basket.status)
27
+
28
+    def test_lines_are_moved_across(self):
29
+        self.assertEqual(1, self.user_basket.lines.all().count())
30
+
31
+    def test_merge_line_takes_max_quantity(self):
32
+        line = self.user_basket.lines.get(product=self.product)
33
+        self.assertEqual(2, line.quantity)
11 34
 
12 35
 
13 36
 class AnonAddToBasketViewTests(TestCase):
@@ -82,6 +105,27 @@ class BasketThresholdTest(TestCase):
82 105
                         response.cookies['messages'].value)
83 106
 
84 107
 
108
+class BasketReportTests(TestCase):
109
+
110
+    def test_open_report_doesnt_error(self):
111
+        data = {
112
+            'start_date': datetime.date(2012, 5, 1),
113
+            'end_date': datetime.date(2012, 5, 17),
114
+            'formatter': 'CSV'
115
+        }
116
+        generator = reports.OpenBasketReportGenerator(**data)
117
+        generator.generate()
118
+
119
+    def test_submitted_report_doesnt_error(self):
120
+        data = {
121
+            'start_date': datetime.date(2012, 5, 1),
122
+            'end_date': datetime.date(2012, 5, 17),
123
+            'formatter': 'CSV'
124
+        }
125
+        generator = reports.SubmittedBasketReportGenerator(**data)
126
+        generator.generate()
127
+
128
+
85 129
 class SavedBasketTests(TestCase):
86 130
 
87 131
     def test_moving_from_saved_basket(self):

+ 75
- 12
tests/functional/checkout_tests.py 查看文件

@@ -5,14 +5,24 @@ from django.core.urlresolvers import reverse
5 5
 from django.conf import settings
6 6
 from django.utils.importlib import import_module
7 7
 
8
-from oscar.test.helpers import create_product
8
+from oscar.test.helpers import create_product, create_voucher
9 9
 from oscar.test import ClientTestCase, patch_settings
10 10
 from oscar.apps.basket.models import Basket
11 11
 from oscar.apps.order.models import Order
12
+from oscar.apps.address.models import Country
12 13
 
13 14
 
14 15
 class CheckoutMixin(object):
15
-    fixtures = ['countries.json']
16
+
17
+    def add_product_to_basket(self):
18
+        product = create_product(price=D('12.00'))
19
+        self.client.post(reverse('basket:add'), {'product_id': product.id,
20
+                                                 'quantity': 1})
21
+
22
+    def add_voucher_to_basket(self):
23
+        voucher = create_voucher()
24
+        self.client.post(reverse('basket:vouchers-add'),
25
+                         {'code': voucher.code})
16 26
 
17 27
     def complete_guest_email_form(self, email='test@example.com'):
18 28
         response = self.client.post(reverse('checkout:index'),
@@ -21,6 +31,10 @@ class CheckoutMixin(object):
21 31
         self.assertIsRedirect(response)
22 32
 
23 33
     def complete_shipping_address(self):
34
+        Country.objects.get_or_create(
35
+            iso_3166_1_a2='GB',
36
+            is_shipping_country=True
37
+        )
24 38
         response = self.client.post(reverse('checkout:shipping-address'),
25 39
                                      {'last_name': 'Doe',
26 40
                                       'line1': '1 Egg Street',
@@ -32,9 +46,8 @@ class CheckoutMixin(object):
32 46
     def complete_shipping_method(self):
33 47
         self.client.get(reverse('checkout:shipping-method'))
34 48
 
35
-    def assertRedirectUrlName(self, response, name):
36
-        location = response['Location'].replace('http://testserver', '')
37
-        self.assertEqual(location, reverse(name))
49
+    def submit(self):
50
+        return self.client.post(reverse('checkout:preview'), {'action': 'place_order'})
38 51
 
39 52
 
40 53
 class DisabledAnonymousCheckoutViewsTests(ClientTestCase):
@@ -96,8 +109,13 @@ class EnabledAnonymousCheckoutViewsTests(ClientTestCase, CheckoutMixin):
96 109
             self.assertEqual('barry@example.com', order.guest_email)
97 110
 
98 111
 
99
-class ShippingAddressViewTests(ClientTestCase):
112
+class TestShippingAddressView(ClientTestCase, CheckoutMixin):
100 113
     fixtures = ['countries.json']
114
+
115
+    def test_pages_returns_200(self):
116
+        self.add_product_to_basket()
117
+        response = self.client.get(reverse('checkout:shipping-address'))
118
+        self.assertIsOk(response)
101 119
     
102 120
     def test_anon_checkout_disabled_by_default(self):
103 121
         self.assertFalse(settings.OSCAR_ALLOW_ANON_CHECKOUT)
@@ -115,49 +133,72 @@ class ShippingAddressViewTests(ClientTestCase):
115 133
         self.assertEqual('1 Egg Street', session_address['line1'])
116 134
         self.assertEqual('N1 9RT', session_address['postcode'])
117 135
 
136
+    def test_user_must_have_a_nonempty_basket(self):
137
+        response = self.client.get(reverse('checkout:shipping-address'))
138
+        self.assertRedirectUrlName(response, 'basket:summary')
118 139
 
119
-class ShippingMethodViewTests(ClientTestCase, CheckoutMixin):
140
+
141
+class TestShippingMethodView(ClientTestCase, CheckoutMixin):
120 142
     fixtures = ['countries.json']
121 143
 
122 144
     def test_shipping_method_view_redirects_if_no_shipping_address(self):
145
+        self.add_product_to_basket()
123 146
         response = self.client.get(reverse('checkout:shipping-method'))
124 147
         self.assertIsRedirect(response)
125 148
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
126 149
 
127 150
     def test_redirects_by_default(self):
151
+        self.add_product_to_basket()
128 152
         self.complete_shipping_address()
129 153
         response = self.client.get(reverse('checkout:shipping-method'))
130 154
         self.assertRedirectUrlName(response, 'checkout:payment-method')
131 155
 
156
+    def test_user_must_have_a_nonempty_basket(self):
157
+        response = self.client.get(reverse('checkout:shipping-method'))
158
+        self.assertRedirectUrlName(response, 'basket:summary')
132 159
 
133
-class PaymentMethodViewTests(ClientTestCase, CheckoutMixin):
160
+
161
+class TestPaymentMethodView(ClientTestCase, CheckoutMixin):
134 162
 
135 163
     def test_view_redirects_if_no_shipping_address(self):
164
+        self.add_product_to_basket() 
136 165
         response = self.client.get(reverse('checkout:payment-method'))
137 166
         self.assertIsRedirect(response)
138 167
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
139 168
 
140 169
     def test_view_redirects_if_no_shipping_method(self):
170
+        self.add_product_to_basket() 
141 171
         self.complete_shipping_address()
142 172
         response = self.client.get(reverse('checkout:payment-method'))
143 173
         self.assertIsRedirect(response)
144 174
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
145 175
 
176
+    def test_user_must_have_a_nonempty_basket(self):
177
+        response = self.client.get(reverse('checkout:payment-method'))
178
+        self.assertRedirectUrlName(response, 'basket:summary')
179
+
146 180
 
147
-class PreviewViewTests(ClientTestCase, CheckoutMixin):
181
+class TestPreviewView(ClientTestCase, CheckoutMixin):
182
+
183
+    def test_user_must_have_a_nonempty_basket(self):
184
+        response = self.client.get(reverse('checkout:preview'))
185
+        self.assertRedirectUrlName(response, 'basket:summary')
148 186
 
149 187
     def test_view_redirects_if_no_shipping_address(self):
188
+        self.add_product_to_basket()
150 189
         response = self.client.get(reverse('checkout:preview'))
151 190
         self.assertIsRedirect(response)
152 191
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
153 192
 
154 193
     def test_view_redirects_if_no_shipping_method(self):
194
+        self.add_product_to_basket()
155 195
         self.complete_shipping_address()
156 196
         response = self.client.get(reverse('checkout:preview'))
157 197
         self.assertIsRedirect(response)
158 198
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
159 199
 
160 200
     def test_ok_response_if_previous_steps_complete(self):
201
+        self.add_product_to_basket()
161 202
         self.complete_shipping_address()
162 203
         self.complete_shipping_method()
163 204
         response = self.client.get(reverse('checkout:preview'))
@@ -166,23 +207,27 @@ class PreviewViewTests(ClientTestCase, CheckoutMixin):
166 207
 
167 208
 class PaymentDetailsViewTests(ClientTestCase, CheckoutMixin):
168 209
 
210
+    def test_user_must_have_a_nonempty_basket(self):
211
+        response = self.client.get(reverse('checkout:payment-details'))
212
+        self.assertRedirectUrlName(response, 'basket:summary')
213
+
169 214
     def test_view_redirects_if_no_shipping_address(self):
215
+        self.add_product_to_basket()
170 216
         response = self.client.post(reverse('checkout:payment-details'))
171 217
         self.assertIsRedirect(response)
172 218
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
173 219
 
174 220
     def test_view_redirects_if_no_shipping_method(self):
221
+        self.add_product_to_basket()
175 222
         self.complete_shipping_address()
176 223
         response = self.client.post(reverse('checkout:payment-details'))
177 224
         self.assertIsRedirect(response)
178 225
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
179 226
 
180 227
     def test_placing_order_with_empty_basket_redirects(self):
181
-        self.complete_shipping_address()
182
-        self.complete_shipping_method()
183 228
         response = self.client.post(reverse('checkout:preview'), {'action': 'place_order'})
184 229
         self.assertIsRedirect(response)
185
-        self.assertRedirectUrlName(response, 'checkout:shipping-address')
230
+        self.assertRedirectUrlName(response, 'basket:summary')
186 231
 
187 232
 
188 233
 class OrderPlacementTests(ClientTestCase, CheckoutMixin):
@@ -206,3 +251,21 @@ class OrderPlacementTests(ClientTestCase, CheckoutMixin):
206 251
         self.assertIsRedirect(self.response)
207 252
         orders = Order.objects.all()
208 253
         self.assertEqual(1, len(orders))
254
+        
255
+
256
+class TestAnonUserOrderPlacementScenarios(ClientTestCase, CheckoutMixin):
257
+
258
+    def test_basic_submission_gets_redirect_to_thankyou(self):
259
+        self.add_product_to_basket()
260
+        self.complete_shipping_address()
261
+        self.complete_shipping_method()
262
+        response = self.submit()
263
+        self.assertRedirectUrlName(response, 'checkout:thank-you')
264
+
265
+    def test_submission_using_voucher(self):
266
+        self.add_product_to_basket()
267
+        self.add_voucher_to_basket()
268
+        self.complete_shipping_address()
269
+        self.complete_shipping_method()
270
+        response = self.submit()
271
+        self.assertRedirectUrlName(response, 'checkout:thank-you')

+ 3
- 0
tests/unit/partner/model_tests.py 查看文件

@@ -58,6 +58,9 @@ class StockRecordTests(TestCase):
58 58
         self.assertEqual(0, self.stockrecord.num_allocated)
59 59
         self.assertEqual(10, self.stockrecord.num_in_stock)
60 60
 
61
+    def test_max_purchase_quantity(self):
62
+        self.assertEqual(10, self.stockrecord.max_purchase_quantity())
63
+
61 64
 
62 65
 class DefaultWrapperTests(TestCase):
63 66
 

Loading…
取消
儲存