Repository: django-oscar/django-oscar

Issue:
Cannot edit attribute values when the product class has an `entity` type attribute
To reproduce (I just tested on repo's master branch, but I found it on the 1.1.1 release)
1. Create a product class with an a `text` type attribute and a `entity` type attribute.
2. Create a product of this class and set some value for the `text` attribute.
3. Save.
4. Set the text attribute to a different value.
5. Save it, but the value won't get updated.

It seems that the existence of an entity type attribute on the product class messes something up



Discussion:

Resolving patch:
diff --git a/src/oscar/apps/catalogue/product_attributes.py b/src/oscar/apps/catalogue/product_attributes.py
--- a/src/oscar/apps/catalogue/product_attributes.py
+++ b/src/oscar/apps/catalogue/product_attributes.py
@@ -19,12 +19,15 @@ def __init__(self, product):
         self.product = product
         self.initialised = False
 
+    def initiate_attributes(self):
+        values = self.get_values().select_related('attribute')
+        for v in values:
+            setattr(self, v.attribute.code, v.value)
+        self.initialised = True
+
     def __getattr__(self, name):
         if not name.startswith('_') and not self.initialised:
-            values = self.get_values().select_related('attribute')
-            for v in values:
-                setattr(self, v.attribute.code, v.value)
-            self.initialised = True
+            self.initiate_attributes()
             return getattr(self, name)
         raise AttributeError(
             _("%(obj)s has no attribute named '%(attr)s'") % {
diff --git a/src/oscar/apps/dashboard/catalogue/forms.py b/src/oscar/apps/dashboard/catalogue/forms.py
--- a/src/oscar/apps/dashboard/catalogue/forms.py
+++ b/src/oscar/apps/dashboard/catalogue/forms.py
@@ -329,8 +329,8 @@ def _post_clean(self):
         Set attributes before ModelForm calls the product's clean method
         (which it does in _post_clean), which in turn validates attributes.
         """
-        product_class = self.instance.get_product_class()
-        for attribute in product_class.attributes.all():
+        self.instance.attr.initiate_attributes()
+        for attribute in self.instance.attr.get_all_attributes():
             field_name = 'attr_%s' % attribute.code
             # An empty text field won't show up in cleaned_data.
             if field_name in self.cleaned_data:


Test patch:
diff --git a/tests/functional/dashboard/product_tests.py b/tests/functional/dashboard/product_tests.py
--- a/tests/functional/dashboard/product_tests.py
+++ b/tests/functional/dashboard/product_tests.py
@@ -1,11 +1,14 @@
+import datetime
+
 from django.core.urlresolvers import reverse
 from oscar.test import factories
 
 from oscar.test.testcases import WebTestCase
 from oscar.core.compat import get_user_model
-from oscar.apps.catalogue.models import Product
+from oscar.apps.catalogue.models import Product, ProductAttribute
 from oscar.test.factories import (
-    CategoryFactory, ProductFactory, ProductClassFactory)
+    CategoryFactory, ProductFactory, ProductAttributeFactory,
+    ProductClassFactory)
 
 User = get_user_model()
 
@@ -145,3 +148,64 @@ def test_product_update_form(self):
 
         self.assertEqual(page.context['product'], self.product)
         self.assertEqual(product.title, expected_title)
+
+
+class TestProductClass(ProductWebTest):
+    def setUp(self):
+        super(TestProductClass, self).setUp()
+        self.pclass = ProductClassFactory(name='T-Shirts', slug='tshirts')
+
+        for attribute_type, __ in ProductAttribute.TYPE_CHOICES:
+            values = {
+                'type': attribute_type, 'code': attribute_type,
+                'product_class': self.pclass, 'name': attribute_type,
+            }
+            if attribute_type == ProductAttribute.OPTION:
+                option_group = factories.AttributeOptionGroupFactory()
+                self.option = factories.AttributeOptionFactory(group=option_group)
+                values['option_group'] = option_group
+            ProductAttributeFactory(**values)
+        self.product = factories.ProductFactory(product_class=self.pclass)
+        self.url = reverse('dashboard:catalogue-product',
+                           kwargs={'pk': self.product.id})
+
+    def test_product_update_attribute_values(self):
+        page = self.get(self.url)
+        product_form = page.form
+        # Send string field values due to an error
+        # in the Webtest during multipart form encode.
+        product_form['attr_text'] = 'test1'
+        product_form['attr_integer'] = '1'
+        product_form['attr_float'] = '1.2'
+        product_form['attr_boolean'] = 'yes'
+        product_form['attr_richtext'] = 'longread'
+        product_form['attr_date'] = '2016-10-12'
+        product_form.submit()
+
+        # Reloading model instance to re-initiate ProductAttributeContainer
+        # with new attributes.
+        self.product = Product.objects.get(pk=self.product.id)
+        self.assertEquals(self.product.attr.text, 'test1')
+        self.assertEquals(self.product.attr.integer, 1)
+        self.assertEquals(self.product.attr.float, 1.2)
+        self.assertTrue(self.product.attr.boolean)
+        self.assertEquals(self.product.attr.richtext, 'longread')
+        self.assertEquals(self.product.attr.date, datetime.date(2016, 10, 12))
+
+        page = self.get(self.url)
+        product_form = page.form
+        product_form['attr_text'] = 'test2'
+        product_form['attr_integer'] = '2'
+        product_form['attr_float'] = '5.2'
+        product_form['attr_boolean'] = ''
+        product_form['attr_richtext'] = 'article'
+        product_form['attr_date'] = '2016-10-10'
+        product_form.submit()
+
+        self.product = Product.objects.get(pk=self.product.id)
+        self.assertEquals(self.product.attr.text, 'test2')
+        self.assertEquals(self.product.attr.integer, 2)
+        self.assertEquals(self.product.attr.float, 5.2)
+        self.assertFalse(self.product.attr.boolean)
+        self.assertEquals(self.product.attr.richtext, 'article')
+        self.assertEquals(self.product.attr.date, datetime.date(2016, 10, 10))