Skip to content
This repository
  • 10 commits
  • 28 files changed
  • 1 comment
  • 1 contributor

Showing 28 changed files with 514 additions and 281 deletions. Show diff stats Hide diff stats

  1. 11  oscar/apps/catalogue/abstract_models.py
  2. 91  oscar/apps/checkout/mixins.py
  3. 87  oscar/apps/checkout/session.py
  4. 4  oscar/apps/checkout/views.py
  5. 27  oscar/apps/dashboard/catalogue/forms.py
  6. 44  oscar/apps/dashboard/catalogue/views.py
  7. 2  oscar/apps/dashboard/reviews/forms.py
  8. 8  oscar/apps/offer/models.py
  9. 12  oscar/apps/offer/receivers.py
  10. 1  oscar/apps/offer/utils.py
  11. 21  oscar/apps/order/utils.py
  12. 23  oscar/apps/voucher/abstract_models.py
  13. 2  oscar/apps/voucher/models.py
  14. 6  oscar/templates/checkout/thank_you.html
  15. 2  oscar/templates/dashboard/catalogue/category_list.html
  16. 8  oscar/templates/dashboard/catalogue/product_update.html
  17. 2  oscar/templates/dashboard/reports/partials/open_basket_report.html
  18. 2  oscar/templates/dashboard/reports/partials/submitted_basket_report.html
  19. 2  oscar/templates/dashboard/vouchers/voucher_detail.html
  20. 13  oscar/test/__init__.py
  21. 14  runtests.py
  22. 4  tests/config.py
  23. 42  tests/functional/checkout_tests.py
  24. 96  tests/functional/dashboard/catalogue_tests.py
  25. 98  tests/functional/dashboard/category_tests.py
  26. 103  tests/functional/dashboard/product_tests.py
  27. 0  tests/unit/dashboard/{cataogue_form_tests.py → catalogue_form_tests.py}
  28. 70  tests/unit/voucher_tests.py
11  oscar/apps/catalogue/abstract_models.py
@@ -152,7 +152,10 @@ class Meta:
152 152
         verbose_name_plural = _('Categories')
153 153
 
154 154
     def has_children(self):
155  
-        return self.get_children().count() > 0
  155
+        return self.get_num_children() > 0
  156
+
  157
+    def get_num_children(self):
  158
+        return self.get_children().count()
156 159
 
157 160
 
158 161
 class AbstractProductCategory(models.Model):
@@ -302,8 +305,10 @@ def options(self):
302 305
 
303 306
     @property
304 307
     def is_top_level(self):
305  
-        u"""Return True if this is a parent product"""
306  
-        return self.parent_id == None
  308
+        """
  309
+        Test if this product is a parent (who may or may not have children)
  310
+        """
  311
+        return self.parent_id is None
307 312
 
308 313
     @property
309 314
     def is_group(self):
91  oscar/apps/checkout/mixins.py
@@ -6,108 +6,23 @@
6 6
 from django.core.exceptions import ObjectDoesNotExist
7 7
 from django.db.models import get_model
8 8
 
9  
-from oscar.apps.shipping.methods import Free
10  
-from oscar.core.loading import get_class, get_classes
11  
-ShippingAddressForm, GatewayForm = get_classes('checkout.forms', ['ShippingAddressForm', 'GatewayForm'])
12  
-OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator')
13  
-CheckoutSessionData = get_class('checkout.utils', 'CheckoutSessionData')
14  
-pre_payment, post_payment = get_classes('checkout.signals', ['pre_payment', 'post_payment'])
15  
-OrderNumberGenerator, OrderCreator = get_classes('order.utils', ['OrderNumberGenerator', 'OrderCreator'])
16  
-UserAddressForm = get_class('address.forms', 'UserAddressForm')
17  
-Repository = get_class('shipping.repository', 'Repository')
18  
-AccountAuthView = get_class('customer.views', 'AccountAuthView')
  9
+from oscar.core.loading import get_class
  10
+OrderCreator = get_class('order.utils', 'OrderCreator')
19 11
 Dispatcher = get_class('customer.utils', 'Dispatcher')
20  
-RedirectRequired, UnableToTakePayment, PaymentError = get_classes(
21  
-    'payment.exceptions', ['RedirectRequired', 'UnableToTakePayment', 'PaymentError'])
22  
-UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
  12
+CheckoutSessionMixin = get_class('checkout.session', 'CheckoutSessionMixin')
23 13
 
24  
-Order = get_model('order', 'Order')
25 14
 ShippingAddress = get_model('order', 'ShippingAddress')
26 15
 CommunicationEvent = get_model('order', 'CommunicationEvent')
27 16
 PaymentEventType = get_model('order', 'PaymentEventType')
28 17
 PaymentEvent = get_model('order', 'PaymentEvent')
29 18
 UserAddress = get_model('address', 'UserAddress')
30 19
 Basket = get_model('basket', 'Basket')
31  
-Email = get_model('customer', 'Email')
32 20
 CommunicationEventType = get_model('customer', 'CommunicationEventType')
33 21
 
34 22
 # Standard logger for checkout events
35 23
 logger = logging.getLogger('oscar.checkout')
36 24
 
37 25
 
38  
-class CheckoutSessionMixin(object):
39  
-    """
40  
-    Mixin to provide common functionality shared between checkout views.
41  
-    """
42  
-
43  
-    def dispatch(self, request, *args, **kwargs):
44  
-        self.checkout_session = CheckoutSessionData(request)
45  
-        return super(CheckoutSessionMixin, self).dispatch(request, *args, **kwargs)
46  
-
47  
-    def get_shipping_address(self):
48  
-        """
49  
-        Return the current shipping address for this checkout session.
50  
-
51  
-        This could either be a ShippingAddress model which has been
52  
-        pre-populated (not saved), or a UserAddress model which will
53  
-        need converting into a ShippingAddress model at submission
54  
-        """
55  
-        addr_data = self.checkout_session.new_shipping_address_fields()
56  
-        if addr_data:
57  
-            # Load address data into a blank address model
58  
-            return ShippingAddress(**addr_data)
59  
-        addr_id = self.checkout_session.user_address_id()
60  
-        if addr_id:
61  
-            try:
62  
-                return UserAddress._default_manager.get(pk=addr_id)
63  
-            except UserAddress.DoesNotExist:
64  
-                # This can happen if you reset all your tables and you still have
65  
-                # session data that refers to addresses that no longer exist
66  
-                pass
67  
-        return None
68  
-
69  
-    def get_shipping_method(self, basket=None):
70  
-        method = self.checkout_session.shipping_method()
71  
-        if method:
72  
-            if not basket:
73  
-                basket = self.request.basket
74  
-            method.set_basket(basket)
75  
-        else:
76  
-            # We default to using free shipping
77  
-            method = Free()
78  
-        return method
79  
-
80  
-    def get_order_totals(self, basket=None, shipping_method=None, **kwargs):
81  
-        """
82  
-        Returns the total for the order with and without tax (as a tuple)
83  
-        """
84  
-        calc = OrderTotalCalculator(self.request)
85  
-        if not basket:
86  
-            basket = self.request.basket
87  
-        if not shipping_method:
88  
-            shipping_method = self.get_shipping_method(basket)
89  
-        total_incl_tax = calc.order_total_incl_tax(basket, shipping_method, **kwargs)
90  
-        total_excl_tax = calc.order_total_excl_tax(basket, shipping_method, **kwargs)
91  
-        return total_incl_tax, total_excl_tax
92  
-
93  
-    def get_context_data(self, **kwargs):
94  
-        """
95  
-        Assign common template variables to the context.
96  
-        """
97  
-        ctx = super(CheckoutSessionMixin, self).get_context_data(**kwargs)
98  
-        ctx['shipping_address'] = self.get_shipping_address()
99  
-
100  
-        method = self.get_shipping_method()
101  
-        if method:
102  
-            ctx['shipping_method'] = method
103  
-            ctx['shipping_total_excl_tax'] = method.basket_charge_excl_tax()
104  
-            ctx['shipping_total_incl_tax'] = method.basket_charge_incl_tax()
105  
-
106  
-        ctx['order_total_incl_tax'], ctx['order_total_excl_tax'] = self.get_order_totals()
107  
-
108  
-        return ctx
109  
-
110  
-
111 26
 class OrderPlacementMixin(CheckoutSessionMixin):
112 27
     """
113 28
     Mixin which provides functionality for placing orders.
87  oscar/apps/checkout/session.py
... ...
@@ -0,0 +1,87 @@
  1
+import logging
  2
+
  3
+from django.db.models import get_model
  4
+
  5
+from oscar.apps.shipping.methods import Free
  6
+from oscar.core.loading import get_class
  7
+OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator')
  8
+CheckoutSessionData = get_class('checkout.utils', 'CheckoutSessionData')
  9
+
  10
+ShippingAddress = get_model('order', 'ShippingAddress')
  11
+UserAddress = get_model('address', 'UserAddress')
  12
+
  13
+# Standard logger for checkout events
  14
+logger = logging.getLogger('oscar.checkout')
  15
+
  16
+
  17
+class CheckoutSessionMixin(object):
  18
+    """
  19
+    Mixin to provide common functionality shared between checkout views.
  20
+    """
  21
+
  22
+    def dispatch(self, request, *args, **kwargs):
  23
+        self.checkout_session = CheckoutSessionData(request)
  24
+        return super(CheckoutSessionMixin, self).dispatch(request, *args, **kwargs)
  25
+
  26
+    def get_shipping_address(self):
  27
+        """
  28
+        Return the current shipping address for this checkout session.
  29
+
  30
+        This could either be a ShippingAddress model which has been
  31
+        pre-populated (not saved), or a UserAddress model which will
  32
+        need converting into a ShippingAddress model at submission
  33
+        """
  34
+        addr_data = self.checkout_session.new_shipping_address_fields()
  35
+        if addr_data:
  36
+            # Load address data into a blank address model
  37
+            return ShippingAddress(**addr_data)
  38
+        addr_id = self.checkout_session.user_address_id()
  39
+        if addr_id:
  40
+            try:
  41
+                return UserAddress._default_manager.get(pk=addr_id)
  42
+            except UserAddress.DoesNotExist:
  43
+                # This can happen if you reset all your tables and you still have
  44
+                # session data that refers to addresses that no longer exist
  45
+                pass
  46
+        return None
  47
+
  48
+    def get_shipping_method(self, basket=None):
  49
+        method = self.checkout_session.shipping_method()
  50
+        if method:
  51
+            if not basket:
  52
+                basket = self.request.basket
  53
+            method.set_basket(basket)
  54
+        else:
  55
+            # We default to using free shipping
  56
+            method = Free()
  57
+        return method
  58
+
  59
+    def get_order_totals(self, basket=None, shipping_method=None, **kwargs):
  60
+        """
  61
+        Returns the total for the order with and without tax (as a tuple)
  62
+        """
  63
+        calc = OrderTotalCalculator(self.request)
  64
+        if not basket:
  65
+            basket = self.request.basket
  66
+        if not shipping_method:
  67
+            shipping_method = self.get_shipping_method(basket)
  68
+        total_incl_tax = calc.order_total_incl_tax(basket, shipping_method, **kwargs)
  69
+        total_excl_tax = calc.order_total_excl_tax(basket, shipping_method, **kwargs)
  70
+        return total_incl_tax, total_excl_tax
  71
+
  72
+    def get_context_data(self, **kwargs):
  73
+        """
  74
+        Assign common template variables to the context.
  75
+        """
  76
+        ctx = super(CheckoutSessionMixin, self).get_context_data(**kwargs)
  77
+        ctx['shipping_address'] = self.get_shipping_address()
  78
+
  79
+        method = self.get_shipping_method()
  80
+        if method:
  81
+            ctx['shipping_method'] = method
  82
+            ctx['shipping_total_excl_tax'] = method.basket_charge_excl_tax()
  83
+            ctx['shipping_total_incl_tax'] = method.basket_charge_incl_tax()
  84
+
  85
+        ctx['order_total_incl_tax'], ctx['order_total_excl_tax'] = self.get_order_totals()
  86
+
  87
+        return ctx
4  oscar/apps/checkout/views.py
@@ -23,8 +23,8 @@
23 23
 RedirectRequired, UnableToTakePayment, PaymentError = get_classes(
24 24
     'payment.exceptions', ['RedirectRequired', 'UnableToTakePayment', 'PaymentError'])
25 25
 UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
26  
-CheckoutSessionMixin, OrderPlacementMixin = get_classes('checkout.mixins',
27  
-    ('CheckoutSessionMixin', 'OrderPlacementMixin'))
  26
+OrderPlacementMixin = get_class('checkout.mixins', 'OrderPlacementMixin')
  27
+CheckoutSessionMixin = get_class('checkout.session', 'CheckoutSessionMixin')
28 28
 
29 29
 Order = get_model('order', 'Order')
30 30
 ShippingAddress = get_model('order', 'ShippingAddress')
27  oscar/apps/dashboard/catalogue/forms.py
... ...
@@ -1,5 +1,5 @@
1 1
 from django import forms
2  
-from django.forms.models import inlineformset_factory
  2
+from django.forms.models import inlineformset_factory, BaseInlineFormSet
3 3
 from django.db.models import get_model
4 4
 from django.template.defaultfilters import slugify
5 5
 from django.utils.translation import ugettext_lazy as _
@@ -9,6 +9,7 @@
9 9
 Product = get_model('catalogue', 'Product')
10 10
 Category = get_model('catalogue', 'Category')
11 11
 StockRecord = get_model('partner', 'StockRecord')
  12
+Partner = get_model('partner', 'Partner')
12 13
 ProductAttributeValue = get_model('catalogue', 'ProductAttributeValue')
13 14
 ProductCategory = get_model('catalogue', 'ProductCategory')
14 15
 ProductImage = get_model('catalogue', 'ProductImage')
@@ -63,6 +64,11 @@ class ProductSearchForm(forms.Form):
63 64
 
64 65
 
65 66
 class StockRecordForm(forms.ModelForm):
  67
+    partner = forms.ModelChoiceField(queryset=Partner.objects.all(),
  68
+                                    required=False,
  69
+                                    label=_("Partner"))
  70
+    partner_sku = forms.CharField(required=False,
  71
+                                  label=_("Partner SKU"))
66 72
 
67 73
     class Meta:
68 74
         model = StockRecord
@@ -196,8 +202,27 @@ class Meta:
196 202
         model = ProductCategory
197 203
 
198 204
 
  205
+class ProductCategoryFormSet(BaseInlineFormSet):
  206
+
  207
+    def clean(self):
  208
+        if self.instance.is_top_level and self.get_num_categories() == 0:
  209
+            raise forms.ValidationError(
  210
+                _("A top-level product must have at least one category"))
  211
+        if self.instance.is_variant and self.get_num_categories() > 0:
  212
+            raise forms.ValidationError(
  213
+                _("A variant product should not have at categories"))
  214
+
  215
+    def get_num_categories(self):
  216
+        num_categories = 0
  217
+        for i in range(0, self.total_form_count()):
  218
+            form = self.forms[i]
  219
+            if form.has_changed():
  220
+                num_categories += 1
  221
+        return num_categories
  222
+
199 223
 ProductCategoryFormSet = inlineformset_factory(Product, ProductCategory,
200 224
                                                form=ProductCategoryForm,
  225
+                                               formset=ProductCategoryFormSet,
201 226
                                                fields=('category',), extra=1)
202 227
 
203 228
 
44  oscar/apps/dashboard/catalogue/views.py
@@ -68,7 +68,7 @@ class ProductCreateRedirectView(generic.RedirectView):
68 68
 
69 69
     def get_redirect_url(self, **kwargs):
70 70
         product_class_id = self.request.GET.get('product_class', None)
71  
-        if not product_class_id.isdigit():
  71
+        if not product_class_id or not product_class_id.isdigit():
72 72
             messages.error(self.request, _("Please choose a product class"))
73 73
             return reverse('dashboard:catalogue-product-list')
74 74
         try:
@@ -96,6 +96,7 @@ def get_context_data(self, **kwargs):
96 96
         if 'image_formset' not in ctx:
97 97
             ctx['image_formset'] = ProductImageFormSet()
98 98
         ctx['title'] = _('Create new product')
  99
+        ctx['product_class'] = self.get_product_class()
99 100
         return ctx
100 101
 
101 102
     def get_product_class(self):
@@ -106,10 +107,20 @@ def get_form_kwargs(self):
106 107
         kwargs['product_class'] = self.get_product_class()
107 108
         return kwargs
108 109
 
  110
+    def is_stockrecord_submitted(self):
  111
+        return len(self.request.POST.get('partner', '')) > 0
  112
+
109 113
     def form_invalid(self, form):
110  
-        stockrecord_form = StockRecordForm(self.request.POST)
  114
+        if self.is_stockrecord_submitted():
  115
+            stockrecord_form = StockRecordForm(self.request.POST)
  116
+        else:
  117
+            stockrecord_form = StockRecordForm()
111 118
         category_formset = ProductCategoryFormSet(self.request.POST)
112 119
         image_formset = ProductImageFormSet(self.request.POST, self.request.FILES)
  120
+
  121
+        messages.error(self.request,
  122
+                       _("Your submitted data was not valid - please "
  123
+                         "correct the below errors"))
113 124
         ctx = self.get_context_data(form=form,
114 125
                                     stockrecord_form=stockrecord_form,
115 126
                                     category_formset=category_formset,
@@ -118,28 +129,37 @@ def form_invalid(self, form):
118 129
 
119 130
     def form_valid(self, form):
120 131
         product = form.save()
121  
-
122  
-        stockrecord_form = StockRecordForm(self.request.POST)
123 132
         category_formset = ProductCategoryFormSet(self.request.POST,
124 133
                                                   instance=product)
125 134
         image_formset = ProductImageFormSet(self.request.POST,
126 135
                                             self.request.FILES,
127 136
                                             instance=product)
128  
-        if all([stockrecord_form.is_valid(), category_formset.is_valid(), image_formset.is_valid()]):
129  
-            # Save product
130  
-            product.save()
131  
-            # Save stock record
132  
-            stockrecord = stockrecord_form.save(commit=False)
133  
-            stockrecord.product = product
134  
-            stockrecord.save()
  137
+        if self.is_stockrecord_submitted():
  138
+            stockrecord_form = StockRecordForm(self.request.POST)
  139
+            is_valid = all([stockrecord_form.is_valid(),
  140
+                            category_formset.is_valid(),
  141
+                            image_formset.is_valid()])
  142
+        else:
  143
+            stockrecord_form = StockRecordForm()
  144
+            is_valid = all([category_formset.is_valid(),
  145
+                            image_formset.is_valid()])
  146
+        if is_valid:
  147
+            if self.is_stockrecord_submitted():
  148
+                # Save stock record
  149
+                stockrecord = stockrecord_form.save(commit=False)
  150
+                stockrecord.product = product
  151
+                stockrecord.save()
135 152
             # Save formsets
136 153
             category_formset.save()
137 154
             image_formset.save()
138 155
             return HttpResponseRedirect(self.get_success_url(product))
139 156
 
  157
+        messages.error(self.request,
  158
+                       _("Your submitted data was not valid - please "
  159
+                         "correct the below errors"))
  160
+
140 161
         # Delete product as its relations were not valid
141 162
         product.delete()
142  
-
143 163
         ctx = self.get_context_data(form=form,
144 164
                                     stockrecord_form=stockrecord_form,
145 165
                                     category_formset=category_formset,
2  oscar/apps/dashboard/reviews/forms.py
@@ -24,7 +24,7 @@ class ProductReviewSearchForm(forms.Form):
24 24
     keyword = forms.CharField(required=False, label=_("Keyword"))
25 25
     status = forms.ChoiceField(required=False, choices=STATUS_CHOICES,
26 26
                                label=_("Status"))
27  
-    date_from = forms.DateTimeField(required=False)
  27
+    date_from = forms.DateTimeField(required=False, label=_("Date from"))
28 28
     date_to = forms.DateTimeField(required=False, label=_('to'))
29 29
     name = forms.CharField(required=False, label=_('Customer name'))
30 30
 
8  oscar/apps/offer/models.py
@@ -251,8 +251,8 @@ def __unicode__(self):
251 251
             desc = _("%(value).2f discount on %(range)s") % {'value': float(self.value),
252 252
                                                              'range': unicode(self.range).lower()}
253 253
 
254  
-        max_item_str = ungettext(" (max %d item)", " (max %d items)", self.max_affected_items)
255  
-        desc += max_item_str % self.max_affected_items
  254
+        if self.max_affected_items:
  255
+            desc += ungettext(" (max 1 item)", " (max %d items)", self.max_affected_items) % self.max_affected_items
256 256
 
257 257
         return desc
258 258
 
@@ -752,7 +752,3 @@ def apply(self, basket, condition=None):
752 752
         else:
753 753
             free_line.discount(discount, 0)
754 754
         return self.round(discount)
755  
-
756  
-
757  
-# We need to import receivers at the bottom of this script
758  
-from oscar.apps.offer.receivers import receive_basket_voucher_change
12  oscar/apps/offer/receivers.py
@@ -16,15 +16,3 @@ def receive_basket_voucher_change(sender, **kwargs):
16 16
         voucher = Voucher._default_manager.get(pk=voucher_id)
17 17
         voucher.num_basket_additions += 1
18 18
         voucher.save()
19  
-
20  
-
21  
-@receiver(post_save, sender=OrderDiscount)        
22  
-def receive_order_discount_save(sender, instance, **kwargs):
23  
-    # Record the amount of discount against the appropriate offers
24  
-    # and vouchers
25  
-    discount = instance
26  
-    if discount.voucher:
27  
-        discount.voucher.total_discount += discount.amount
28  
-        discount.voucher.save()
29  
-    if discount.offer:
30  
-        discount.offer.record_usage(discount.amount)
1  oscar/apps/offer/utils.py
@@ -48,6 +48,7 @@ def apply(self, request, basket):
48 48
         offers = self.get_offers(request, basket) 
49 49
         logger.debug("Found %d offers to apply to basket %d", len(offers), basket.id)
50 50
         discounts = self.get_basket_discounts(basket, offers)
  51
+
51 52
         # Store this list of discounts with the basket so it can be 
52 53
         # rendered in templates
53 54
         basket.set_discounts(list(discounts.values()))
21  oscar/apps/order/utils.py
@@ -69,8 +69,11 @@ def place_order(self, basket, total_incl_tax=None, total_excl_tax=None,
69 69
         for line in basket.all_lines():
70 70
             self.create_line_models(order, line)
71 71
             self.update_stock_records(line)
  72
+
72 73
         for discount in basket.get_discounts():
73 74
             self.create_discount_model(order, discount)
  75
+            self.record_discount(discount)
  76
+
74 77
         for voucher in basket.vouchers.all():
75 78
             self.record_voucher_usage(order, voucher, user)
76 79
         
@@ -78,6 +81,8 @@ def place_order(self, basket, total_incl_tax=None, total_excl_tax=None,
78 81
         order_placed.send(sender=self, order=order, user=user)
79 82
         
80 83
         return order
  84
+
  85
+
81 86
         
82 87
     def create_order_model(self, user, basket, shipping_address, shipping_method, 
83 88
                            billing_address, total_incl_tax, total_excl_tax, 
@@ -200,17 +205,21 @@ def create_discount_model(self, order, discount):
200 205
         Creates an order discount model for each discount attached to the basket.
201 206
         """
202 207
         order_discount = OrderDiscount(order=order,
203  
-                                       offer_id=discount['offer'].id, 
  208
+                                       offer_id=discount['offer'].id,
204 209
                                        amount=discount['discount'])
205  
-        if discount['voucher']:
206  
-            order_discount.voucher_id = discount['voucher'].id
207  
-            order_discount.voucher_code = discount['voucher'].code
  210
+        voucher = discount.get('voucher', None)
  211
+        if voucher:
  212
+            order_discount.voucher_id = voucher.id
  213
+            order_discount.voucher_code = voucher.code
208 214
         order_discount.save()
  215
+
  216
+    def record_discount(self, discount):
  217
+        discount['offer'].record_usage(discount['discount'])
  218
+        if 'voucher' in discount:
  219
+            discount['voucher'].record_discount(discount['discount'])
209 220
         
210 221
     def record_voucher_usage(self, order, voucher, user):
211 222
         """
212 223
         Updates the models that care about this voucher.
213 224
         """
214 225
         voucher.record_usage(order, user)
215  
-        voucher.num_orders += 1
216  
-        voucher.save()
23  oscar/apps/voucher/abstract_models.py
@@ -8,7 +8,7 @@
8 8
 
9 9
 class AbstractVoucher(models.Model):
10 10
     """
11  
-    A voucher.  This is simply a link to a collection of offers
  11
+    A voucher.  This is simply a link to a collection of offers.
12 12
 
13 13
     Note that there are three possible "usage" models:
14 14
     (a) Single use
@@ -19,7 +19,7 @@ class AbstractVoucher(models.Model):
19 19
         help_text=_("""This will be shown in the checkout and basket once the voucher is entered"""))
20 20
     code = models.CharField(_("Code"), max_length=128, db_index=True, unique=True,
21 21
         help_text=_("""Case insensitive / No spaces allowed"""))
22  
-    offers = models.ManyToManyField('offer.ConditionalOffer', related_name='vouchers', 
  22
+    offers = models.ManyToManyField('offer.ConditionalOffer', related_name='vouchers',
23 23
                                     limit_choices_to={'offer_type': "Voucher"})
24 24
 
25 25
     SINGLE_USE, MULTI_USE, ONCE_PER_CUSTOMER = ('Single use', 'Multi-use', 'Once per customer')
@@ -33,7 +33,7 @@ class AbstractVoucher(models.Model):
33 33
     start_date = models.DateField(_('Start Date'))
34 34
     end_date = models.DateField(_('End Date'))
35 35
 
36  
-    # Summary information
  36
+    # Audit information
37 37
     num_basket_additions = models.PositiveIntegerField(_('Times added to basket'), default=0)
38 38
     num_orders = models.PositiveIntegerField(_('Times on orders'), default=0)
39 39
     total_discount = models.DecimalField(_('Total discount'), decimal_places=2, max_digits=12, default=Decimal('0.00'))
@@ -59,7 +59,7 @@ def save(self, *args, **kwargs):
59 59
 
60 60
     def is_active(self, test_date=None):
61 61
         """
62  
-        Tests whether this voucher is currently active.
  62
+        Test whether this voucher is currently active.
63 63
         """
64 64
         if not test_date:
65 65
             test_date = datetime.date.today()
@@ -67,7 +67,7 @@ def is_active(self, test_date=None):
67 67
 
68 68
     def is_available_to_user(self, user=None):
69 69
         """
70  
-        Tests whether this voucher is available to the passed user.
  70
+        Test whether this voucher is available to the passed user.
71 71
         
72 72
         Returns a tuple of a boolean for whether it is successulf, and a message
73 73
         """
@@ -96,6 +96,15 @@ def record_usage(self, order, user):
96 96
             self.applications.create(voucher=self, order=order, user=user)
97 97
         else:
98 98
             self.applications.create(voucher=self, order=order)
  99
+        self.num_orders += 1
  100
+        self.save()
  101
+
  102
+    def record_discount(self, discount):
  103
+        """
  104
+        Record a discount that this offer has given
  105
+        """
  106
+        self.total_discount += discount
  107
+        self.save()
99 108
 
100 109
     @property
101 110
     def benefit(self):
@@ -120,4 +129,6 @@ class Meta:
120 129
         verbose_name_plural = _("Voucher Applications")
121 130
 
122 131
     def __unicode__(self):
123  
-        return _("'%(voucher)s' used by '%(user)s'") % {'voucher': self.voucher, 'user': self.user}
  132
+        return _("'%(voucher)s' used by '%(user)s'") % {
  133
+            'voucher': self.voucher,
  134
+            'user': self.user}
2  oscar/apps/voucher/models.py
@@ -7,5 +7,3 @@ class Voucher(AbstractVoucher):
7 7
 
8 8
 class VoucherApplication(AbstractVoucherApplication):
9 9
     pass
10  
-
11  
-
6  oscar/templates/checkout/thank_you.html
@@ -91,11 +91,7 @@ <h4 class="span11">{% trans "Basket total" %}</h4>
91 91
 </div>
92 92
 <div class="basket-items">
93 93
     <div class="row-fluid">
94  
-		<h4 class="span11">
95  
-            {% blocktrans with order.shipping_method as shipping_method %}
96  
-            Shipping charge - {{ shipping_method }}
97  
-            {% endblocktrans %}
98  
-        </h4>
  94
+		<h4 class="span11">{% blocktrans with method=order.shipping_method %}Shipping charge - {{ method }}{% endblocktrans %}</h4>
99 95
         <div class="span1">{{ order.shipping_incl_tax|currency }}</div>
100 96
     </div>
101 97
 </div>
2  oscar/templates/dashboard/catalogue/category_list.html
@@ -48,7 +48,7 @@
48 48
         <tr>
49 49
             <td>{{ category.name }}</td>
50 50
 			<td>{{ category.description|default:""|truncatewords:6 }}</td>
51  
-			<td>{{ category.get_children_count }}</td>
  51
+			<td>{{ category.get_num_children }}</td>
52 52
             <td>
53 53
                 <a class="btn btn-primary" href="{% url dashboard:catalogue-category-update category.id %}">Edit category</a>
54 54
 				<a class="btn btn-primary {% if not category.has_children %}disabled{% endif %}" href="{% url dashboard:catalogue-category-detail-list pk=category.pk %}">Edit children</a>
8  oscar/templates/dashboard/catalogue/product_update.html
@@ -36,8 +36,9 @@
36 36
             <h3 class="app-ico ico_expand icon">Product information</h3>
37 37
         </div>
38 38
 		<div class="control-group fields-full">
39  
-			{% trans "Product class" %}: <strong>{{ product.product_class }}</strong>
  39
+			{% trans "Product class" %}: <strong>{{ product_class.name }}</strong>
40 40
 		</div>
  41
+		{{ form.non_field_errors }}
41 42
         {% for field in form %}
42 43
             {% if forloop.counter > 4 %}
43 44
                 <div class="form-inline pull-left">
@@ -87,7 +88,12 @@ <h3 class="app-ico ico_expand icon">Product information</h3>
87 88
 		<div class="sub-header">
88 89
 			<h3 class="app-ico ico_home icon">{% trans "Category information" %}</h3>
89 90
 		</div>
  91
+		<p>{% blocktrans %}The first category in this list is the "primary" category for the product, and will be the one displayed
  92
+		in the site breadcrumb trail.
  93
+		{% endblocktrans %}
  94
+		</p>
90 95
     	{{ category_formset.management_form }}
  96
+	{{ category_formset.non_form_errors }}
91 97
     	{% for category_form in category_formset %}
92 98
 			{% include "partials/form_fields_inline.html" with form=category_form %}
93 99
     		<hr/>
2  oscar/templates/dashboard/reports/partials/open_basket_report.html
@@ -15,7 +15,7 @@
15 15
     </tr>
16 16
     {% for basket in objects %}
17 17
     <tr>
18  
-        <td>{{ basket.owner.email }}</td>
  18
+        <td>{{ basket.owner.email|default:"-" }}</td>
19 19
         <td>{{ basket.owner.get_full_name|default:"-" }}</td>
20 20
         <td>{{ basket.num_lines }}</td>
21 21
         <td>{{ basket.num_items }}</td>
2  oscar/templates/dashboard/reports/partials/submitted_basket_report.html
@@ -15,7 +15,7 @@
15 15
     </tr>
16 16
     {% for basket in objects %}
17 17
     <tr>
18  
-        <td>{{ basket.owner.email }}</td>
  18
+        <td>{{ basket.owner.email|default:"-" }}</td>
19 19
         <td>{{ basket.owner.get_full_name|default:"-" }}</td>
20 20
         <td>{{ basket.num_lines }}</td>
21 21
         <td>{{ basket.num_items }}</td>
2  oscar/templates/dashboard/vouchers/voucher_detail.html
@@ -45,7 +45,7 @@
45 45
 		<tbody>
46 46
 			<tr><th>{% trans "Number of basket additions" %}</th><td>{{ voucher.num_basket_additions }}</td></tr>
47 47
 			<tr><th>{% trans "Number of orders" %}</th><td>{{ voucher.num_orders }}</td></tr>
48  
-			<tr><th>{% trans "Total discount" %}</th><td>{{ voucher.total_discount }}</td></tr>
  48
+			<tr><th>{% trans "Total discount" %}</th><td>{{ voucher.total_discount|currency }}</td></tr>
49 49
 		</tbody>
50 50
 	</table>
51 51
 
13  oscar/test/__init__.py
@@ -32,9 +32,12 @@ class ClientTestCase(TestCase):
32 32
     def setUp(self):
33 33
         self.client = Client()
34 34
         if not self.is_anonymous:
35  
-            self.user = self.create_user()
36  
-            self.client.login(username=self.username,
37  
-                              password=self.password)
  35
+            self.login()
  36
+
  37
+    def login(self):
  38
+        self.user = self.create_user()
  39
+        self.client.login(username=self.username,
  40
+                          password=self.password)
38 41
 
39 42
     def create_user(self):
40 43
         user = User.objects.create_user(self.username,
@@ -52,10 +55,10 @@ def assertIsRedirect(self, response, expected_url=None):
52 55
             location = URL.from_string(response['Location'])
53 56
             self.assertEqual(expected_url, location.path())
54 57
 
55  
-    def assertRedirectUrlName(self, response, name):
  58
+    def assertRedirectUrlName(self, response, name, kwargs=None):
56 59
         self.assertIsRedirect(response)
57 60
         location = response['Location'].replace('http://testserver', '')
58  
-        self.assertEqual(location, reverse(name))
  61
+        self.assertEqual(location, reverse(name, kwargs=kwargs))
59 62
 
60 63
     def assertIsOk(self, response):
61 64
         self.assertEqual(httplib.OK, response.status_code)
14  runtests.py
@@ -4,18 +4,16 @@
4 4
 from optparse import OptionParser
5 5
 from coverage import coverage
6 6
 
7  
-# This configures the settings
8 7
 from tests.config import configure
9  
-configure()
10  
-
11  
-from django_nose import NoseTestSuiteRunner
12 8
 
13 9
 logging.disable(logging.CRITICAL)
14 10
 
15 11
 
16 12
 def run_tests(options, *test_args):
  13
+    from django_nose import NoseTestSuiteRunner
17 14
     test_runner = NoseTestSuiteRunner(verbosity=options.verbosity,
18  
-                                      pdb=options.pdb)
  15
+                                      pdb=options.pdb,
  16
+                                      )
19 17
     if not test_args:
20 18
         test_args = ['tests']
21 19
     num_failures = test_runner.run_tests(test_args)
@@ -33,6 +31,12 @@ def run_tests(options, *test_args):
33 31
                       action='store_true', help="Whether to drop into PDB on failure/error")
34 32
     (options, args) = parser.parse_args()
35 33
 
  34
+    # If no args, then use 'progressive' plugin to keep the screen real estate
  35
+    # used down to a minimum.  Otherwise, use the spec plugin
  36
+    nose_args = ['-s', '-x',
  37
+                 '--with-progressive' if not args else '--with-spec']
  38
+    configure(nose_args)
  39
+
36 40
     if options.use_coverage:
37 41
         print 'Running tests with coverage'
38 42
         c = coverage(source=['oscar'])
4  tests/config.py
@@ -4,7 +4,7 @@
4 4
 from oscar import OSCAR_CORE_APPS
5 5
 
6 6
 
7  
-def configure():
  7
+def configure(nose_args):
8 8
     if not settings.configured:
9 9
         from oscar.defaults import OSCAR_SETTINGS
10 10
 
@@ -56,6 +56,6 @@ def configure():
56 56
             HAYSTACK_SEARCH_ENGINE='dummy',
57 57
             HAYSTACK_SITECONF = 'oscar.search_sites',
58 58
             APPEND_SLASH=True,
59  
-            NOSE_ARGS=['-s', '-x', '--with-spec'],
  59
+            NOSE_ARGS=nose_args,
60 60
             **OSCAR_SETTINGS
61 61
         )
42  tests/functional/checkout_tests.py
@@ -10,6 +10,7 @@
10 10
 from oscar.apps.basket.models import Basket
11 11
 from oscar.apps.order.models import Order
12 12
 from oscar.apps.address.models import Country
  13
+from oscar.apps.voucher.models import Voucher
13 14
 
14 15
 
15 16
 class CheckoutMixin(object):
@@ -19,8 +20,9 @@ def add_product_to_basket(self):
19 20
         self.client.post(reverse('basket:add'), {'product_id': product.id,
20 21
                                                  'quantity': 1})
21 22
 
22  
-    def add_voucher_to_basket(self):
23  
-        voucher = create_voucher()
  23
+    def add_voucher_to_basket(self, voucher=None):
  24
+        if voucher is None:
  25
+            voucher = create_voucher()
24 26
         self.client.post(reverse('basket:vouchers-add'),
25 27
                          {'code': voucher.code})
26 28
 
@@ -41,7 +43,7 @@ def complete_shipping_address(self):
41 43
                                       'postcode': 'N1 9RT',
42 44
                                       'country': 'GB',
43 45
                                      })
44  
-        self.assertIsRedirect(response)
  46
+        self.assertRedirectUrlName(response, 'checkout:shipping-method')
45 47
 
46 48
     def complete_shipping_method(self):
47 49
         self.client.get(reverse('checkout:shipping-method'))
@@ -205,7 +207,7 @@ def test_ok_response_if_previous_steps_complete(self):
205 207
         self.assertIsOk(response)
206 208
 
207 209
 
208  
-class PaymentDetailsViewTests(ClientTestCase, CheckoutMixin):
  210
+class TestPaymentDetailsView(ClientTestCase, CheckoutMixin):
209 211
 
210 212
     def test_user_must_have_a_nonempty_basket(self):
211 213
         response = self.client.get(reverse('checkout:payment-details'))
@@ -230,12 +232,12 @@ def test_placing_order_with_empty_basket_redirects(self):
230 232
         self.assertRedirectUrlName(response, 'basket:summary')
231 233
 
232 234
 
233  
-class OrderPlacementTests(ClientTestCase, CheckoutMixin):
  235
+class TestOrderPlacement(ClientTestCase, CheckoutMixin):
234 236
 
235 237
     def setUp(self):
236 238
         Order.objects.all().delete()
237 239
 
238  
-        super(OrderPlacementTests, self).setUp()
  240
+        super(TestOrderPlacement, self).setUp()
239 241
         self.basket = Basket.objects.create(owner=self.user)
240 242
         self.basket.add_product(create_product(price=D('12.00')))
241 243
 
@@ -253,19 +255,25 @@ def test_order_is_created(self):
253 255
         self.assertEqual(1, len(orders))
254 256
         
255 257
 
256  
-class TestAnonUserOrderPlacementScenarios(ClientTestCase, CheckoutMixin):
  258
+class TestPlacingOrderUsingAVoucher(ClientTestCase, CheckoutMixin):
257 259
 
258  
-    def test_basic_submission_gets_redirect_to_thankyou(self):
  260
+    def setUp(self):
  261
+        self.login()
259 262
         self.add_product_to_basket()
  263
+        voucher = create_voucher()
  264
+        self.add_voucher_to_basket(voucher)
260 265
         self.complete_shipping_address()
261 266
         self.complete_shipping_method()
262  
-        response = self.submit()
263  
-        self.assertRedirectUrlName(response, 'checkout:thank-you')
  267
+        self.response = self.submit()
264 268
 
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')
  269
+        # Reload voucher
  270
+        self.voucher = Voucher.objects.get(id=voucher.id)
  271
+
  272
+    def test_is_successful(self):
  273
+        self.assertRedirectUrlName(self.response, 'checkout:thank-you')
  274
+
  275
+    def test_records_use(self):
  276
+        self.assertEquals(1, self.voucher.num_orders)
  277
+
  278
+    def test_records_discount(self):
  279
+        self.assertEquals(1, self.voucher.num_orders)
96  tests/functional/dashboard/catalogue_tests.py
... ...
@@ -1,10 +1,6 @@
1 1
 from django.core.urlresolvers import reverse
2  
-from django.test import TestCase
3 2
 
4 3
 from oscar.test import ClientTestCase
5  
-from oscar.apps.catalogue.models import Category
6  
-from oscar.apps.dashboard.catalogue.forms import CategoryForm
7  
-from oscar.apps.catalogue.categories import create_from_breadcrumbs
8 4
 
9 5
 
10 6
 class TestCatalogueViews(ClientTestCase):
@@ -17,95 +13,3 @@ def test_exist(self):
17 13
                ]
18 14
         for url in urls:
19 15
             self.assertIsOk(self.client.get(url))
20  
-
21  
-
22  
-def create_test_category_tree():
23  
-    trail = 'A > B > C'
24  
-    create_from_breadcrumbs(trail)
25  
-    trail = 'A > B > D'
26  
-    create_from_breadcrumbs(trail)
27  
-    trail = 'A > E > F'
28  
-    create_from_breadcrumbs(trail)
29  
-    trail = 'A > E > G'
30  
-    create_from_breadcrumbs(trail)
31  
-
32  
-
33  
-class TestCategoryForm(TestCase):
34  
-
35  
-    def setUp(self):
36  
-        Category.objects.all().delete()
37  
-
38  
-    def test_conflicting_slugs_recognized(self):
39  
-        create_test_category_tree()
40  
-
41  
-        f = CategoryForm()
42  
-
43  
-        #root categories
44  
-        ref_node_pk = Category.objects.get(name='A').pk
45  
-        conflicting = f.is_slug_conflicting('A', ref_node_pk, 'right')
46  
-        self.assertEqual(conflicting, True)
47  
-
48  
-        conflicting = f.is_slug_conflicting('A', None, 'left')
49  
-        self.assertEqual(conflicting, True)
50  
-
51  
-        conflicting = f.is_slug_conflicting('A', None, 'first-child')
52  
-        self.assertEqual(conflicting, True)
53  
-
54  
-        conflicting = f.is_slug_conflicting('B', None, 'left')
55  
-        self.assertEqual(conflicting, False)
56  
-
57  
-        #subcategories
58  
-        ref_node_pk = Category.objects.get(name='C').pk
59  
-        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'left')
60  
-        self.assertEqual(conflicting, True)
61  
-
62  
-        ref_node_pk = Category.objects.get(name='B').pk
63  
-        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'first-child')
64  
-        self.assertEqual(conflicting, True)
65  
-
66  
-        ref_node_pk = Category.objects.get(name='F').pk
67  
-        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'left')
68  
-        self.assertEqual(conflicting, False)
69  
-
70  
-        ref_node_pk = Category.objects.get(name='E').pk
71  
-        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'first-child')
72  
-        self.assertEqual(conflicting, False)
73  
-
74  
-        #updating
75  
-        f.instance = Category.objects.get(name='E')
76  
-        ref_node_pk = Category.objects.get(name='A').pk
77  
-        conflicting = f.is_slug_conflicting('E', ref_node_pk, 'first-child')
78  
-        self.assertEqual(conflicting, False)
79  
-
80  
-
81  
-class CategoryTests(ClientTestCase):
82  
-    is_staff = True
83  
-
84  
-    def setUp(self):
85  
-        super(CategoryTests, self).setUp()
86  
-        create_test_category_tree()
87  
-
88  
-    def test_category_create(self):
89  
-        a = Category.objects.get(name='A')
90  
-        b = Category.objects.get(name='B')
91  
-        c = Category.objects.get(name='C')
92  
-
93  
-        # Redirect to subcategory list view
94  
-        response = self.client.post(reverse('dashboard:catalogue-category-create'),
95  
-                                            {'name': 'Testee',
96  
-                                             '_position': 'left',
97  
-                                             '_ref_node_id': c.id,})
98  
-
99  
-        self.assertIsRedirect(response, reverse('dashboard:catalogue-category-detail-list', 
100  
-                                                args=(b.pk,)))
101  
-
102  
-        # Redirect to main category list view
103  
-        response = self.client.post(reverse('dashboard:catalogue-category-create'),
104  
-                                            {'name': 'Testee',
105  
-                                             '_position': 'right',
106  
-                                             '_ref_node_id': a.id,})
107  
-
108  
-        self.assertIsRedirect(response, reverse('dashboard:catalogue-category-list'))
109  
-
110  
-        self.assertEqual(Category.objects.all().count(), 9)
111  
-
98  tests/functional/dashboard/category_tests.py
... ...
@@ -0,0 +1,98 @@
  1
+from django.core.urlresolvers import reverse
  2
+from django.test import TestCase
  3
+
  4
+from oscar.test import ClientTestCase
  5
+from oscar.apps.catalogue.models import Category
  6
+from oscar.apps.dashboard.catalogue.forms import CategoryForm
  7
+from oscar.apps.catalogue.categories import create_from_breadcrumbs
  8
+
  9
+
  10
+def create_test_category_tree():
  11
+    trail = 'A > B > C'
  12
+    create_from_breadcrumbs(trail)
  13
+    trail = 'A > B > D'
  14
+    create_from_breadcrumbs(trail)
  15
+    trail = 'A > E > F'
  16
+    create_from_breadcrumbs(trail)
  17
+    trail = 'A > E > G'
  18
+    create_from_breadcrumbs(trail)
  19
+
  20
+
  21
+class TestCategoryForm(TestCase):
  22
+
  23
+    def setUp(self):
  24
+        Category.objects.all().delete()
  25
+
  26
+    def test_conflicting_slugs_recognized(self):
  27
+        create_test_category_tree()
  28
+
  29
+        f = CategoryForm()
  30
+
  31
+        #root categories
  32
+        ref_node_pk = Category.objects.get(name='A').pk
  33
+        conflicting = f.is_slug_conflicting('A', ref_node_pk, 'right')
  34
+        self.assertEqual(conflicting, True)
  35
+
  36
+        conflicting = f.is_slug_conflicting('A', None, 'left')
  37
+        self.assertEqual(conflicting, True)
  38
+
  39
+        conflicting = f.is_slug_conflicting('A', None, 'first-child')
  40
+        self.assertEqual(conflicting, True)
  41
+
  42
+        conflicting = f.is_slug_conflicting('B', None, 'left')
  43
+        self.assertEqual(conflicting, False)
  44
+
  45
+        #subcategories
  46
+        ref_node_pk = Category.objects.get(name='C').pk
  47
+        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'left')
  48
+        self.assertEqual(conflicting, True)
  49
+
  50
+        ref_node_pk = Category.objects.get(name='B').pk
  51
+        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'first-child')
  52
+        self.assertEqual(conflicting, True)
  53
+
  54
+        ref_node_pk = Category.objects.get(name='F').pk
  55
+        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'left')
  56
+        self.assertEqual(conflicting, False)
  57
+
  58
+        ref_node_pk = Category.objects.get(name='E').pk
  59
+        conflicting = f.is_slug_conflicting('D', ref_node_pk, 'first-child')
  60
+        self.assertEqual(conflicting, False)
  61
+
  62
+        #updating
  63
+        f.instance = Category.objects.get(name='E')
  64
+        ref_node_pk = Category.objects.get(name='A').pk
  65
+        conflicting = f.is_slug_conflicting('E', ref_node_pk, 'first-child')
  66
+        self.assertEqual(conflicting, False)
  67
+
  68
+
  69
+class CategoryTests(ClientTestCase):
  70
+    is_staff = True
  71
+
  72
+    def setUp(self):
  73
+        super(CategoryTests, self).setUp()
  74
+        create_test_category_tree()
  75
+
  76
+    def test_category_create(self):
  77
+        a = Category.objects.get(name='A')
  78
+        b = Category.objects.get(name='B')
  79
+        c = Category.objects.get(name='C')
  80
+
  81
+        # Redirect to subcategory list view
  82
+        response = self.client.post(reverse('dashboard:catalogue-category-create'),
  83
+                                            {'name': 'Testee',
  84
+                                             '_position': 'left',
  85
+                                             '_ref_node_id': c.id,})
  86
+
  87
+        self.assertIsRedirect(response, reverse('dashboard:catalogue-category-detail-list',
  88
+                                                args=(b.pk,)))
  89
+
  90
+        # Redirect to main category list view
  91
+        response = self.client.post(reverse('dashboard:catalogue-category-create'),
  92
+                                            {'name': 'Testee',
  93
+                                             '_position': 'right',
  94
+                                             '_ref_node_id': a.id,})
  95
+
  96
+        self.assertIsRedirect(response, reverse('dashboard:catalogue-category-list'))
  97
+
  98
+        self.assertEqual(Category.objects.all().count(), 9)
103  tests/functional/dashboard/product_tests.py
... ...
@@ -0,0 +1,103 @@
  1
+from django.core.urlresolvers import reverse
  2
+from django_dynamic_fixture import G
  3
+
  4
+from oscar.test import ClientTestCase
  5
+from oscar.apps.catalogue.models import ProductClass, Category, Product
  6
+
  7
+
  8
+class TestGatewayPage(ClientTestCase):
  9
+    is_staff = True
  10
+
  11
+    def test_redirects_to_list_page_when_no_query_param(self):
  12
+        url = reverse('dashboard:catalogue-product-create')
  13
+        response = self.client.get(url)
  14
+        self.assertRedirectUrlName(response,
  15
+                                   'dashboard:catalogue-product-list')
  16
+
  17
+    def test_redirects_to_list_page_when_invalid_query_param(self):
  18
+        url = reverse('dashboard:catalogue-product-create')
  19
+        response = self.client.get(url + '?product_class=bad')
  20
+        self.assertRedirectUrlName(response,
  21
+                                   'dashboard:catalogue-product-list')
  22
+
  23
+    def test_redirects_to_form_page_when_valid_query_param(self):
  24
+        pclass = G(ProductClass)
  25
+        url = reverse('dashboard:catalogue-product-create')
  26
+        response = self.client.get(url + '?product_class=%d' % pclass.id)
  27
+        self.assertRedirectUrlName(response,
  28
+                                   'dashboard:catalogue-product-create',
  29
+                                   {'product_class_id': pclass.id})
  30
+
  31
+
  32
+class TestCreateGroupProduct(ClientTestCase):
  33
+    is_staff = True
  34
+
  35
+    def setUp(self):
  36
+        self.pclass = G(ProductClass)
  37
+        super(TestCreateGroupProduct, self).setUp()
  38
+
  39
+    def submit(self, **params):
  40
+        data = {'title': 'Nice T-Shirt',
  41
+                'productcategory_set-TOTAL_FORMS': '1',
  42
+                'productcategory_set-INITIAL_FORMS': '0',
  43
+                'productcategory_set-MAX_NUM_FORMS': '',
  44
+                'images-TOTAL_FORMS': '2',
  45
+                'images-INITIAL_FORMS': '0',
  46
+                'images-MAX_NUM_FORMS': '',
  47
+               }
  48
+        data.update(params)
  49
+        url = reverse('dashboard:catalogue-product-create',
  50
+                      kwargs={'product_class_id': self.pclass.id})
  51
+        return self.client.post(url, data)
  52
+
  53
+    def test_title_is_required(self):
  54
+        response = self.submit(title='')
  55
+        self.assertIsOk(response)
  56
+
  57
+    def test_requires_a_category(self):
  58
+        response = self.submit()
  59
+        self.assertIsOk(response)
  60
+
  61
+    def test_doesnt_smoke(self):
  62
+        category = G(Category)
  63
+        data = {
  64
+            'productcategory_set-0-category': category.id,
  65
+            'productcategory_set-0-id': '',
  66
+            'productcategory_set-0-product': '',
  67
+        }
  68
+        response = self.submit(**data)
  69
+        self.assertRedirectUrlName(response, 'dashboard:catalogue-product-list')
  70
+
  71
+
  72
+class TestCreateChildProduct(ClientTestCase):
  73
+    is_staff = True
  74
+
  75
+    def setUp(self):
  76
+        self.pclass = G(ProductClass)
  77
+        self.parent = G(Product)
  78
+        super(TestCreateChildProduct, self).setUp()
  79
+
  80
+    def submit(self, **params):
  81
+        data = {'title': 'Nice T-Shirt',
  82
+                'productcategory_set-TOTAL_FORMS': '1',
  83
+                'productcategory_set-INITIAL_FORMS': '0',
  84
+                'productcategory_set-MAX_NUM_FORMS': '',
  85
+                'images-TOTAL_FORMS': '2',
  86
+                'images-INITIAL_FORMS': '0',
  87
+                'images-MAX_NUM_FORMS': '',
  88
+               }
  89
+        data.update(params)
  90
+        url = reverse('dashboard:catalogue-product-create',
  91
+                      kwargs={'product_class_id': self.pclass.id})
  92
+        return self.client.post(url, data)
  93
+
  94
+    def test_categories_are_not_required(self):
  95
+        category = G(Category)
  96
+        data = {
  97
+            'parent': self.parent.id,
  98
+            'productcategory_set-0-category': category.id,
  99
+            'productcategory_set-0-id': '',
  100
+            'productcategory_set-0-product': '',
  101
+        }
  102
+        response = self.submit(**data)
  103
+        self.assertIsOk(response)
0  tests/unit/dashboard/cataogue_form_tests.py → tests/unit/dashboard/catalogue_form_tests.py
File renamed without changes
70  tests/unit/voucher_tests.py
... ...
@@ -1,11 +1,36 @@
1 1
 import datetime
  2
+from decimal import Decimal as D
2 3
 
3 4
 from django.test import TestCase
  5
+from django.core import exceptions
  6
+from django.contrib.auth.models import User
  7
+from django_dynamic_fixture import G
4 8
 
5 9
 from oscar.apps.voucher.models import Voucher
  10
+from oscar.apps.order.models import Order
  11
+
  12
+START_DATE = datetime.date(2011, 01, 01)
  13
+END_DATE = datetime.date(2012, 01, 01)
6 14
 
7 15
 
8