Переглянути джерело

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 2 роки тому
джерело
коміт
71405c508a
Аккаунт користувача з таким Email не знайдено

+ 45
- 21
src/oscar/apps/catalogue/product_attributes.py Переглянути файл

19
         self.__dict__ = state
19
         self.__dict__ = state
20
 
20
 
21
     def __init__(self, product):
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
     @property
28
     @property
26
     def product(self):
29
     def product(self):
27
         return self._product
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
     def initialize(self):
42
     def initialize(self):
30
-        self._initialized = True
43
+        self.initialized = True
31
         # initialize should not overwrite any values that have allready been set
44
         # initialize should not overwrite any values that have allready been set
32
         attrs = self.__dict__
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
             attrs.setdefault(v.attribute.code, v.value)
47
             attrs.setdefault(v.attribute.code, v.value)
35
 
48
 
36
     def refresh(self):
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
             setattr(self, v.attribute.code, v.value)
51
             setattr(self, v.attribute.code, v.value)
39
 
52
 
40
     def __getattribute__(self, name):
53
     def __getattribute__(self, name):
41
         try:
54
         try:
42
             return super().__getattribute__(name)
55
             return super().__getattribute__(name)
43
         except AttributeError:
56
         except AttributeError:
44
-            if self._initialized:
57
+            if self.initialized:
45
                 raise
58
                 raise
46
             else:
59
             else:
47
                 self.initialize()
60
                 self.initialize()
50
 
63
 
51
     def __getattr__(self, name):
64
     def __getattr__(self, name):
52
         raise AttributeError(
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
     def validate_attributes(self):
74
     def validate_attributes(self):
56
         for attribute in self.get_all_attributes():
75
         for attribute in self.get_all_attributes():
58
             if value is None:
77
             if value is None:
59
                 if attribute.required:
78
                 if attribute.required:
60
                     raise ValidationError(
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
             else:
83
             else:
64
                 try:
84
                 try:
65
                     attribute.validate_value(value)
85
                     attribute.validate_value(value)
66
                 except ValidationError as e:
86
                 except ValidationError as e:
67
                     raise ValidationError(
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
     def get_values(self):
92
     def get_values(self):
72
         return self.product.get_attribute_values()
93
         return self.product.get_attribute_values()
87
         for attribute in self.get_all_attributes():
108
         for attribute in self.get_all_attributes():
88
             if hasattr(self, attribute.code):
109
             if hasattr(self, attribute.code):
89
                 value = getattr(self, attribute.code)
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
                 attribute.save_value(self.product, value)
125
                 attribute.save_value(self.product, value)

+ 24
- 0
tests/unit/catalogue/test_product_attributes.py Переглянути файл

143
             "The child now has 1 attribute",
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
 class ProductAttributeQuerysetTest(TestCase):
171
 class ProductAttributeQuerysetTest(TestCase):
148
     fixtures = ["productattributes"]
172
     fixtures = ["productattributes"]

Завантаження…
Відмінити
Зберегти