Bladeren bron

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 jaren geleden
bovenliggende
commit
a07cca3b00
No account linked to committer's email address

+ 38
- 3
src/oscar/apps/catalogue/migrations/0024_remove_duplicate_attributes.py Bestand weergeven

1
 # Generated by Django 3.2.9 on 2022-01-25 19:01
1
 # Generated by Django 3.2.9 on 2022-01-25 19:01
2
+import logging
2
 
3
 
3
 from django.db import migrations
4
 from django.db import migrations
4
 from django.db.models import CharField, Count, Value
5
 from django.db.models import CharField, Count, Value
5
 from django.db.models.functions import Concat
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
 def remove_duplicate_attributes(apps, schema_editor):
17
 def remove_duplicate_attributes(apps, schema_editor):
8
     """
18
     """
9
     Removes duplicate attributes that have the same code and product class.
19
     Removes duplicate attributes that have the same code and product class.
10
     """
20
     """
11
     ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
21
     ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
22
+    ProductAttributeValue = apps.get_model('catalogue', 'ProductAttributeValue')
12
     ProductClass = apps.get_model("catalogue", "ProductClass")
23
     ProductClass = apps.get_model("catalogue", "ProductClass")
13
 
24
 
14
     # Instead of iterating over all attributes, we concat the code and product class pk
25
     # Instead of iterating over all attributes, we concat the code and product class pk
15
     # with a "|" so we can find duplicate attributes in one query.
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
         code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
28
         code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
18
     ).values('code_and_product_class').annotate(
29
     ).values('code_and_product_class').annotate(
19
         same_code_count=Count('code_and_product_class')
30
         same_code_count=Count('code_and_product_class')
52
         # Choose one attribute that will be used to move to and others to be deleted.
63
         # Choose one attribute that will be used to move to and others to be deleted.
53
         to_be_used_attribute = used_attributes.first()
64
         to_be_used_attribute = used_attributes.first()
54
         to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
65
         to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
66
+
55
         for attribute in to_be_deleted_attributes:
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
             attribute.delete()
92
             attribute.delete()
58
 
93
 
59
 
94
 
66
 
101
 
67
     operations = [
102
     operations = [
68
         migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
103
         migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
69
-    ]
104
+    ]

+ 38
- 3
tests/_site/apps/catalogue/migrations/0024_remove_duplicate_attributes.py Bestand weergeven

1
 # Generated by Django 3.2.9 on 2022-01-25 19:01
1
 # Generated by Django 3.2.9 on 2022-01-25 19:01
2
+import logging
2
 
3
 
3
 from django.db import migrations
4
 from django.db import migrations
4
 from django.db.models import CharField, Count, Value
5
 from django.db.models import CharField, Count, Value
5
 from django.db.models.functions import Concat
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
 def remove_duplicate_attributes(apps, schema_editor):
17
 def remove_duplicate_attributes(apps, schema_editor):
8
     """
18
     """
9
     Removes duplicate attributes that have the same code and product class.
19
     Removes duplicate attributes that have the same code and product class.
10
     """
20
     """
11
     ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
21
     ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
22
+    ProductAttributeValue = apps.get_model('catalogue', 'ProductAttributeValue')
12
     ProductClass = apps.get_model("catalogue", "ProductClass")
23
     ProductClass = apps.get_model("catalogue", "ProductClass")
13
 
24
 
14
     # Instead of iterating over all attributes, we concat the code and product class pk
25
     # Instead of iterating over all attributes, we concat the code and product class pk
15
     # with a "|" so we can find duplicate attributes in one query.
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
         code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
28
         code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
18
     ).values('code_and_product_class').annotate(
29
     ).values('code_and_product_class').annotate(
19
         same_code_count=Count('code_and_product_class')
30
         same_code_count=Count('code_and_product_class')
52
         # Choose one attribute that will be used to move to and others to be deleted.
63
         # Choose one attribute that will be used to move to and others to be deleted.
53
         to_be_used_attribute = used_attributes.first()
64
         to_be_used_attribute = used_attributes.first()
54
         to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
65
         to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
66
+
55
         for attribute in to_be_deleted_attributes:
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
             attribute.delete()
92
             attribute.delete()
58
 
93
 
59
 
94
 
66
 
101
 
67
     operations = [
102
     operations = [
68
         migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
103
         migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
69
-    ]
104
+    ]

Laden…
Annuleren
Opslaan