瀏覽代碼

fix 0024_remove_duplicate_attributes.py (#3989)

* 0024_remove_duplicate_attributes.py handle multiple attribute values with same attribute code

* Use _get_value instead of copying the method
master
Joey 3 年之前
父節點
當前提交
a07cca3b00
No account linked to committer's email address

+ 38
- 3
src/oscar/apps/catalogue/migrations/0024_remove_duplicate_attributes.py 查看文件

@@ -1,19 +1,30 @@
1 1
 # Generated by Django 3.2.9 on 2022-01-25 19:01
2
+import logging
2 3
 
3 4
 from django.db import migrations
4 5
 from django.db.models import CharField, Count, Value
5 6
 from django.db.models.functions import Concat
6 7
 
8
+from oscar.core.loading import get_model
9
+
10
+# Needed for calling _get_value, the historical model can't be used for that.
11
+NonHistoricalProductAttributeValue = get_model('catalogue', 'ProductAttributeValue')
12
+
13
+
14
+logger = logging.getLogger(__name__)
15
+
16
+
7 17
 def remove_duplicate_attributes(apps, schema_editor):
8 18
     """
9 19
     Removes duplicate attributes that have the same code and product class.
10 20
     """
11 21
     ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
22
+    ProductAttributeValue = apps.get_model('catalogue', 'ProductAttributeValue')
12 23
     ProductClass = apps.get_model("catalogue", "ProductClass")
13 24
 
14 25
     # Instead of iterating over all attributes, we concat the code and product class pk
15 26
     # with a "|" so we can find duplicate attributes in one query.
16
-    duplicate_attributes = ProductAttribute.objects.annotate(
27
+    duplicate_attributes = ProductAttribute.objects.filter(product_class__isnull=False).annotate(
17 28
         code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
18 29
     ).values('code_and_product_class').annotate(
19 30
         same_code_count=Count('code_and_product_class')
@@ -52,8 +63,32 @@ def remove_duplicate_attributes(apps, schema_editor):
52 63
         # Choose one attribute that will be used to move to and others to be deleted.
53 64
         to_be_used_attribute = used_attributes.first()
54 65
         to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
66
+
55 67
         for attribute in to_be_deleted_attributes:
56
-            attribute.productattributevalue_set.all().update(attribute=to_be_used_attribute)
68
+            for attribute_value in attribute.productattributevalue_set.all():
69
+                product = attribute_value.product
70
+
71
+                # ProductAttributeValue has a unique together constraint on 'product' and 'attribute'.
72
+                # This means, if the product of the current 'attribute_value' already has a ProductAttributeValue
73
+                # linked to the 'to_be_used_attribute' attribute, we can't update the attribute on the
74
+                # 'attribute_value' as this would raise an IntegrityError.
75
+                to_be_used_attribute_value = to_be_used_attribute.productattributevalue_set.filter(product=product).first()
76
+                if not to_be_used_attribute_value:
77
+                    attribute_value.attribute = to_be_used_attribute
78
+                    attribute_value.save()
79
+                else:
80
+                    msg = """Product with ID '%s' had more than one attribute value linked to an attribute
81
+                    with code '%s'. We've kept the value '%s' and removed the value '%s' as this is the one you
82
+                    would see in the dashboard when editing the product.
83
+                    """ % (
84
+                        product.id,
85
+                        attribute.code,
86
+                        NonHistoricalProductAttributeValue._get_value(to_be_used_attribute_value),
87
+                        NonHistoricalProductAttributeValue._get_value(attribute_value)
88
+                    )
89
+                    logger.warning(msg)
90
+
91
+            # Once the attribute values have been updated, we can safely remove the attribute instance.
57 92
             attribute.delete()
58 93
 
59 94
 
@@ -66,4 +101,4 @@ class Migration(migrations.Migration):
66 101
 
67 102
     operations = [
68 103
         migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
69
-    ]
104
+    ]

+ 38
- 3
tests/_site/apps/catalogue/migrations/0024_remove_duplicate_attributes.py 查看文件

@@ -1,19 +1,30 @@
1 1
 # Generated by Django 3.2.9 on 2022-01-25 19:01
2
+import logging
2 3
 
3 4
 from django.db import migrations
4 5
 from django.db.models import CharField, Count, Value
5 6
 from django.db.models.functions import Concat
6 7
 
8
+from oscar.core.loading import get_model
9
+
10
+# Needed for calling _get_value, the historical model can't be used for that.
11
+NonHistoricalProductAttributeValue = get_model('catalogue', 'ProductAttributeValue')
12
+
13
+
14
+logger = logging.getLogger(__name__)
15
+
16
+
7 17
 def remove_duplicate_attributes(apps, schema_editor):
8 18
     """
9 19
     Removes duplicate attributes that have the same code and product class.
10 20
     """
11 21
     ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
22
+    ProductAttributeValue = apps.get_model('catalogue', 'ProductAttributeValue')
12 23
     ProductClass = apps.get_model("catalogue", "ProductClass")
13 24
 
14 25
     # Instead of iterating over all attributes, we concat the code and product class pk
15 26
     # with a "|" so we can find duplicate attributes in one query.
16
-    duplicate_attributes = ProductAttribute.objects.annotate(
27
+    duplicate_attributes = ProductAttribute.objects.filter(product_class__isnull=False).annotate(
17 28
         code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
18 29
     ).values('code_and_product_class').annotate(
19 30
         same_code_count=Count('code_and_product_class')
@@ -52,8 +63,32 @@ def remove_duplicate_attributes(apps, schema_editor):
52 63
         # Choose one attribute that will be used to move to and others to be deleted.
53 64
         to_be_used_attribute = used_attributes.first()
54 65
         to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
66
+
55 67
         for attribute in to_be_deleted_attributes:
56
-            attribute.productattributevalue_set.all().update(attribute=to_be_used_attribute)
68
+            for attribute_value in attribute.productattributevalue_set.all():
69
+                product = attribute_value.product
70
+
71
+                # ProductAttributeValue has a unique together constraint on 'product' and 'attribute'.
72
+                # This means, if the product of the current 'attribute_value' already has a ProductAttributeValue
73
+                # linked to the 'to_be_used_attribute' attribute, we can't update the attribute on the
74
+                # 'attribute_value' as this would raise an IntegrityError.
75
+                to_be_used_attribute_value = to_be_used_attribute.productattributevalue_set.filter(product=product).first()
76
+                if not to_be_used_attribute_value:
77
+                    attribute_value.attribute = to_be_used_attribute
78
+                    attribute_value.save()
79
+                else:
80
+                    msg = """Product with ID '%s' had more than one attribute value linked to an attribute
81
+                    with code '%s'. We've kept the value '%s' and removed the value '%s' as this is the one you
82
+                    would see in the dashboard when editing the product.
83
+                    """ % (
84
+                        product.id,
85
+                        attribute.code,
86
+                        NonHistoricalProductAttributeValue._get_value(to_be_used_attribute_value),
87
+                        NonHistoricalProductAttributeValue._get_value(attribute_value)
88
+                    )
89
+                    logger.warning(msg)
90
+
91
+            # Once the attribute values have been updated, we can safely remove the attribute instance.
57 92
             attribute.delete()
58 93
 
59 94
 
@@ -66,4 +101,4 @@ class Migration(migrations.Migration):
66 101
 
67 102
     operations = [
68 103
         migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
69
-    ]
104
+    ]

Loading…
取消
儲存