Browse Source

Add OSCAR_MAX_BASKET_QUANTITY_THRESHOLD

master
Patryk Zawadzki 14 years ago
parent
commit
8a38a5ac13
4 changed files with 106 additions and 64 deletions
  1. 33
    15
      oscar/apps/basket/forms.py
  2. 39
    18
      oscar/apps/basket/tests.py
  3. 32
    31
      oscar/apps/basket/views.py
  4. 2
    0
      oscar/defaults.py

+ 33
- 15
oscar/apps/basket/forms.py View File

@@ -1,4 +1,5 @@
1 1
 from django import forms
2
+from django.conf import settings
2 3
 from django.db.models import get_model
3 4
 from django.utils.translation import gettext_lazy as _
4 5
 
@@ -9,7 +10,7 @@ Product = get_model('catalogue', 'product')
9 10
 
10 11
 class BasketLineForm(forms.ModelForm):
11 12
     save_for_later = forms.BooleanField(initial=False, required=False)
12
-    
13
+
13 14
     class Meta:
14 15
         model = basketline_model
15 16
         exclude = ('basket', 'product', 'line_reference', )
@@ -17,7 +18,7 @@ class BasketLineForm(forms.ModelForm):
17 18
 
18 19
 class SavedLineForm(forms.ModelForm):
19 20
     move_to_basket = forms.BooleanField(initial=False, required=False)
20
-    
21
+
21 22
     class Meta:
22 23
         model = basketline_model
23 24
         exclude = ('basket', 'product', 'line_reference', 'quantity', )
@@ -25,14 +26,14 @@ class SavedLineForm(forms.ModelForm):
25 26
 
26 27
 class BasketVoucherForm(forms.Form):
27 28
     code = forms.CharField(max_length=128)
28
-    
29
+
29 30
     def __init__(self, *args, **kwargs):
30 31
         return super(BasketVoucherForm, self).__init__(*args,**kwargs)
31
-        
32
+
32 33
 
33 34
 class ProductSelectionForm(forms.Form):
34 35
     product_id = forms.IntegerField(min_value=1)
35
-    
36
+
36 37
     def clean_product_id(self):
37 38
         id = self.cleaned_data['product_id']
38 39
 
@@ -45,16 +46,17 @@ class ProductSelectionForm(forms.Form):
45 46
 class AddToBasketForm(forms.Form):
46 47
     product_id = forms.IntegerField(widget=forms.HiddenInput(), min_value=1)
47 48
     quantity = forms.IntegerField(initial=1, min_value=1)
48
-    
49
-    def __init__(self, instance, *args, **kwargs):
49
+
50
+    def __init__(self, basket, instance, *args, **kwargs):
50 51
         super(AddToBasketForm, self).__init__(*args, **kwargs)
52
+        self.basket = basket
51 53
         self.instance = instance
52 54
         if instance:
53 55
             if instance.is_group:
54 56
                 self._create_group_product_fields(instance)
55 57
             else:
56 58
                 self._create_product_fields(instance)
57
-    
59
+
58 60
     def clean_product_id(self):
59 61
         id = self.cleaned_data['product_id']
60 62
         product = Product.objects.get(id=id)
@@ -62,6 +64,22 @@ class AddToBasketForm(forms.Form):
62 64
             raise forms.ValidationError(_("This product is not available for purchase"))
63 65
         return id
64 66
 
67
+    def clean_quantity(self):
68
+        qty = self.cleaned_data['quantity']
69
+        basket_threshold = settings.OSCAR_MAX_BASKET_QUANTITY_THRESHOLD
70
+        if basket_threshold:
71
+            total_basket_quantity = self.basket.num_items
72
+            max_allowed = basket_threshold - total_basket_quantity
73
+            if qty > max_allowed:
74
+                raise forms.ValidationError(
75
+                    _("Due to technical limitations we are not able to ship"
76
+                      " more than %(threshold)d items in one order. Your basket"
77
+                      " currently has %(basket)d items.") % {
78
+                            'threshold': basket_threshold,
79
+                            'basket': total_basket_quantity,
80
+                    })
81
+        return qty
82
+
65 83
     def _create_group_product_fields(self, item):
66 84
         """
67 85
         Adds the fields for a "group"-type product (eg, a parent product with a
@@ -70,23 +88,23 @@ class AddToBasketForm(forms.Form):
70 88
         choices = []
71 89
         for variant in item.variants.all():
72 90
             if variant.has_stockrecord:
73
-                summary = u"%s (%s) - %.2f" % (variant.get_title(), variant.attribute_summary(), 
91
+                summary = u"%s (%s) - %.2f" % (variant.get_title(), variant.attribute_summary(),
74 92
                                                variant.stockrecord.price_incl_tax)
75 93
                 choices.append((variant.id, summary))
76 94
         self.fields['product_id'] = forms.ChoiceField(choices=tuple(choices))
77
-    
95
+
78 96
     def _create_product_fields(self, item):
79 97
         u"""Add the product option fields."""
80 98
         for option in item.options:
81 99
             self._add_option_field(item, option)
82
-    
100
+
83 101
     def _add_option_field(self, item, option):
84 102
         u"""
85 103
         Creates the appropriate form field for the product option.
86
-        
87
-        This is designed to be overridden so that specific widgets can be used for 
104
+
105
+        This is designed to be overridden so that specific widgets can be used for
88 106
         certain types of options.
89 107
         """
90
-        self.fields[option.code] = forms.CharField()    
91
-    
108
+        self.fields[option.code] = forms.CharField()
109
+
92 110
 

+ 39
- 18
oscar/apps/basket/tests.py View File

@@ -1,10 +1,10 @@
1 1
 from decimal import Decimal as D
2 2
 
3
-from django.utils import unittest
4
-from django.test.client import Client
3
+from django.conf import settings
5 4
 from django.core.urlresolvers import reverse
5
+from django.test import TestCase
6 6
 
7
-from oscar.apps.basket.models import Basket, Line 
7
+from oscar.apps.basket.models import Basket, Line
8 8
 from oscar.test.helpers import create_product, TwillTestCase
9 9
 
10 10
 
@@ -17,42 +17,39 @@ class ViewTest(TwillTestCase):
17 17
         self.assertPageTitleMatches('Oscar')
18 18
 
19 19
 
20
-class BasketModelTest(unittest.TestCase):
21
-    
20
+class BasketModelTest(TestCase):
21
+
22 22
     def setUp(self):
23 23
         self.basket = Basket.objects.create()
24 24
         self.dummy_product = create_product()
25
-    
25
+
26 26
     def test_empty_baskets_have_zero_lines(self):
27 27
         self.assertTrue(Basket().num_lines == 0)
28
-        
28
+
29 29
     def test_new_baskets_are_empty(self):
30 30
         self.assertTrue(Basket().is_empty)
31
-        
31
+
32 32
     def test_basket_have_with_one_line(self):
33 33
         Line.objects.create(basket=self.basket, product=self.dummy_product)
34 34
         self.assertTrue(self.basket.num_lines == 1)
35
-        
35
+
36 36
     def test_add_product_creates_line(self):
37 37
         self.basket.add_product(self.dummy_product)
38 38
         self.assertTrue(self.basket.num_lines == 1)
39
-        
39
+
40 40
     def test_adding_multiproduct_line_returns_correct_number_of_items(self):
41 41
         self.basket.add_product(self.dummy_product, 10)
42 42
         self.assertEqual(self.basket.num_items, 10)
43
-       
44
-        
45
-class BasketViewsTest(unittest.TestCase):
46
-    
47
-    def setUp(self):
48
-        self.client = Client()
49
-    
43
+
44
+
45
+class BasketViewsTest(TestCase):
46
+
50 47
     def test_empty_basket_view(self):
51 48
         url = reverse('basket:summary')
52 49
         response = self.client.get(url)
53 50
         self.assertEquals(200, response.status_code)
54 51
         self.assertEquals(0, response.context['basket'].num_lines)
55
-        
52
+
56 53
     def test_anonymous_add_to_basket_creates_cookie(self):
57 54
         dummy_product = create_product(price=D('10.00'))
58 55
         url = reverse('basket:add')
@@ -61,3 +58,27 @@ class BasketViewsTest(unittest.TestCase):
61 58
                        'quantity': 1}
62 59
         response = self.client.post(url, post_params)
63 60
         self.assertTrue('oscar_open_basket' in response.cookies)
61
+
62
+class BasketThresholdTest(TestCase):
63
+
64
+    def setUp(self):
65
+        self._old_threshold = settings.OSCAR_MAX_BASKET_QUANTITY_THRESHOLD
66
+        settings.OSCAR_MAX_BASKET_QUANTITY_THRESHOLD = 3
67
+
68
+    def tearDown(self):
69
+        settings.OSCAR_MAX_BASKET_QUANTITY_THRESHOLD = self._old_threshold
70
+
71
+    def test_adding_more_than_threshold_raises(self):
72
+        dummy_product = create_product(price=D('10.00'))
73
+        url = reverse('basket:add')
74
+        post_params = {'product_id': dummy_product.id,
75
+                       'action': 'add',
76
+                       'quantity': 2}
77
+        response = self.client.post(url, post_params)
78
+        self.assertTrue('oscar_open_basket' in response.cookies)
79
+        post_params = {'product_id': dummy_product.id,
80
+                       'action': 'add',
81
+                       'quantity': 2}
82
+        response = self.client.post(url, post_params)
83
+        self.assertTrue('Your basket currently has 2 items.' in
84
+                        response.cookies['messages'].value)

+ 32
- 31
oscar/apps/basket/views.py View File

@@ -22,14 +22,14 @@ class BasketView(ModelFormSetView):
22 22
     extra = 0
23 23
     can_delete = True
24 24
     template_name='basket/basket.html'
25
-    
25
+
26 26
     def get_queryset(self):
27 27
         return self.request.basket.lines.all()
28
-    
28
+
29 29
     def get_context_data(self, **kwargs):
30 30
         context = super(BasketView, self).get_context_data(**kwargs)
31 31
         context['voucher_form'] = BasketVoucherForm()
32
-        
32
+
33 33
         if self.request.user.is_authenticated():
34 34
             try:
35 35
                 saved_basket = self.basket_model.saved.get(owner=self.request.user)
@@ -40,7 +40,7 @@ class BasketView(ModelFormSetView):
40 40
             except self.basket_model.DoesNotExist:
41 41
                 pass
42 42
         return context
43
-    
43
+
44 44
     def get_success_url(self):
45 45
         return self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
46 46
 
@@ -51,11 +51,11 @@ class BasketView(ModelFormSetView):
51 51
                 line = form.instance
52 52
                 if self.request.user.is_authenticated():
53 53
                     self.move_line_to_saved_basket(line)
54
-                    messages.info(self.request, _(u"'%(title)s' has been saved for later" % {'title': line.product}))   
54
+                    messages.info(self.request, _(u"'%(title)s' has been saved for later" % {'title': line.product}))
55 55
                 else:
56 56
                     needs_auth = True
57 57
         if needs_auth:
58
-            messages.error(self.request, "You can't save an item for later if you're not logged in!")     
58
+            messages.error(self.request, "You can't save an item for later if you're not logged in!")
59 59
         return super(BasketView, self).formset_valid(formset)
60 60
 
61 61
     def move_line_to_saved_basket(self, line):
@@ -69,27 +69,28 @@ class BasketView(ModelFormSetView):
69 69
 
70 70
 class BasketAddView(FormView):
71 71
     """
72
-    Handles the add-to-basket operation, shouldn't be accessed via 
72
+    Handles the add-to-basket operation, shouldn't be accessed via
73 73
     GET because there's nothing sensible to render.
74 74
     """
75 75
     form_class = AddToBasketForm
76 76
     product_select_form_class = ProductSelectionForm
77 77
     product_model = get_model('catalogue', 'product')
78 78
     add_signal = basket_addition
79
-    
79
+
80 80
     def get(self, request, *args, **kwargs):
81 81
         return HttpResponseRedirect(reverse('basket:summary'))
82
-    
83
-    def get_form_kwargs(self): 
82
+
83
+    def get_form_kwargs(self):
84 84
         kwargs = super(BasketAddView, self).get_form_kwargs()
85 85
         product_select_form = self.product_select_form_class(self.request.POST)
86 86
 
87 87
         if product_select_form.is_valid():
88 88
             kwargs['instance'] = product_select_form.cleaned_data['product_id']
89 89
         else:
90
-             raise Http404()
90
+            raise Http404()
91
+        kwargs['basket'] = self.request.basket
91 92
         return kwargs
92
-    
93
+
93 94
     def get_success_url(self):
94 95
         return self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
95 96
 
@@ -100,18 +101,18 @@ class BasketAddView(FormView):
100 101
                 options.append({'option': option, 'value': form.cleaned_data[option.code]})
101 102
         self.request.basket.add_product(form.instance, form.cleaned_data['quantity'], options)
102 103
         messages.info(self.request, _(u"'%(title)s' (quantity %(quantity)d) has been added to your basket" %
103
-                {'title': form.instance.get_title(), 
104
+                {'title': form.instance.get_title(),
104 105
                  'quantity': form.cleaned_data['quantity']}))
105
-        
106
+
106 107
         # Send signal for basket addition
107 108
         self.add_signal.send(sender=self, product=form.instance, user=self.request.user)
108
-        
109
+
109 110
         return super(BasketAddView, self).form_valid(form)
110
-    
111
+
111 112
     def form_invalid(self, form):
112 113
         msgs = []
113 114
         for error in form.errors.values():
114
-            msgs.append(error.as_text()) 
115
+            msgs.append(error.as_text())
115 116
         messages.error(self.request, ",".join(msgs))
116 117
         return HttpResponseRedirect(self.request.META.get('HTTP_REFERER',reverse('basket:summary')))
117 118
 
@@ -120,7 +121,7 @@ class VoucherView(ListView):
120 121
     model = get_model('voucher', 'voucher')
121 122
     can_delete = True
122 123
     extra = 0
123
-    
124
+
124 125
     def get_queryset(self):
125 126
         self.request.basket.vouchers.all()
126 127
 
@@ -128,28 +129,28 @@ class VoucherView(ListView):
128 129
 class VoucherAddView(FormView):
129 130
     form_class = BasketVoucherForm
130 131
     voucher_model = get_model('voucher', 'voucher')
131
-    
132
+
132 133
     def get(self, request, *args, **kwargs):
133 134
         return HttpResponseRedirect(reverse('basket:summary'))
134
-    
135
+
135 136
     def apply_voucher_to_basket(self, voucher):
136 137
         if not voucher.is_active():
137 138
             messages.error(self.request, _("The '%(code)s' voucher has expired" % {'code': voucher.code}))
138 139
             return
139
-        
140
+
140 141
         is_available, message = voucher.is_available_to_user(self.request.user)
141 142
         if not is_available:
142 143
             messages.error(self.request, message)
143 144
             return
144
-        
145
+
145 146
         self.request.basket.vouchers.add(voucher)
146
-        
147
+
147 148
         # Recalculate discounts to see if the voucher gives any
148 149
         discounts_before = self.request.basket.get_discounts()
149 150
         self.request.basket.remove_discounts()
150 151
         Applicator().apply(self.request, self.request.basket)
151 152
         discounts_after = self.request.basket.get_discounts()
152
-        
153
+
153 154
         # Look for discounts from this new voucher
154 155
         found_discount = False
155 156
         for discount in discounts_after:
@@ -161,7 +162,7 @@ class VoucherAddView(FormView):
161 162
             self.request.basket.vouchers.remove(voucher)
162 163
         else:
163 164
             messages.info(self.request, _("Voucher '%(code)s' added to basket" % {'code': voucher.code}))
164
-    
165
+
165 166
     def form_valid(self, form):
166 167
         code = form.cleaned_data['code']
167 168
         if not self.request.basket.id:
@@ -173,20 +174,20 @@ class VoucherAddView(FormView):
173 174
                 voucher = self.voucher_model._default_manager.get(code=code)
174 175
             except self.voucher_model.DoesNotExist:
175 176
                 messages.error(self.request, _("No voucher found with code '%(code)s'" % {'code': code}))
176
-            else:        
177
+            else:
177 178
                 self.apply_voucher_to_basket(voucher)
178 179
         return HttpResponseRedirect(self.request.META.get('HTTP_REFERER', reverse('basket:summary')))
179 180
 
180 181
     def form_invalid(self, form):
181 182
         return HttpResponseRedirect(reverse('basket:summary'))
182
-    
183
+
183 184
 
184 185
 class VoucherRemoveView(View):
185 186
     voucher_model = get_model('voucher', 'voucher')
186
-    
187
+
187 188
     def get(self, request, *args, **kwargs):
188 189
         return HttpResponseRedirect(reverse('basket:summary'))
189
-    
190
+
190 191
     def post(self, request, *args, **kwargs):
191 192
         voucher_id = int(kwargs.pop('pk'))
192 193
         if not request.basket.id:
@@ -220,7 +221,7 @@ class SavedView(ModelFormSetView):
220 221
             return saved_basket.lines.all().select_related('product', 'product__stockrecord')
221 222
         except self.basket_model.DoesNotExist:
222 223
             return []
223
-        
224
+
224 225
     def get_success_url(self):
225 226
         return self.request.META.get('HTTP_REFERER', reverse('basket:summary'))
226 227
 
@@ -228,7 +229,7 @@ class SavedView(ModelFormSetView):
228 229
         for form in formset:
229 230
             if form.cleaned_data['move_to_basket']:
230 231
                 msg = "'%s' has been moved back to your basket" % form.instance.product
231
-                messages.info(self.request, msg)                
232
+                messages.info(self.request, msg)
232 233
                 real_basket = self.request.basket
233 234
                 real_basket.merge_line(form.instance)
234 235
         return super(SavedView, self).formset_valid(formset)

+ 2
- 0
oscar/defaults.py View File

@@ -42,3 +42,5 @@ OSCAR_FROM_EMAIL = 'oscar@example.com'
42 42
 # Offers
43 43
 OSCAR_OFFER_BLACKLIST_PRODUCT = None
44 44
 
45
+# Max total number of items in basket
46
+OSCAR_MAX_BASKET_QUANTITY_THRESHOLD = None

Loading…
Cancel
Save