Bladeren bron

Make ProductAttributesContainer a lazy object so that initialisation is simpler.

Fixes #3258.
master
Samir Shah 5 jaren geleden
bovenliggende
commit
f6157f2268

+ 4
- 0
docs/source/releases/v3.0.rst Bestand weergeven

@@ -47,6 +47,10 @@ Backwards incompatible changes
47 47
 Bug fixes
48 48
 ~~~~~~~~~
49 49
 
50
+- ``catalogue.product_attributes.ProductAttributesContainer`` was refactored to ensure that attributes
51
+  inside the container are always properly loaded at initialisation. The container is now wrapped in a
52
+  ``SimpleLazyObject`` when assigned to ``Product.attr``. ``ProductAttributesContainer.initiate_attributes()``
53
+  was removed as the database query now happens on instantiation.
50 54
 
51 55
 Removal of deprecated features
52 56
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 2
- 2
src/oscar/apps/catalogue/abstract_models.py Bestand weergeven

@@ -15,7 +15,7 @@ from django.db.models import Count, Exists, OuterRef, Sum
15 15
 from django.db.models.fields import Field
16 16
 from django.db.models.lookups import StartsWith
17 17
 from django.urls import reverse
18
-from django.utils.functional import cached_property
18
+from django.utils.functional import SimpleLazyObject, cached_property
19 19
 from django.utils.html import strip_tags
20 20
 from django.utils.safestring import mark_safe
21 21
 from django.utils.translation import get_language
@@ -435,7 +435,7 @@ class AbstractProduct(models.Model):
435 435
 
436 436
     def __init__(self, *args, **kwargs):
437 437
         super().__init__(*args, **kwargs)
438
-        self.attr = ProductAttributesContainer(product=self)
438
+        self.attr = SimpleLazyObject(lambda: ProductAttributesContainer(product=self))
439 439
 
440 440
     def __str__(self):
441 441
         if self.title:

+ 2
- 11
src/oscar/apps/catalogue/product_attributes.py Bestand weergeven

@@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
2 2
 from django.utils.translation import gettext_lazy as _
3 3
 
4 4
 
5
-class ProductAttributesContainer(object):
5
+class ProductAttributesContainer:
6 6
     """
7 7
     Stolen liberally from django-eav, but simplified to be product-specific
8 8
 
@@ -13,25 +13,16 @@ class ProductAttributesContainer(object):
13 13
 
14 14
     def __setstate__(self, state):
15 15
         self.__dict__ = state
16
-        self.initialised = False
17 16
 
18 17
     def __init__(self, product):
19 18
         self.product = product
20
-        self.initialised = False
21
-
22
-    def initiate_attributes(self):
23 19
         values = self.get_values().select_related('attribute')
24 20
         for v in values:
25 21
             setattr(self, v.attribute.code, v.value)
26
-        self.initialised = True
27 22
 
28 23
     def __getattr__(self, name):
29
-        if not name.startswith('_') and not self.initialised:
30
-            self.initiate_attributes()
31
-            return getattr(self, name)
32 24
         raise AttributeError(
33
-            _("%(obj)s has no attribute named '%(attr)s'") % {
34
-                'obj': self.product.get_product_class(), 'attr': name})
25
+            _("%(obj)s has no attribute named '%(attr)s'") % {'obj': self.product.get_product_class(), 'attr': name})
35 26
 
36 27
     def validate_attributes(self):
37 28
         for attribute in self.get_all_attributes():

+ 0
- 1
src/oscar/apps/dashboard/catalogue/forms.py Bestand weergeven

@@ -286,7 +286,6 @@ class ProductForm(forms.ModelForm):
286 286
         Set attributes before ModelForm calls the product's clean method
287 287
         (which it does in _post_clean), which in turn validates attributes.
288 288
         """
289
-        self.instance.attr.initiate_attributes()
290 289
         for attribute in self.instance.attr.get_all_attributes():
291 290
             field_name = 'attr_%s' % attribute.code
292 291
             # An empty text field won't show up in cleaned_data.

+ 26
- 2
tests/integration/catalogue/test_attributes.py Bestand weergeven

@@ -4,9 +4,33 @@ from django.core.exceptions import ValidationError
4 4
 from django.core.files.uploadedfile import SimpleUploadedFile
5 5
 from django.test import TestCase
6 6
 
7
+from oscar.apps.catalogue.models import Product
7 8
 from oscar.test import factories
8 9
 
9 10
 
11
+class TestContainer(TestCase):
12
+
13
+    def test_attributes_initialised_before_write(self):
14
+        # Regression test for https://github.com/django-oscar/django-oscar/issues/3258
15
+        product_class = factories.ProductClassFactory()
16
+        product_class.attributes.create(name='a1', code='a1', required=True)
17
+        product_class.attributes.create(name='a2', code='a2', required=False)
18
+        product_class.attributes.create(name='a3', code='a3', required=True)
19
+        product = factories.ProductFactory(product_class=product_class)
20
+        product.attr.a1 = "v1"
21
+        product.attr.a3 = "v3"
22
+        product.attr.save()
23
+
24
+        product = Product.objects.get(pk=product.pk)
25
+        product.attr.a1 = "v2"
26
+        product.attr.a3 = "v6"
27
+        product.attr.save()
28
+
29
+        product = Product.objects.get(pk=product.pk)
30
+        assert product.attr.a1 == "v2"
31
+        assert product.attr.a3 == "v6"
32
+
33
+
10 34
 class TestBooleanAttributes(TestCase):
11 35
 
12 36
     def setUp(self):
@@ -52,7 +76,7 @@ class TestMultiOptionAttributes(TestCase):
52 76
         product = factories.ProductFactory()
53 77
         # We'll save two out of the three available options
54 78
         self.attr.save_value(product, [self.options[0], self.options[2]])
55
-        product.refresh_from_db()
79
+        product = Product.objects.get(pk=product.pk)
56 80
         self.assertEqual(list(product.attr.sizes), [self.options[0], self.options[2]])
57 81
 
58 82
     def test_delete_multi_option_value(self):
@@ -60,7 +84,7 @@ class TestMultiOptionAttributes(TestCase):
60 84
         self.attr.save_value(product, [self.options[0], self.options[1]])
61 85
         # Now delete these values
62 86
         self.attr.save_value(product, None)
63
-        product.refresh_from_db()
87
+        product = Product.objects.get(pk=product.pk)
64 88
         self.assertFalse(hasattr(product.attr, 'sizes'))
65 89
 
66 90
     def test_multi_option_value_as_text(self):

Laden…
Annuleren
Opslaan