Explorar el Código

Avoid unexpected behaviour when saving modified child attributes. (#4007)

This fixes the issue where modifying child properties would have no effect in
the dashboard, if the parent would have the same property. This is not directly
obvious in the dashboard context. Allowing child attributes to be saved if they
are directly modified even if they are the same, avoid confusion.
master
Voxin Muyli hace 2 años
padre
commit
71405c508a
No account linked to committer's email address

+ 45
- 21
src/oscar/apps/catalogue/product_attributes.py Ver fichero

@@ -19,29 +19,42 @@ class ProductAttributesContainer:
19 19
         self.__dict__ = state
20 20
 
21 21
     def __init__(self, product):
22
-        self._product = product
23
-        self._initialized = False
22
+        # use __dict__ directly to avoid triggering __setattr__, which would
23
+        # cause a recursion error on _initialized.
24
+        self.__dict__.update(
25
+            {"_product": product, "_initialized": False, "_dirty": set()}
26
+        )
24 27
 
25 28
     @property
26 29
     def product(self):
27 30
         return self._product
28 31
 
32
+    @property
33
+    def initialized(self):
34
+        return self._initialized
35
+
36
+    @initialized.setter
37
+    def initialized(self, value):
38
+        # use __dict__ directly to avoid triggering __setattr__, which would
39
+        # cause a recursion error.
40
+        self.__dict__["_initialized"] = value
41
+
29 42
     def initialize(self):
30
-        self._initialized = True
43
+        self.initialized = True
31 44
         # initialize should not overwrite any values that have allready been set
32 45
         attrs = self.__dict__
33
-        for v in self.get_values().select_related('attribute'):
46
+        for v in self.get_values().select_related("attribute"):
34 47
             attrs.setdefault(v.attribute.code, v.value)
35 48
 
36 49
     def refresh(self):
37
-        for v in self.get_values().select_related('attribute'):
50
+        for v in self.get_values().select_related("attribute"):
38 51
             setattr(self, v.attribute.code, v.value)
39 52
 
40 53
     def __getattribute__(self, name):
41 54
         try:
42 55
             return super().__getattribute__(name)
43 56
         except AttributeError:
44
-            if self._initialized:
57
+            if self.initialized:
45 58
                 raise
46 59
             else:
47 60
                 self.initialize()
@@ -50,7 +63,13 @@ class ProductAttributesContainer:
50 63
 
51 64
     def __getattr__(self, name):
52 65
         raise AttributeError(
53
-            _("%(obj)s has no attribute named '%(attr)s'") % {'obj': self.product.get_product_class(), 'attr': name})
66
+            _("%(obj)s has no attribute named '%(attr)s'")
67
+            % {"obj": self.product.get_product_class(), "attr": name}
68
+        )
69
+
70
+    def __setattr__(self, name, value):
71
+        self._dirty.add(name)
72
+        super().__setattr__(name, value)
54 73
 
55 74
     def validate_attributes(self):
56 75
         for attribute in self.get_all_attributes():
@@ -58,15 +77,17 @@ class ProductAttributesContainer:
58 77
             if value is None:
59 78
                 if attribute.required:
60 79
                     raise ValidationError(
61
-                        _("%(attr)s attribute cannot be blank") %
62
-                        {'attr': attribute.code})
80
+                        _("%(attr)s attribute cannot be blank")
81
+                        % {"attr": attribute.code}
82
+                    )
63 83
             else:
64 84
                 try:
65 85
                     attribute.validate_value(value)
66 86
                 except ValidationError as e:
67 87
                     raise ValidationError(
68
-                        _("%(attr)s attribute %(err)s") %
69
-                        {'attr': attribute.code, 'err': e})
88
+                        _("%(attr)s attribute %(err)s")
89
+                        % {"attr": attribute.code, "err": e}
90
+                    )
70 91
 
71 92
     def get_values(self):
72 93
         return self.product.get_attribute_values()
@@ -87,15 +108,18 @@ class ProductAttributesContainer:
87 108
         for attribute in self.get_all_attributes():
88 109
             if hasattr(self, attribute.code):
89 110
                 value = getattr(self, attribute.code)
90
-                # Make sure that if a value comes from a parent product, it is not
91
-                # copied to the child, we do this by checking if a value has been
92
-                # changed, which would not be the case if the value comes from the
93
-                # parent.
94
-                try:
95
-                    attribute_value_current = self.get_value_by_attribute(attribute)
96
-                    if attribute_value_current.value == value:
97
-                        continue  # no new value needs to be saved
98
-                except ObjectDoesNotExist:
99
-                    pass  # there is no existing value, so a value needs to be saved.
111
+                if attribute.code not in self._dirty:
112
+                    # Make sure that if a value comes from a parent product, it is not
113
+                    # copied to the child, we do this by checking if a value has been
114
+                    # changed, which would not be the case if the value comes from the
115
+                    # parent.
116
+                    # for attributes are are set explicitly (_dirty), this check is not
117
+                    # needed and should always be saved.
118
+                    try:
119
+                        attribute_value_current = self.get_value_by_attribute(attribute)
120
+                        if attribute_value_current.value == value:
121
+                            continue  # no new value needs to be saved
122
+                    except ObjectDoesNotExist:
123
+                        pass  # there is no existing value, so a value needs to be saved.
100 124
 
101 125
                 attribute.save_value(self.product, value)

+ 24
- 0
tests/unit/catalogue/test_product_attributes.py Ver fichero

@@ -143,6 +143,30 @@ class ProductAttributeTest(TestCase):
143 143
             "The child now has 1 attribute",
144 144
         )
145 145
 
146
+    def test_explicit_identical_child_attribute(self):
147
+        self.assertEqual(self.product.attr.weight, 3, "parent product has weight 3")
148
+        self.assertEqual(self.child_product.attr.weight, 3, "chiuld product also has weight 3")
149
+        self.assertEqual(
150
+            ProductAttributeValue.objects.filter(product_id=self.product.pk).count(),
151
+            1,
152
+            "The parent has 1 attributes, which is the weight",
153
+        )
154
+        self.assertEqual(
155
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
156
+            0,
157
+            "The child has no attributes, because it gets weight from the parent",
158
+        )
159
+        # explicitly set a value to the child
160
+        self.child_product.attr.weight = 3
161
+        self.child_product.full_clean()
162
+        self.child_product.save()
163
+        self.assertEqual(
164
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
165
+            1,
166
+            "The child now has 1 attribute, because we explicitly set the attribute, "
167
+            "so it saved, even when the parent has the same value",
168
+        )
169
+
146 170
 
147 171
 class ProductAttributeQuerysetTest(TestCase):
148 172
     fixtures = ["productattributes"]

Loading…
Cancelar
Guardar