浏览代码

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
 allows it to handle a wide range of e-commerce requirements, from large-scale B2C
10
 allows it to handle a wide range of e-commerce requirements, from large-scale B2C
11
 sites to complex B2B sites rich in domain-specific business logic.
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
 * `Official homepage`_ 
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
 * `Google Group`_ - the mailing list is django-oscar@googlegroups.com
20
 * `Google Group`_ - the mailing list is django-oscar@googlegroups.com
20
 * `Continuous integration homepage`_ on `travis-ci.org`_
21
 * `Continuous integration homepage`_ on `travis-ci.org`_
21
 * `Twitter account for news and updates`_
22
 * `Twitter account for news and updates`_
24
 .. image:: https://secure.travis-ci.org/tangentlabs/django-oscar.png
25
 .. image:: https://secure.travis-ci.org/tangentlabs/django-oscar.png
25
 
26
 
26
 .. _`Official homepage`: http://oscarcommerce.com
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
 .. _`Documentation`: http://django-oscar.readthedocs.org/en/latest/
29
 .. _`Documentation`: http://django-oscar.readthedocs.org/en/latest/
29
 .. _`readthedocs.org`: http://readthedocs.org
30
 .. _`readthedocs.org`: http://readthedocs.org
30
 .. _`Continuous integration homepage`: http://travis-ci.org/#!/tangentlabs/django-oscar 
31
 .. _`Continuous integration homepage`: http://travis-ci.org/#!/tangentlabs/django-oscar 
34
 .. _`Google Group`: https://groups.google.com/forum/?fromgroups#!forum/django-oscar
35
 .. _`Google Group`: https://groups.google.com/forum/?fromgroups#!forum/django-oscar
35
 
36
 
36
 Oscar was written by `David Winterbottom`_ (`@codeinthehole`_) and is developed
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
 Oscar is released under the permissive `New BSD license`_.
41
 Oscar is released under the permissive `New BSD license`_.
40
 
42
 
42
 .. _`@codeinthehole`: https://twitter.com/codeinthehole
44
 .. _`@codeinthehole`: https://twitter.com/codeinthehole
43
 .. _`Tangent Labs`: http://www.tangentlabs.co.uk
45
 .. _`Tangent Labs`: http://www.tangentlabs.co.uk
44
 .. _`New BSD license`: https://github.com/tangentlabs/django-oscar/blob/master/LICENSE
46
 .. _`New BSD license`: https://github.com/tangentlabs/django-oscar/blob/master/LICENSE
47
+.. _`Mirumee`: http://mirumee.com/
45
 
48
 
46
 Case studies
49
 Case studies
47
 ------------
50
 ------------

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

11
 # All configuration values have a default; values that are commented out
11
 # All configuration values have a default; values that are commented out
12
 # serve to show the default.
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
 # If extensions (or modules to document with autodoc) are in another directory,
14
 # If extensions (or modules to document with autodoc) are in another directory,
19
 # add these directories to sys.path here. If the directory is relative to the
15
 # add these directories to sys.path here. If the directory is relative to the
20
 # documentation root, use os.path.abspath to make it absolute, like shown here.
16
 # documentation root, use os.path.abspath to make it absolute, like shown here.
21
 #sys.path.insert(0, os.path.abspath('.'))
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
 # -- General configuration -----------------------------------------------------
25
 # -- General configuration -----------------------------------------------------
24
 
26
 
43
 
45
 
44
 # General information about the project.
46
 # General information about the project.
45
 project = u'django-oscar'
47
 project = u'django-oscar'
46
-copyright = u'2011, David Winterbottom'
48
+copyright = u'Tangent Labs'
47
 
49
 
48
 # The version info for the project you're documenting, acts as replacement for
50
 # The version info for the project you're documenting, acts as replacement for
49
 # |version| and |release|, also used in various other places throughout the
51
 # |version| and |release|, also used in various other places throughout the

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

39
 
39
 
40
 * Extension libraries available for PayPal, GoCardless, DataCash and more
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
 Oscar is developed by `Tangent Labs`_, a London-based digital agency.  It is
58
 Oscar is developed by `Tangent Labs`_, a London-based digital agency.  It is
43
 used in production in several applications to sell everything from beer mats to
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
 .. _`Tangent Labs`: http://www.tangentlabs.co.uk
62
 .. _`Tangent Labs`: http://www.tangentlabs.co.uk
47
 .. _`source is on Github`: https://github.com/tangentlabs/django-oscar
63
 .. _`source is on Github`: https://github.com/tangentlabs/django-oscar

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

1
 ================================
1
 ================================
2
 How to configure stock messaging
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
-=====================
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
 Signals
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
 adding functionality.
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
 order_placed
104
 order_placed
9
 ------------
105
 ------------
10
 
106
 
11
-.. data:: oscar.apps.order.order_placed
107
+.. data:: oscar.apps.order.signals.order_placed
12
     :class:
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
 Arguments sent with this signal:
112
 Arguments sent with this signal:
17
 
113
 

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

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

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

46
         )
46
         )
47
         return urlpatterns
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
         self.discounts = []
129
         self.discounts = []
130
         self._lines = None
130
         self._lines = None
131
 
131
 
132
-    def merge_line(self, line):
132
+    def merge_line(self, line, add_quantities=True):
133
         """
133
         """
134
         For transferring a line from another basket to this one.
134
         For transferring a line from another basket to this one.
135
 
135
 
142
             line.basket = self
142
             line.basket = self
143
             line.save()
143
             line.save()
144
         else:
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
             existing_line.save()
150
             existing_line.save()
148
             line.delete()
151
             line.delete()
149
 
152
 
150
-    def merge(self, basket):
153
+    def merge(self, basket, add_quantities=True):
151
         """
154
         """
152
         Merges another basket with this one.
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
         for line_to_merge in basket.all_lines():
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
         basket.status = MERGED
162
         basket.status = MERGED
157
         basket.date_merged = datetime.datetime.now()
163
         basket.date_merged = datetime.datetime.now()
158
         basket.save()
164
         basket.save()
189
         shipping.
195
         shipping.
190
         """
196
         """
191
         for line in self.all_lines():
197
         for line in self.all_lines():
192
-            if line.product.os_shipping_required:
198
+            if line.product.is_shipping_required:
193
                 return True
199
                 return True
194
         return False
200
         return False
195
 
201
 

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

55
         
55
         
56
         This is its own method to allow it to be overridden
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
     def process_response(self, request, response):
60
     def process_response(self, request, response):
61
         # Delete any surplus cookies
61
         # Delete any surplus cookies

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

1
 import django.dispatch
1
 import django.dispatch
2
 
2
 
3
 basket_addition = django.dispatch.Signal(providing_args=["product", "user"])
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
 from django.core.exceptions import ObjectDoesNotExist
7
 from django.core.exceptions import ObjectDoesNotExist
8
 
8
 
9
 from extra_views import ModelFormSetView
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
 from oscar.core.loading import get_class, get_classes
11
 from oscar.core.loading import get_class, get_classes
12
 Applicator = get_class('offer.utils', 'Applicator')
12
 Applicator = get_class('offer.utils', 'Applicator')
13
 BasketLineForm, AddToBasketForm, BasketVoucherForm, \
13
 BasketLineForm, AddToBasketForm, BasketVoucherForm, \
169
 class VoucherAddView(FormView):
169
 class VoucherAddView(FormView):
170
     form_class = BasketVoucherForm
170
     form_class = BasketVoucherForm
171
     voucher_model = get_model('voucher', 'voucher')
171
     voucher_model = get_model('voucher', 'voucher')
172
+    add_signal = voucher_addition
172
 
173
 
173
     def get(self, request, *args, **kwargs):
174
     def get(self, request, *args, **kwargs):
174
         return HttpResponseRedirect(reverse('basket:summary'))
175
         return HttpResponseRedirect(reverse('basket:summary'))
185
 
186
 
186
         self.request.basket.vouchers.add(voucher)
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
         # Recalculate discounts to see if the voucher gives any
194
         # Recalculate discounts to see if the voucher gives any
189
         discounts_before = self.request.basket.get_discounts()
195
         discounts_before = self.request.basket.get_discounts()
190
         self.request.basket.remove_discounts()
196
         self.request.basket.remove_discounts()

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

1
 import django.dispatch
1
 import django.dispatch
2
 
2
 
3
 product_viewed = django.dispatch.Signal(providing_args=["product", "user", "request", "response"])
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
 product_search = django.dispatch.Signal(providing_args=["query", '"user'])
6
 product_search = django.dispatch.Signal(providing_args=["query", '"user'])

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

9
         super(ShippingAddressForm,self ).__init__(*args, **kwargs)
9
         super(ShippingAddressForm,self ).__init__(*args, **kwargs)
10
         self.set_country_queryset() 
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
     class Meta:
16
     class Meta:
16
         model = get_model('order', 'shippingaddress')
17
         model = get_model('order', 'shippingaddress')

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

1
 from django.dispatch import Signal
1
 from django.dispatch import Signal
2
 
2
 
3
 pre_payment = Signal(providing_args=["view"])
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
     def reset_shipping_data(self):
71
     def reset_shipping_data(self):
72
         self._flush_namespace('shipping')
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
     def ship_to_user_address(self, address):
74
     def ship_to_user_address(self, address):
89
         """
75
         """
90
         Set existing shipping address id to session and unset address fields from session
76
         Set existing shipping address id to session and unset address fields from session
112
         return self._get('shipping', 'user_address_id')
98
         return self._get('shipping', 'user_address_id')
113
     user_address_id = shipping_user_address_id
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
     def is_shipping_address_set(self):
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
         new_fields = self.new_shipping_address_fields()
107
         new_fields = self.new_shipping_address_fields()
120
         has_new_address = new_fields is not None
108
         has_new_address = new_fields is not None
121
         has_old_address = self.user_address_id() > 0
109
         has_old_address = self.user_address_id() > 0

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

176
     form_class = ShippingAddressForm
176
     form_class = ShippingAddressForm
177
 
177
 
178
     def get(self, request, *args, **kwargs):
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
         # Check that guests have entered an email address
184
         # Check that guests have entered an email address
180
         if not request.user.is_authenticated() and not self.checkout_session.get_guest_email():
185
         if not request.user.is_authenticated() and not self.checkout_session.get_guest_email():
181
             messages.error(request, _("Please either sign in or enter your email address"))
186
             messages.error(request, _("Please either sign in or enter your email address"))
183
 
188
 
184
         # Check to see that a shipping address is actually required.  It may not be if
189
         # Check to see that a shipping address is actually required.  It may not be if
185
         # the basket is purely downloads
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
             messages.info(request, _("Your basket does not require a shipping address to be submitted"))
192
             messages.info(request, _("Your basket does not require a shipping address to be submitted"))
188
-            self.checkout_session.no_shipping_required()
189
             return HttpResponseRedirect(self.get_success_url())
193
             return HttpResponseRedirect(self.get_success_url())
190
-        else:
191
-            self.checkout_session.shipping_required()
192
 
194
 
193
         return super(ShippingAddressView, self).get(request, *args, **kwargs)
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
     def get_initial(self):
197
     def get_initial(self):
202
         return self.checkout_session.new_shipping_address_fields()
198
         return self.checkout_session.new_shipping_address_fields()
203
 
199
 
318
     template_name = 'checkout/shipping_methods.html';
314
     template_name = 'checkout/shipping_methods.html';
319
 
315
 
320
     def get(self, request, *args, **kwargs):
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
         # Check that shipping is required at all
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
             self.checkout_session.use_shipping_method(NoShippingRequired().code)
324
             self.checkout_session.use_shipping_method(NoShippingRequired().code)
324
             return self.get_success_response()
325
             return self.get_success_response()
325
 
326
 
394
     """
395
     """
395
 
396
 
396
     def get(self, request, *args, **kwargs):
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
         # Check that shipping address has been completed
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
             messages.error(request, _("Please choose a shipping address"))
405
             messages.error(request, _("Please choose a shipping address"))
400
             return HttpResponseRedirect(reverse('checkout:shipping-address'))
406
             return HttpResponseRedirect(reverse('checkout:shipping-address'))
407
+
401
         # Check that shipping method has been set
408
         # Check that shipping method has been set
402
         if not self.checkout_session.is_shipping_method_set():
409
         if not self.checkout_session.is_shipping_method_set():
403
             messages.error(request, _("Please choose a shipping method"))
410
             messages.error(request, _("Please choose a shipping method"))
513
         If the shipping address was selected from the user's address book,
520
         If the shipping address was selected from the user's address book,
514
         then we convert the UserAddress to a ShippingAddress.
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
             return None
524
             return None
518
 
525
 
519
         addr_data = self.checkout_session.new_shipping_address_fields()
526
         addr_data = self.checkout_session.new_shipping_address_fields()
670
         return [self.template_name_preview] if self.preview else [self.template_name]
677
         return [self.template_name_preview] if self.preview else [self.template_name]
671
 
678
 
672
     def get_error_response(self):
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
         # Check that shipping address has been completed
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
             messages.error(self.request, _("Please choose a shipping address"))
686
             messages.error(self.request, _("Please choose a shipping address"))
676
             return HttpResponseRedirect(reverse('checkout:shipping-address'))
687
             return HttpResponseRedirect(reverse('checkout:shipping-address'))
677
         # Check that shipping method has been set
688
         # Check that shipping method has been set
692
         then the method can call submit()
703
         then the method can call submit()
693
         """
704
         """
694
         error_response = self.get_error_response()
705
         error_response = self.get_error_response()
706
+        
695
         if error_response:
707
         if error_response:
696
             return error_response
708
             return error_response
697
         if self.preview:
709
         if self.preview:
761
         # Next, check that basket isn't empty
773
         # Next, check that basket isn't empty
762
         if basket.is_empty:
774
         if basket.is_empty:
763
             messages.error(self.request, _("This order cannot be submitted as the basket is empty"))
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
             return HttpResponseRedirect(url)
777
             return HttpResponseRedirect(url)
766
 
778
 
767
         # Domain-specific checks on the basket
779
         # Domain-specific checks on the basket

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

118
         image_formset = ProductImageFormSet(self.request.POST,
118
         image_formset = ProductImageFormSet(self.request.POST,
119
                                             self.request.FILES,
119
                                             self.request.FILES,
120
                                             instance=product)
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
             # Save product
122
             # Save product
123
             product.save()
123
             product.save()
124
             # Save stock record
124
             # Save stock record
192
         image_formset = ProductImageFormSet(self.request.POST,
192
         image_formset = ProductImageFormSet(self.request.POST,
193
                                             self.request.FILES,
193
                                             self.request.FILES,
194
                                             instance=self.object)
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
             form.save()
196
             form.save()
197
             stockrecord_form.save()
197
             stockrecord_form.save()
198
             category_formset.save()
198
             category_formset.save()

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

191
     @property
191
     @property
192
     def availability_code(self):
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
         return get_partner_wrapper(self.partner.name).availability_code(self)
198
         return get_partner_wrapper(self.partner.name).availability_code(self)
197
     
199
     
198
     @property
200
     @property
199
     def availability(self):
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
         return get_partner_wrapper(self.partner.name).availability(self)
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
         Return an item's availability as a string
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
     @property
216
     @property
212
     def dispatch_date(self):
217
     def dispatch_date(self):

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

47
         Return a code for the availability of this product.
47
         Return a code for the availability of this product.
48
 
48
 
49
         This is normally used within CSS to add icons to stock messages
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
         if stockrecord.net_stock_level > 0:
53
         if stockrecord.net_stock_level > 0:
52
             return 'instock'
54
             return 'instock'
55
         return 'outofstock'
57
         return 'outofstock'
56
     
58
     
57
     def availability(self, stockrecord):
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
         if stockrecord.net_stock_level > 0:
65
         if stockrecord.net_stock_level > 0:
59
             return _("In stock (%d available)" % stockrecord.net_stock_level)
66
             return _("In stock (%d available)" % stockrecord.net_stock_level)
60
         if self.is_available_to_buy(stockrecord):
67
         if self.is_available_to_buy(stockrecord):

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

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

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

90
         """
90
         """
91
         Records a usage of this voucher in an order.
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
     @property
98
     @property
96
     def benefit(self):
99
     def benefit(self):
102
     For tracking how often a voucher has been used
105
     For tracking how often a voucher has been used
103
     """
106
     """
104
     voucher = models.ForeignKey('voucher.Voucher', related_name="applications")
107
     voucher = models.ForeignKey('voucher.Voucher', related_name="applications")
108
+
105
     # It is possible for an anonymous user to apply a voucher so we need to allow
109
     # It is possible for an anonymous user to apply a voucher so we need to allow
106
     # the user to be nullable
110
     # the user to be nullable
107
     user = models.ForeignKey('auth.User', blank=True, null=True)
111
     user = models.ForeignKey('auth.User', blank=True, null=True)

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

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

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

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

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

50
         <tr>
50
         <tr>
51
             <td><input type="checkbox" name="selected_review" class="selected_review" value="{{ review.id }}"/>
51
             <td><input type="checkbox" name="selected_review" class="selected_review" value="{{ review.id }}"/>
52
                 <td>
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
                 </td>
58
                 </td>
55
             <td>
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
             <td>
65
             <td>
58
 				{% if review.user %}
66
 				{% if review.user %}
59
                 <a href="{% url dashboard:user-detail review.user.id %}">{{ review.get_reviewer_name }}</a>
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
 		{% csrf_token %}
43
 		{% csrf_token %}
44
 		<div class="form-actions">
44
 		<div class="form-actions">
45
 			<button class="btn btn-danger btn-large" type="submit">Delete</button> or
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
 		</div>
47
 		</div>
48
 	</form>
48
 	</form>
49
 
49
 

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

4
 from django.test import TestCase
4
 from django.test import TestCase
5
 from django.test.client import Client
5
 from django.test.client import Client
6
 from django.contrib.auth.models import User
6
 from django.contrib.auth.models import User
7
+from django.core.urlresolvers import reverse
7
 from purl import URL
8
 from purl import URL
8
 
9
 
9
 
10
 
51
             location = URL.from_string(response['Location'])
52
             location = URL.from_string(response['Location'])
52
             self.assertEqual(expected_url, location.path())
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
     def assertIsOk(self, response):
60
     def assertIsOk(self, response):
57
         self.assertEqual(httplib.OK, response.status_code)
61
         self.assertEqual(httplib.OK, response.status_code)

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

1
-from StringIO import StringIO
2
 from decimal import Decimal as D
1
 from decimal import Decimal as D
3
 import random
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
 from oscar.apps.basket.models import Basket
5
 from oscar.apps.basket.models import Basket
11
 from oscar.apps.catalogue.models import ProductClass, Product, ProductAttribute, ProductAttributeValue
6
 from oscar.apps.catalogue.models import ProductClass, Product, ProductAttribute, ProductAttributeValue
14
 from oscar.apps.partner.models import Partner, StockRecord
9
 from oscar.apps.partner.models import Partner, StockRecord
15
 from oscar.apps.shipping.methods import Free
10
 from oscar.apps.shipping.methods import Free
16
 from oscar.apps.offer.models import Range, ConditionalOffer, Condition, Benefit
11
 from oscar.apps.offer.models import Range, ConditionalOffer, Condition, Benefit
12
+from oscar.apps.voucher.models import Voucher
17
 
13
 
18
 
14
 
19
 def create_product(price=None, title="Dummy title", product_class="Dummy item class",
15
 def create_product(price=None, title="Dummy title", product_class="Dummy item class",
72
 
68
 
73
 
69
 
74
 def create_offer():
70
 def create_offer():
71
+    """
72
+    Helper method for creating an offer
73
+    """
75
     range = Range.objects.create(name="All products range", includes_all_products=True)
74
     range = Range.objects.create(name="All products range", includes_all_products=True)
76
     condition = Condition.objects.create(range=range,
75
     condition = Condition.objects.create(range=range,
77
                                          type=Condition.COUNT,
76
                                          type=Condition.COUNT,
79
     benefit = Benefit.objects.create(range=range,
78
     benefit = Benefit.objects.create(range=range,
80
                                      type=Benefit.PERCENTAGE,
79
                                      type=Benefit.PERCENTAGE,
81
                                      value=20)
80
                                      value=20)
82
-    offer= ConditionalOffer.objects.create(
81
+    offer = ConditionalOffer.objects.create(
83
         name='Dummy offer',
82
         name='Dummy offer',
84
         offer_type='Site',
83
         offer_type='Site',
85
         condition=condition,
84
         condition=condition,
88
     return offer
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
 from django.conf import settings, global_settings
3
 from django.conf import settings, global_settings
4
 from oscar import OSCAR_CORE_APPS
4
 from oscar import OSCAR_CORE_APPS
5
 
5
 
6
+
6
 def configure():
7
 def configure():
7
     if not settings.configured:
8
     if not settings.configured:
8
         from oscar.defaults import OSCAR_SETTINGS
9
         from oscar.defaults import OSCAR_SETTINGS
55
             HAYSTACK_SEARCH_ENGINE='dummy',
56
             HAYSTACK_SEARCH_ENGINE='dummy',
56
             HAYSTACK_SITECONF = 'oscar.search_sites',
57
             HAYSTACK_SITECONF = 'oscar.search_sites',
57
             APPEND_SLASH=True,
58
             APPEND_SLASH=True,
58
-            NOSE_ARGS=['-s', '-x', '--with-spec'],
59
+            NOSE_ARGS=['-s', '-x'],
59
             **OSCAR_SETTINGS
60
             **OSCAR_SETTINGS
60
         )
61
         )

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

1
 from decimal import Decimal as D
1
 from decimal import Decimal as D
2
 import httplib
2
 import httplib
3
+import datetime
3
 
4
 
4
 from django.contrib.auth.models import User
5
 from django.contrib.auth.models import User
5
 from django.conf import settings
6
 from django.conf import settings
8
 
9
 
9
 from oscar.test.helpers import create_product
10
 from oscar.test.helpers import create_product
10
 from oscar.apps.basket.models import Basket
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
 class AnonAddToBasketViewTests(TestCase):
36
 class AnonAddToBasketViewTests(TestCase):
82
                         response.cookies['messages'].value)
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
 class SavedBasketTests(TestCase):
129
 class SavedBasketTests(TestCase):
86
 
130
 
87
     def test_moving_from_saved_basket(self):
131
     def test_moving_from_saved_basket(self):

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

5
 from django.conf import settings
5
 from django.conf import settings
6
 from django.utils.importlib import import_module
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
 from oscar.test import ClientTestCase, patch_settings
9
 from oscar.test import ClientTestCase, patch_settings
10
 from oscar.apps.basket.models import Basket
10
 from oscar.apps.basket.models import Basket
11
 from oscar.apps.order.models import Order
11
 from oscar.apps.order.models import Order
12
+from oscar.apps.address.models import Country
12
 
13
 
13
 
14
 
14
 class CheckoutMixin(object):
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
     def complete_guest_email_form(self, email='test@example.com'):
27
     def complete_guest_email_form(self, email='test@example.com'):
18
         response = self.client.post(reverse('checkout:index'),
28
         response = self.client.post(reverse('checkout:index'),
21
         self.assertIsRedirect(response)
31
         self.assertIsRedirect(response)
22
 
32
 
23
     def complete_shipping_address(self):
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
         response = self.client.post(reverse('checkout:shipping-address'),
38
         response = self.client.post(reverse('checkout:shipping-address'),
25
                                      {'last_name': 'Doe',
39
                                      {'last_name': 'Doe',
26
                                       'line1': '1 Egg Street',
40
                                       'line1': '1 Egg Street',
32
     def complete_shipping_method(self):
46
     def complete_shipping_method(self):
33
         self.client.get(reverse('checkout:shipping-method'))
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
 class DisabledAnonymousCheckoutViewsTests(ClientTestCase):
53
 class DisabledAnonymousCheckoutViewsTests(ClientTestCase):
96
             self.assertEqual('barry@example.com', order.guest_email)
109
             self.assertEqual('barry@example.com', order.guest_email)
97
 
110
 
98
 
111
 
99
-class ShippingAddressViewTests(ClientTestCase):
112
+class TestShippingAddressView(ClientTestCase, CheckoutMixin):
100
     fixtures = ['countries.json']
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
     def test_anon_checkout_disabled_by_default(self):
120
     def test_anon_checkout_disabled_by_default(self):
103
         self.assertFalse(settings.OSCAR_ALLOW_ANON_CHECKOUT)
121
         self.assertFalse(settings.OSCAR_ALLOW_ANON_CHECKOUT)
115
         self.assertEqual('1 Egg Street', session_address['line1'])
133
         self.assertEqual('1 Egg Street', session_address['line1'])
116
         self.assertEqual('N1 9RT', session_address['postcode'])
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
     fixtures = ['countries.json']
142
     fixtures = ['countries.json']
121
 
143
 
122
     def test_shipping_method_view_redirects_if_no_shipping_address(self):
144
     def test_shipping_method_view_redirects_if_no_shipping_address(self):
145
+        self.add_product_to_basket()
123
         response = self.client.get(reverse('checkout:shipping-method'))
146
         response = self.client.get(reverse('checkout:shipping-method'))
124
         self.assertIsRedirect(response)
147
         self.assertIsRedirect(response)
125
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
148
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
126
 
149
 
127
     def test_redirects_by_default(self):
150
     def test_redirects_by_default(self):
151
+        self.add_product_to_basket()
128
         self.complete_shipping_address()
152
         self.complete_shipping_address()
129
         response = self.client.get(reverse('checkout:shipping-method'))
153
         response = self.client.get(reverse('checkout:shipping-method'))
130
         self.assertRedirectUrlName(response, 'checkout:payment-method')
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
     def test_view_redirects_if_no_shipping_address(self):
163
     def test_view_redirects_if_no_shipping_address(self):
164
+        self.add_product_to_basket() 
136
         response = self.client.get(reverse('checkout:payment-method'))
165
         response = self.client.get(reverse('checkout:payment-method'))
137
         self.assertIsRedirect(response)
166
         self.assertIsRedirect(response)
138
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
167
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
139
 
168
 
140
     def test_view_redirects_if_no_shipping_method(self):
169
     def test_view_redirects_if_no_shipping_method(self):
170
+        self.add_product_to_basket() 
141
         self.complete_shipping_address()
171
         self.complete_shipping_address()
142
         response = self.client.get(reverse('checkout:payment-method'))
172
         response = self.client.get(reverse('checkout:payment-method'))
143
         self.assertIsRedirect(response)
173
         self.assertIsRedirect(response)
144
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
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
     def test_view_redirects_if_no_shipping_address(self):
187
     def test_view_redirects_if_no_shipping_address(self):
188
+        self.add_product_to_basket()
150
         response = self.client.get(reverse('checkout:preview'))
189
         response = self.client.get(reverse('checkout:preview'))
151
         self.assertIsRedirect(response)
190
         self.assertIsRedirect(response)
152
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
191
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
153
 
192
 
154
     def test_view_redirects_if_no_shipping_method(self):
193
     def test_view_redirects_if_no_shipping_method(self):
194
+        self.add_product_to_basket()
155
         self.complete_shipping_address()
195
         self.complete_shipping_address()
156
         response = self.client.get(reverse('checkout:preview'))
196
         response = self.client.get(reverse('checkout:preview'))
157
         self.assertIsRedirect(response)
197
         self.assertIsRedirect(response)
158
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
198
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
159
 
199
 
160
     def test_ok_response_if_previous_steps_complete(self):
200
     def test_ok_response_if_previous_steps_complete(self):
201
+        self.add_product_to_basket()
161
         self.complete_shipping_address()
202
         self.complete_shipping_address()
162
         self.complete_shipping_method()
203
         self.complete_shipping_method()
163
         response = self.client.get(reverse('checkout:preview'))
204
         response = self.client.get(reverse('checkout:preview'))
166
 
207
 
167
 class PaymentDetailsViewTests(ClientTestCase, CheckoutMixin):
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
     def test_view_redirects_if_no_shipping_address(self):
214
     def test_view_redirects_if_no_shipping_address(self):
215
+        self.add_product_to_basket()
170
         response = self.client.post(reverse('checkout:payment-details'))
216
         response = self.client.post(reverse('checkout:payment-details'))
171
         self.assertIsRedirect(response)
217
         self.assertIsRedirect(response)
172
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
218
         self.assertRedirectUrlName(response, 'checkout:shipping-address')
173
 
219
 
174
     def test_view_redirects_if_no_shipping_method(self):
220
     def test_view_redirects_if_no_shipping_method(self):
221
+        self.add_product_to_basket()
175
         self.complete_shipping_address()
222
         self.complete_shipping_address()
176
         response = self.client.post(reverse('checkout:payment-details'))
223
         response = self.client.post(reverse('checkout:payment-details'))
177
         self.assertIsRedirect(response)
224
         self.assertIsRedirect(response)
178
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
225
         self.assertRedirectUrlName(response, 'checkout:shipping-method')
179
 
226
 
180
     def test_placing_order_with_empty_basket_redirects(self):
227
     def test_placing_order_with_empty_basket_redirects(self):
181
-        self.complete_shipping_address()
182
-        self.complete_shipping_method()
183
         response = self.client.post(reverse('checkout:preview'), {'action': 'place_order'})
228
         response = self.client.post(reverse('checkout:preview'), {'action': 'place_order'})
184
         self.assertIsRedirect(response)
229
         self.assertIsRedirect(response)
185
-        self.assertRedirectUrlName(response, 'checkout:shipping-address')
230
+        self.assertRedirectUrlName(response, 'basket:summary')
186
 
231
 
187
 
232
 
188
 class OrderPlacementTests(ClientTestCase, CheckoutMixin):
233
 class OrderPlacementTests(ClientTestCase, CheckoutMixin):
206
         self.assertIsRedirect(self.response)
251
         self.assertIsRedirect(self.response)
207
         orders = Order.objects.all()
252
         orders = Order.objects.all()
208
         self.assertEqual(1, len(orders))
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
         self.assertEqual(0, self.stockrecord.num_allocated)
58
         self.assertEqual(0, self.stockrecord.num_allocated)
59
         self.assertEqual(10, self.stockrecord.num_in_stock)
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
 class DefaultWrapperTests(TestCase):
65
 class DefaultWrapperTests(TestCase):
63
 
66
 

正在加载...
取消
保存