Procházet zdrojové kódy

Make attribute codes unique per product class (#3823)

master
Joey před 3 roky
rodič
revize
f1d3afb7d5
Žádný účet není propojen s e-mailovou adresou tvůrce revize

+ 1
- 0
src/oscar/apps/catalogue/abstract_models.py Zobrazit soubor

870
         ordering = ['code']
870
         ordering = ['code']
871
         verbose_name = _('Product attribute')
871
         verbose_name = _('Product attribute')
872
         verbose_name_plural = _('Product attributes')
872
         verbose_name_plural = _('Product attributes')
873
+        unique_together = ('code', 'product_class')
873
 
874
 
874
     @property
875
     @property
875
     def is_option(self):
876
     def is_option(self):

+ 69
- 0
src/oscar/apps/catalogue/migrations/0024_remove_duplicate_attributes.py Zobrazit soubor

1
+# Generated by Django 3.2.9 on 2022-01-25 19:01
2
+
3
+from django.db import migrations
4
+from django.db.models import CharField, Count, Value
5
+from django.db.models.functions import Concat
6
+
7
+def remove_duplicate_attributes(apps, schema_editor):
8
+    """
9
+    Removes duplicate attributes that have the same code and product class.
10
+    """
11
+    ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
12
+    ProductClass = apps.get_model("catalogue", "ProductClass")
13
+
14
+    # 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.
16
+    duplicate_attributes = ProductAttribute.objects.annotate(
17
+        code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
18
+    ).values('code_and_product_class').annotate(
19
+        same_code_count=Count('code_and_product_class')
20
+    ).filter(same_code_count__gt=1)
21
+
22
+    for attribute in duplicate_attributes:
23
+        attribute_code, product_class_pk = attribute["code_and_product_class"].split("|")
24
+        product_class = ProductClass.objects.get(pk=product_class_pk)
25
+        attributes = ProductAttribute.objects.filter(
26
+            code=attribute_code,
27
+            product_class=product_class
28
+        )
29
+        used_attributes = attributes.filter(productattributevalue__isnull=False)
30
+        used_attribute_count = used_attributes.distinct().count()
31
+
32
+        # In most cases, the used attributes count will be one or zero as
33
+        # the dashboard will always show one attribute. If the used attribute
34
+        # count is one, we exclude this from attributes and remove the others.
35
+        # If it's zero, we pick the last created and delete others.
36
+        if used_attribute_count == 1:
37
+            attributes.exclude(pk=used_attributes.first().pk).delete()
38
+            continue
39
+        elif used_attribute_count == 0:
40
+            attributes.exclude(pk=attributes.last().pk).delete()
41
+            continue
42
+
43
+        # If we found multiple attributes that have values linked to them,
44
+        # we must move them to one attribute and then delete the others.
45
+        # We can only do this if the value_types are all the same!
46
+        ASSERTION_MESSAGE = """Duplicate attribute found with code: %s but different types!
47
+        You could fix this by renaming the duplicate codes or by matching all types to one
48
+        type and update the attribute values accordingly for their new type. After that you can
49
+        re-run the migration.""" % attribute_code
50
+        assert used_attributes.values("type").distinct().count() == 1, ASSERTION_MESSAGE
51
+
52
+        # Choose one attribute that will be used to move to and others to be deleted.
53
+        to_be_used_attribute = used_attributes.first()
54
+        to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
55
+        for attribute in to_be_deleted_attributes:
56
+            attribute.productattributevalue_set.all().update(attribute=to_be_used_attribute)
57
+            attribute.delete()
58
+
59
+
60
+
61
+class Migration(migrations.Migration):
62
+
63
+    dependencies = [
64
+        ('catalogue', '0023_auto_20210824_1414'),
65
+    ]
66
+
67
+    operations = [
68
+        migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
69
+    ]

+ 17
- 0
src/oscar/apps/catalogue/migrations/0025_attribute_code_uniquetogether_constraint.py Zobrazit soubor

1
+# Generated by Django 3.2.9 on 2022-01-25 20:17
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('catalogue', '0024_remove_duplicate_attributes'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AlterUniqueTogether(
14
+            name='productattribute',
15
+            unique_together={('code', 'product_class')},
16
+        ),
17
+    ]

+ 12
- 1
tests/integration/catalogue/test_attributes.py Zobrazit soubor

4
 from django.core.files.uploadedfile import SimpleUploadedFile
4
 from django.core.files.uploadedfile import SimpleUploadedFile
5
 from django.test import TestCase
5
 from django.test import TestCase
6
 
6
 
7
-from oscar.apps.catalogue.models import Product, ProductAttribute
7
+from oscar.apps.catalogue.models import Product, ProductAttribute, ProductClass
8
 from oscar.test import factories
8
 from oscar.test import factories
9
 
9
 
10
 
10
 
44
         product.attr.refresh()
44
         product.attr.refresh()
45
         assert product.attr.a1 == "v2"
45
         assert product.attr.a1 == "v2"
46
 
46
 
47
+    def test_attribute_code_uniquenesss(self):
48
+        product_class = factories.ProductClassFactory()
49
+        attribute1 = ProductAttribute.objects.create(name='a1', code='a1', product_class=product_class)
50
+        attribute1.full_clean()
51
+        attribute2 = ProductAttribute.objects.create(name='a1', code='a1', product_class=product_class)
52
+        with self.assertRaises(ValidationError):
53
+            attribute2.full_clean()
54
+        another_product_class = ProductClass.objects.create(name="another product class")
55
+        attribute3 = ProductAttribute.objects.create(name='a1', code='a1', product_class=another_product_class)
56
+        attribute3.full_clean()
57
+
47
 
58
 
48
 class TestBooleanAttributes(TestCase):
59
 class TestBooleanAttributes(TestCase):
49
 
60
 

Načítá se…
Zrušit
Uložit