Browse Source

Rework the clean() method of the add-to-basket form

This fixes a bug with adding customised products to the basket, where
the clean method would get a MultipleObjectsReturned exception as the
lookup wasn't taking options into account.

Also fixed a display issue with required options in basket forms.
master
David Winterbottom 13 years ago
parent
commit
61ee11cd31

+ 14
- 0
oscar/apps/basket/abstract_models.py View File

@@ -393,6 +393,10 @@ class AbstractBasket(models.Model):
393 393
     def contains_a_voucher(self):
394 394
         return self.vouchers.all().count() > 0
395 395
 
396
+    # =============
397
+    # Query methods
398
+    # =============
399
+
396 400
     def contains_voucher(self, code):
397 401
         """
398 402
         Test whether the basket contains a voucher with a given code
@@ -404,6 +408,16 @@ class AbstractBasket(models.Model):
404 408
         else:
405 409
             return True
406 410
 
411
+    def line_quantity(self, product, options=None):
412
+        """
413
+        Return the current quantity of a specific product and options
414
+        """
415
+        ref = self._create_line_reference(product, options)
416
+        try:
417
+            return self.lines.get(line_reference=ref).quantity
418
+        except ObjectDoesNotExist:
419
+            return 0
420
+
407 421
 
408 422
 class AbstractLine(models.Model):
409 423
     """

+ 21
- 9
oscar/apps/basket/forms.py View File

@@ -126,7 +126,20 @@ class AddToBasketForm(forms.Form):
126 126
             else:
127 127
                 self._create_product_fields(instance)
128 128
 
129
+    def cleaned_options(self):
130
+        """
131
+        Return submitted options in a clean format
132
+        """
133
+        options = []
134
+        for option in self.instance.options:
135
+            if option.code in self.cleaned_data:
136
+                options.append({
137
+                    'option': option,
138
+                    'value': self.cleaned_data[option.code]})
139
+        return options
140
+
129 141
     def clean(self):
142
+        # Check product exists
130 143
         try:
131 144
             product = Product.objects.get(
132 145
                 id=self.cleaned_data.get('product_id', None))
@@ -134,13 +147,9 @@ class AddToBasketForm(forms.Form):
134 147
             raise forms.ValidationError(
135 148
                 _("Please select a valid product"))
136 149
 
137
-        qty = self.cleaned_data.get('quantity', 1)
138
-        try:
139
-            line = self.basket.lines.get(product=product)
140
-        except Line.DoesNotExist:
141
-            desired_qty = qty
142
-        else:
143
-            desired_qty = qty + line.quantity
150
+        current_qty = self.basket.line_quantity(product,
151
+                                                self.cleaned_options())
152
+        desired_qty = current_qty + self.cleaned_data.get('quantity', 1)
144 153
 
145 154
         is_available, reason = product.is_purchase_permitted(
146 155
             user=self.user, quantity=desired_qty)
@@ -183,7 +192,9 @@ class AddToBasketForm(forms.Form):
183 192
                                                       label=_("Variant"))
184 193
 
185 194
     def _create_product_fields(self, item):
186
-        """Add the product option fields."""
195
+        """
196
+        Add the product option fields.
197
+        """
187 198
         for option in item.options:
188 199
             self._add_option_field(item, option)
189 200
 
@@ -194,7 +205,8 @@ class AddToBasketForm(forms.Form):
194 205
         This is designed to be overridden so that specific widgets can be used
195 206
         for certain types of options.
196 207
         """
197
-        self.fields[option.code] = forms.CharField()
208
+        kwargs = {'required': option.is_required}
209
+        self.fields[option.code] = forms.CharField(**kwargs)
198 210
 
199 211
 
200 212
 class SimpleAddToBasketForm(AddToBasketForm):

+ 2
- 7
oscar/apps/basket/views.py View File

@@ -173,14 +173,9 @@ class BasketAddView(FormView):
173 173
         return url
174 174
 
175 175
     def form_valid(self, form):
176
-        options = []
177
-        for option in form.instance.options:
178
-            if option.code in form.cleaned_data:
179
-                options.append({
180
-                    'option': option,
181
-                    'value': form.cleaned_data[option.code]})
182 176
         self.request.basket.add_product(
183
-            form.instance, form.cleaned_data['quantity'], options)
177
+            form.instance, form.cleaned_data['quantity'],
178
+            form.cleaned_options())
184 179
         messages.success(self.request, self.get_success_message(form))
185 180
 
186 181
         # Send signal for basket addition

+ 11
- 5
oscar/apps/catalogue/abstract_models.py View File

@@ -777,15 +777,16 @@ class AbstractAttributeEntityType(models.Model):
777 777
 
778 778
 
779 779
 class AbstractOption(models.Model):
780
-    u"""
780
+    """
781 781
     An option that can be selected for a particular item when the product
782 782
     is added to the basket.
783 783
 
784
-    Eg a list ID for an SMS message send, or a personalised message to
784
+    For example,  a list ID for an SMS message send, or a personalised message to
785 785
     print on a T-shirt.
786 786
 
787
-    This is not the same as an attribute as options do not have a fixed value for
788
-    a particular item - options, they need to be specified by the customer.
787
+    This is not the same as an 'attribute' as options do not have a fixed value
788
+    for a particular item.  Instead, option need to be specified by a customer
789
+    when add the item to their basket.
789 790
     """
790 791
     name = models.CharField(_("Name"), max_length=128)
791 792
     code = models.SlugField(_("Code"), max_length=128)
@@ -795,7 +796,8 @@ class AbstractOption(models.Model):
795 796
         (REQUIRED, _("Required - a value for this option must be specified")),
796 797
         (OPTIONAL, _("Optional - a value for this option can be omitted")),
797 798
     )
798
-    type = models.CharField(_("Status"), max_length=128, default=REQUIRED, choices=TYPE_CHOICES)
799
+    type = models.CharField(_("Status"), max_length=128, default=REQUIRED,
800
+                            choices=TYPE_CHOICES)
799 801
 
800 802
     class Meta:
801 803
         abstract = True
@@ -810,6 +812,10 @@ class AbstractOption(models.Model):
810 812
             self.code = slugify(self.name)
811 813
         super(AbstractOption, self).save(*args, **kwargs)
812 814
 
815
+    @property
816
+    def is_required(self):
817
+        return self.type == self.REQUIRED
818
+
813 819
 
814 820
 class MissingProductImage(object):
815 821
 

+ 2
- 4
oscar/templatetags/basket_tags.py View File

@@ -53,8 +53,6 @@ class BasketFormNode(template.Node):
53 53
             initial = {}
54 54
             if not product.is_group:
55 55
                 initial['product_id'] = product.id
56
-            context[self.form_var] = self.form_class(basket,
57
-                                                     user=None,
58
-                                                     instance=product,
59
-                                                     initial=initial)
56
+            context[self.form_var] = self.form_class(
57
+                basket, user=None, instance=product, initial=initial)
60 58
         return ''

+ 17
- 1
tests/unit/basket_tests.py View File

@@ -9,9 +9,10 @@ from oscar.apps.basket.middleware import BasketMiddleware
9 9
 from oscar.test.helpers import create_product
10 10
 from oscar.apps.basket.reports import (
11 11
     OpenBasketReportGenerator, SubmittedBasketReportGenerator)
12
+from oscar.apps.catalogue.models import Option
12 13
 
13 14
 
14
-class TestBasketModel(TestCase):
15
+class TestABasket(TestCase):
15 16
 
16 17
     def setUp(self):
17 18
         self.basket = Basket()
@@ -48,6 +49,21 @@ class TestBasketModel(TestCase):
48 49
         self.basket.flush()
49 50
         self.assertEqual(self.basket.num_items, 0)
50 51
 
52
+    def test_returns_correct_quantity_for_missing_product(self):
53
+        self.assertEqual(0, self.basket.line_quantity(self.product))
54
+
55
+    def test_returns_correct_quantity_for_existing_product(self):
56
+        self.basket.add_product(self.product)
57
+        self.assertEqual(1, self.basket.line_quantity(self.product))
58
+
59
+    def test_returns_correct_quantity_for_existing_product_with_options(self):
60
+        option = Option.objects.create(name="Message")
61
+        options = [{"option": option, "value": "2"}]
62
+        self.basket.add_product(self.product, options=options)
63
+        self.assertEqual(0, self.basket.line_quantity(self.product))
64
+        self.assertEqual(1, self.basket.line_quantity(self.product, options))
65
+
66
+
51 67
 
52 68
 class TestBasketMiddleware(TestCase):
53 69
 

Loading…
Cancel
Save