Преглед на файлове

Make sure to only save attributes when they have been changed. (#3995)

* Make sure to only save attributes when they have been saved.

This makes sure attributes on the parent are not needlessly saved to the child.

* Renamed testcase

* Added tests for child product attributes not copying parent attributes

* Make ProductAttributesContainer even more lazy and initialize only when an attribute is being read.

* Changed docstring
master
Voxin Muyli преди 3 години
родител
ревизия
6f333f6564
No account linked to committer's email address

+ 2
- 2
src/oscar/apps/catalogue/abstract_models.py Целия файл

@@ -16,7 +16,7 @@ from django.db.models.fields import Field
16 16
 from django.db.models.lookups import StartsWith
17 17
 from django.template.defaultfilters import striptags
18 18
 from django.urls import reverse
19
-from django.utils.functional import SimpleLazyObject, cached_property
19
+from django.utils.functional import cached_property
20 20
 from django.utils.html import strip_tags
21 21
 from django.utils.safestring import mark_safe
22 22
 from django.utils.translation import get_language
@@ -445,7 +445,7 @@ class AbstractProduct(models.Model):
445 445
 
446 446
     def __init__(self, *args, **kwargs):
447 447
         super().__init__(*args, **kwargs)
448
-        self.attr = SimpleLazyObject(lambda: ProductAttributesContainer(product=self))
448
+        self.attr = ProductAttributesContainer(product=self)
449 449
 
450 450
     def __str__(self):
451 451
         if self.title:

+ 37
- 5
src/oscar/apps/catalogue/product_attributes.py Целия файл

@@ -1,4 +1,4 @@
1
-from django.core.exceptions import ValidationError
1
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
2 2
 from django.utils.translation import gettext_lazy as _
3 3
 
4 4
 
@@ -19,14 +19,35 @@ class ProductAttributesContainer:
19 19
         self.__dict__ = state
20 20
 
21 21
     def __init__(self, product):
22
-        self.product = product
23
-        self.refresh()
22
+        self._product = product
23
+        self._initialized = False
24
+
25
+    @property
26
+    def product(self):
27
+        return self._product
28
+
29
+    def initialize(self):
30
+        self._initialized = True
31
+        # initialize should not overwrite any values that have allready been set
32
+        attrs = self.__dict__
33
+        for v in self.get_values().select_related('attribute'):
34
+            attrs.setdefault(v.attribute.code, v.value)
24 35
 
25 36
     def refresh(self):
26
-        values = self.get_values().select_related('attribute')
27
-        for v in values:
37
+        for v in self.get_values().select_related('attribute'):
28 38
             setattr(self, v.attribute.code, v.value)
29 39
 
40
+    def __getattribute__(self, name):
41
+        try:
42
+            return super().__getattribute__(name)
43
+        except AttributeError:
44
+            if self._initialized:
45
+                raise
46
+            else:
47
+                self.initialize()
48
+                # try again, whever happens then will just be the result.
49
+                return super().__getattribute__(name)
50
+
30 51
     def __getattr__(self, name):
31 52
         raise AttributeError(
32 53
             _("%(obj)s has no attribute named '%(attr)s'") % {'obj': self.product.get_product_class(), 'attr': name})
@@ -66,4 +87,15 @@ class ProductAttributesContainer:
66 87
         for attribute in self.get_all_attributes():
67 88
             if hasattr(self, attribute.code):
68 89
                 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.
100
+
69 101
                 attribute.save_value(self.product, value)

+ 258
- 0
tests/unit/catalogue/test_product_attributes.py Целия файл

@@ -0,0 +1,258 @@
1
+from django.core.exceptions import ValidationError
2
+from django.test import TestCase
3
+
4
+from oscar.core.loading import get_model
5
+from oscar.test.factories import (
6
+    ProductAttributeFactory, ProductClassFactory, ProductFactory)
7
+
8
+Product = get_model("catalogue", "Product")
9
+ProductAttribute = get_model("catalogue", "ProductAttribute")
10
+ProductAttributeValue = get_model("catalogue", "ProductAttributeValue")
11
+
12
+
13
+class ProductAttributeTest(TestCase):
14
+
15
+    def setUp(self):
16
+        super().setUp()
17
+
18
+        # setup the productclass
19
+        self.product_class = product_class = ProductClassFactory(name='Cows', slug='cows')
20
+        self.name_attr = ProductAttributeFactory(
21
+            type=ProductAttribute.TEXT, product_class=product_class, name="name", code="name")
22
+        self.weight_attrs = ProductAttributeFactory(
23
+            type=ProductAttribute.INTEGER, name="weight", code="weight", product_class=product_class)
24
+
25
+        # create the parent product
26
+        self.product = product = ProductFactory(
27
+            title="I am your father", stockrecords=None, product_class=product_class, structure="parent", upc="1234")
28
+        product.attr.weight = 3
29
+        product.full_clean()
30
+        product.save()
31
+
32
+        # create the child product
33
+        self.child_product = ProductFactory(
34
+            parent=product, structure="child", categories=None, product_class=None,
35
+            title="You are my father", upc="child-1234")
36
+        self.child_product.full_clean()
37
+
38
+    def test_update_child_with_attributes(self):
39
+        "Attributes preseent on the parent should not be copied to the child "
40
+        "when title of the child is modified"
41
+        self.assertEqual(
42
+            ProductAttributeValue.objects.filter(product_id=self.product.pk).count(),
43
+            1,
44
+            "The parent has 1 attributes",
45
+        )
46
+
47
+        # establish baseline
48
+        self.assertEqual(
49
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
50
+            0,
51
+            "The child has no attributes",
52
+        )
53
+        self.assertEqual(self.child_product.parent_id, self.product.pk)
54
+        self.assertIsNone(self.child_product.product_class)
55
+        self.assertEqual(self.child_product.upc, "child-1234")
56
+        self.assertEqual(self.child_product.slug, "you-are-my-father")
57
+        self.assertNotEqual(self.child_product.title, "Klaas is my real father")
58
+
59
+        self.child_product.title = "Klaas is my real father"
60
+        self.child_product.save()
61
+
62
+        self.child_product.refresh_from_db()
63
+        self.assertEqual(self.child_product.title, "Klaas is my real father")
64
+        self.assertEqual(
65
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
66
+            0,
67
+            "The child has no attributes",
68
+        )
69
+
70
+    def test_update_child_attributes(self):
71
+        "Attributes preseent on the parent should not be copied to the child "
72
+        "when the child attributes are modified"
73
+        self.assertEqual(
74
+            ProductAttributeValue.objects.filter(product_id=self.product.pk).count(),
75
+            1,
76
+            "The parent has 1 attributes",
77
+        )
78
+
79
+        # establish baseline
80
+        self.assertEqual(
81
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
82
+            0,
83
+            "The child has no attributes",
84
+        )
85
+        self.assertEqual(self.child_product.parent_id, self.product.pk)
86
+        self.assertIsNone(self.child_product.product_class)
87
+        self.assertEqual(self.child_product.upc, "child-1234")
88
+        self.assertEqual(self.child_product.slug, "you-are-my-father")
89
+        self.assertNotEqual(self.child_product.title, "Klaas is my real father")
90
+
91
+        self.child_product.title = "Klaas is my real father"
92
+        self.child_product.attr.name = "Berta"
93
+        self.child_product.save()
94
+
95
+        self.child_product.refresh_from_db()
96
+        self.assertEqual(self.child_product.title, "Klaas is my real father")
97
+        self.assertEqual(
98
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
99
+            1,
100
+            "The child now has 1 attribute",
101
+        )
102
+
103
+    def test_update_attributes_to_parent_and_child(self):
104
+        "Attributes present on the parent should not be copied to the child "
105
+        "ever, not even newly added attributes"
106
+        self.assertEqual(
107
+            ProductAttributeValue.objects.filter(product_id=self.product.pk).count(),
108
+            1,
109
+            "The parent has 1 attributes",
110
+        )
111
+        self.product.attr.name = "Greta"
112
+        self.product.save()
113
+        self.product.refresh_from_db()
114
+        self.product.attr.refresh()
115
+
116
+        self.assertEqual(
117
+            ProductAttributeValue.objects.filter(product_id=self.product.pk).count(),
118
+            2,
119
+            "The parent now has 2 attributes",
120
+        )
121
+
122
+        # establish baseline
123
+        self.assertEqual(
124
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
125
+            0,
126
+            "The child has no attributes",
127
+        )
128
+        self.assertEqual(self.child_product.parent_id, self.product.pk)
129
+        self.assertIsNone(self.child_product.product_class)
130
+        self.assertEqual(self.child_product.upc, "child-1234")
131
+        self.assertEqual(self.child_product.slug, "you-are-my-father")
132
+        self.assertNotEqual(self.child_product.title, "Klaas is my real father")
133
+
134
+        self.child_product.title = "Klaas is my real father"
135
+        self.child_product.attr.name = "Berta"
136
+        self.child_product.save()
137
+
138
+        self.child_product.refresh_from_db()
139
+        self.assertEqual(self.child_product.title, "Klaas is my real father")
140
+        self.assertEqual(
141
+            ProductAttributeValue.objects.filter(product=self.child_product).count(),
142
+            1,
143
+            "The child now has 1 attribute",
144
+        )
145
+
146
+
147
+class ProductAttributeQuerysetTest(TestCase):
148
+    fixtures = ["productattributes"]
149
+
150
+    def test_query_multiple_producttypes(self):
151
+        "We should be able to query over multiple product classes"
152
+        result = Product.objects.filter_by_attributes(henkie="bah bah")
153
+        self.assertEqual(result.count(), 2)
154
+        result1, result2 = list(result)
155
+
156
+        self.assertNotEqual(result1.product_class, result2.product_class)
157
+        self.assertEqual(result1.attr.henkie, result2.attr.henkie)
158
+
159
+    def test_further_filtering(self):
160
+        "The returned queryset should be ready for further filtering"
161
+        result = Product.objects.filter_by_attributes(henkie="bah bah")
162
+        photo = result.filter(title__contains="Photo")
163
+        self.assertEqual(photo.count(), 1)
164
+
165
+    def test_empty_results(self):
166
+        "Empty results are possible without errors"
167
+        result = Product.objects.filter_by_attributes(doesnotexist=True)
168
+        self.assertFalse(result.exists(), "querying with bulshit attributes should give no results")
169
+        result = Product.objects.filter_by_attributes(henkie="zulthoofd")
170
+        self.assertFalse(result.exists(), "querying with non existing values should give no results")
171
+        result = Product.objects.filter_by_attributes(henkie=True)
172
+        self.assertFalse(result.exists(), "querying with wring value type should give no results")
173
+
174
+    def test_text_value(self):
175
+        result = Product.objects.filter_by_attributes(subtitle="superhenk")
176
+        self.assertTrue(result.exists())
177
+        result = Product.objects.filter_by_attributes(subtitle="kekjo")
178
+        self.assertTrue(result.exists())
179
+        result = Product.objects.filter_by_attributes(subtitle=True)
180
+        self.assertFalse(result.exists())
181
+
182
+    def test_formatted_text(self):
183
+        html = "<p style=\"margin: 0px; font-stretch: normal; font-size: 12px; line-height: normal; font-family: Helvetica;\">Vivamus auctor leo vel dui. Aliquam erat volutpat. Phasellus nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Cras tempor. Morbi egestas, <em>urna</em> non consequat tempus, <strong>nunc</strong> arcu mollis enim, eu aliquam erat nulla non nibh. Duis consectetuer malesuada velit. Nam ante nulla, interdum vel, tristique ac, condimentum non, tellus. Proin ornare feugiat nisl. Suspendisse dolor nisl, ultrices at, eleifend vel, consequat at, dolor.</p>"  # noqa
184
+        result = Product.objects.filter_by_attributes(additional_info=html)
185
+        self.assertTrue(result.exists())
186
+
187
+    def test_boolean(self):
188
+        result = Product.objects.filter_by_attributes(available=True)
189
+        self.assertTrue(result.exists())
190
+        result = Product.objects.filter_by_attributes(available=0)
191
+        self.assertTrue(result.exists())
192
+        with self.assertRaises(ValidationError):
193
+            result = Product.objects.filter_by_attributes(available="henk")
194
+
195
+    def test_number(self):
196
+        result = Product.objects.filter_by_attributes(facets=4)
197
+        self.assertTrue(result.exists())
198
+        with self.assertRaises(ValueError):
199
+            result = Product.objects.filter_by_attributes(facets="four")
200
+
201
+        result = Product.objects.filter_by_attributes(facets=1)
202
+        self.assertFalse(result.exists())
203
+
204
+    def test_float(self):
205
+        result = Product.objects.filter_by_attributes(hypothenusa=1.25)
206
+        self.assertTrue(result.exists())
207
+        with self.assertRaises(ValueError):
208
+            result = Product.objects.filter_by_attributes(facets="four")
209
+
210
+        result = Product.objects.filter_by_attributes(hypothenusa=1)
211
+        self.assertFalse(result.exists())
212
+
213
+    def test_option(self):
214
+        result = Product.objects.filter_by_attributes(kind="totalitarian")
215
+        self.assertTrue(result.exists())
216
+        result = Product.objects.filter_by_attributes(kind=True)
217
+        self.assertFalse(result.exists())
218
+
219
+        result = Product.objects.filter_by_attributes(kind="omnimous")
220
+        self.assertFalse(result.exists())
221
+
222
+    def test_multi_option(self):
223
+        result = Product.objects.filter_by_attributes(subkinds="megalomane")
224
+        self.assertTrue(result.exists())
225
+        self.assertEqual(result.count(), 2)
226
+        result = Product.objects.filter_by_attributes(subkinds=True)
227
+        self.assertFalse(result.exists())
228
+
229
+        result = Product.objects.filter_by_attributes(subkinds="omnimous")
230
+        self.assertFalse(result.exists())
231
+
232
+        result = Product.objects.filter_by_attributes(subkinds__contains="om")
233
+        self.assertTrue(result.exists(), "megalomane conains om")
234
+        self.assertEqual(result.count(), 2)
235
+
236
+    def test_multiple_attributes(self):
237
+        result = Product.objects.filter_by_attributes(subkinds="megalomane", available=True)
238
+        self.assertTrue(result.exists())
239
+
240
+        result = Product.objects.filter_by_attributes(
241
+            kind="totalitarian",
242
+            hypothenusa=1.25,
243
+            facets=8,
244
+            subtitle="superhenk",
245
+            subkinds="megalomane",
246
+            available=False
247
+        )
248
+        self.assertTrue(result.exists())
249
+
250
+    def test_lookups(self):
251
+        result = Product.objects.filter_by_attributes(facets__lte=4)
252
+        self.assertEqual(result.count(), 1)
253
+
254
+        result = Product.objects.filter_by_attributes(facets__lte=8)
255
+        self.assertEqual(result.count(), 2)
256
+
257
+        result = Product.objects.filter_by_attributes(facets__lt=8)
258
+        self.assertEqual(result.count(), 1)

+ 0
- 120
tests/unit/catalogue/test_product_attributes_query.py Целия файл

@@ -1,120 +0,0 @@
1
-from django.core.exceptions import ValidationError
2
-from django.test import TestCase
3
-
4
-from oscar.core.loading import get_model
5
-
6
-Product = get_model("catalogue", "Product")
7
-
8
-
9
-class ProductAttributeQuerysetTest(TestCase):
10
-    fixtures = ["productattributes"]
11
-
12
-    def test_query_multiple_producttypes(self):
13
-        "We should be able to query over multiple product classes"
14
-        result = Product.objects.filter_by_attributes(henkie="bah bah")
15
-        self.assertEqual(result.count(), 2)
16
-        result1, result2 = list(result)
17
-
18
-        self.assertNotEqual(result1.product_class, result2.product_class)
19
-        self.assertEqual(result1.attr.henkie, result2.attr.henkie)
20
-
21
-    def test_further_filtering(self):
22
-        "The returned queryset should be ready for further filtering"
23
-        result = Product.objects.filter_by_attributes(henkie="bah bah")
24
-        photo = result.filter(title__contains="Photo")
25
-        self.assertEqual(photo.count(), 1)
26
-
27
-    def test_empty_results(self):
28
-        "Empty results are possible without errors"
29
-        result = Product.objects.filter_by_attributes(doesnotexist=True)
30
-        self.assertFalse(result.exists(), "querying with bulshit attributes should give no results")
31
-        result = Product.objects.filter_by_attributes(henkie="zulthoofd")
32
-        self.assertFalse(result.exists(), "querying with non existing values should give no results")
33
-        result = Product.objects.filter_by_attributes(henkie=True)
34
-        self.assertFalse(result.exists(), "querying with wring value type should give no results")
35
-
36
-    def test_text_value(self):
37
-        result = Product.objects.filter_by_attributes(subtitle="superhenk")
38
-        self.assertTrue(result.exists())
39
-        result = Product.objects.filter_by_attributes(subtitle="kekjo")
40
-        self.assertTrue(result.exists())
41
-        result = Product.objects.filter_by_attributes(subtitle=True)
42
-        self.assertFalse(result.exists())
43
-
44
-    def test_formatted_text(self):
45
-        html = "<p style=\"margin: 0px; font-stretch: normal; font-size: 12px; line-height: normal; font-family: Helvetica;\">Vivamus auctor leo vel dui. Aliquam erat volutpat. Phasellus nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Cras tempor. Morbi egestas, <em>urna</em> non consequat tempus, <strong>nunc</strong> arcu mollis enim, eu aliquam erat nulla non nibh. Duis consectetuer malesuada velit. Nam ante nulla, interdum vel, tristique ac, condimentum non, tellus. Proin ornare feugiat nisl. Suspendisse dolor nisl, ultrices at, eleifend vel, consequat at, dolor.</p>"  # noqa
46
-        result = Product.objects.filter_by_attributes(additional_info=html)
47
-        self.assertTrue(result.exists())
48
-
49
-    def test_boolean(self):
50
-        result = Product.objects.filter_by_attributes(available=True)
51
-        self.assertTrue(result.exists())
52
-        result = Product.objects.filter_by_attributes(available=0)
53
-        self.assertTrue(result.exists())
54
-        with self.assertRaises(ValidationError):
55
-            result = Product.objects.filter_by_attributes(available="henk")
56
-
57
-    def test_number(self):
58
-        result = Product.objects.filter_by_attributes(facets=4)
59
-        self.assertTrue(result.exists())
60
-        with self.assertRaises(ValueError):
61
-            result = Product.objects.filter_by_attributes(facets="four")
62
-
63
-        result = Product.objects.filter_by_attributes(facets=1)
64
-        self.assertFalse(result.exists())
65
-
66
-    def test_float(self):
67
-        result = Product.objects.filter_by_attributes(hypothenusa=1.25)
68
-        self.assertTrue(result.exists())
69
-        with self.assertRaises(ValueError):
70
-            result = Product.objects.filter_by_attributes(facets="four")
71
-
72
-        result = Product.objects.filter_by_attributes(hypothenusa=1)
73
-        self.assertFalse(result.exists())
74
-
75
-    def test_option(self):
76
-        result = Product.objects.filter_by_attributes(kind="totalitarian")
77
-        self.assertTrue(result.exists())
78
-        result = Product.objects.filter_by_attributes(kind=True)
79
-        self.assertFalse(result.exists())
80
-
81
-        result = Product.objects.filter_by_attributes(kind="omnimous")
82
-        self.assertFalse(result.exists())
83
-
84
-    def test_multi_option(self):
85
-        result = Product.objects.filter_by_attributes(subkinds="megalomane")
86
-        self.assertTrue(result.exists())
87
-        self.assertEqual(result.count(), 2)
88
-        result = Product.objects.filter_by_attributes(subkinds=True)
89
-        self.assertFalse(result.exists())
90
-
91
-        result = Product.objects.filter_by_attributes(subkinds="omnimous")
92
-        self.assertFalse(result.exists())
93
-
94
-        result = Product.objects.filter_by_attributes(subkinds__contains="om")
95
-        self.assertTrue(result.exists(), "megalomane conains om")
96
-        self.assertEqual(result.count(), 2)
97
-
98
-    def test_multiple_attributes(self):
99
-        result = Product.objects.filter_by_attributes(subkinds="megalomane", available=True)
100
-        self.assertTrue(result.exists())
101
-
102
-        result = Product.objects.filter_by_attributes(
103
-            kind="totalitarian",
104
-            hypothenusa=1.25,
105
-            facets=8,
106
-            subtitle="superhenk",
107
-            subkinds="megalomane",
108
-            available=False
109
-        )
110
-        self.assertTrue(result.exists())
111
-
112
-    def test_lookups(self):
113
-        result = Product.objects.filter_by_attributes(facets__lte=4)
114
-        self.assertEqual(result.count(), 1)
115
-
116
-        result = Product.objects.filter_by_attributes(facets__lte=8)
117
-        self.assertEqual(result.count(), 2)
118
-
119
-        result = Product.objects.filter_by_attributes(facets__lt=8)
120
-        self.assertEqual(result.count(), 1)

Loading…
Отказ
Запис