Parcourir la source

Rebuild product attribute entities

Product attributes had a mechanism to point at related model instances
in a generic way, but nobody managed to understand or get it working.
This comment removes that logic, including the AttributeEntity and
AttributeEntityType models.

Instead, this commit models the relation via a generic foreign key on
ProductAttributeValue.

I don't think there's a good default behaviour for the entity selection
as presumably each deployment will want to limit the selection in some
way. Therefore, there's currently no frontend support for it.

variants.json has been rebuilt by executing the migration and dumping
the correct data afterwards. That explains the other changes in it, as
usually we'd just use vim line matching to shape it into a form that
lets us build the sandbox.
master
Maik Hoepfel il y a 11 ans
Parent
révision
1f0f2e35bb

+ 6
- 0
docs/source/releases/v0.8.rst Voir le fichier

@@ -319,6 +319,12 @@ Misc
319 319
   trivial ``ProductAttribute.get_validator`` and the unused
320 320
   ``ProductAttribute.is_value_valid`` methods have been removed.
321 321
 
322
+* It is now possible to use product attributes to add a relation to arbitrary
323
+  model instances. There was some (presumably broken) support for it before,
324
+  but you should now be able to use product attributes of type ``entity`` as
325
+  expected. There's currently no frontend or dashboard support for it, as there
326
+  is no good default behaviour.
327
+
322 328
 .. _rewritten: https://github.com/tangentlabs/django-oscar/commit/d8b4dbfed17be90846ea4bc47b5f7b39ad944c24
323 329
 
324 330
 Basket line stockrecords

+ 21
- 64
oscar/apps/catalogue/abstract_models.py Voir le fichier

@@ -16,6 +16,8 @@ from django.db import models
16 16
 from django.db.models import Sum, Count
17 17
 from django.utils.translation import ugettext_lazy as _, pgettext_lazy
18 18
 from django.utils.functional import cached_property
19
+from django.contrib.contenttypes.generic import GenericForeignKey
20
+from django.contrib.contenttypes.models import ContentType
19 21
 
20 22
 from treebeard.mp_tree import MP_Node
21 23
 
@@ -620,10 +622,6 @@ class AbstractProductAttribute(models.Model):
620 622
         'catalogue.AttributeOptionGroup', blank=True, null=True,
621 623
         verbose_name=_("Option Group"),
622 624
         help_text=_('Select an option group if using type "Option"'))
623
-    entity_type = models.ForeignKey(
624
-        'catalogue.AttributeEntityType', blank=True, null=True,
625
-        verbose_name=_("Entity Type"),
626
-        help_text=_('Select an entity type if using type "Entity"'))
627 625
     required = models.BooleanField(_('Required'), default=False)
628 626
 
629 627
     class Meta:
@@ -666,14 +664,9 @@ class AbstractProductAttribute(models.Model):
666 664
             raise ValidationError(_("Must be a boolean"))
667 665
 
668 666
     def _validate_entity(self, value):
669
-        if not isinstance(value, get_model('catalogue', 'AttributeEntity')):
670
-            raise ValidationError(
671
-                _("Must be an AttributeEntity model object instance"))
672
-        if not value.pk:
673
-            raise ValidationError(_("Model has not been saved yet"))
674
-        if value.type != self.entity_type:
675
-            raise ValidationError(
676
-                _("Entity must be of type %s" % self.entity_type.name))
667
+        # This feels rather naive
668
+        if not isinstance(value, models.Model):
669
+            raise ValidationError(_("Must be a model instance"))
677 670
 
678 671
     def _validate_option(self, value):
679 672
         if not isinstance(value, get_model('catalogue', 'AttributeOption')):
@@ -743,8 +736,8 @@ class AbstractProductAttributeValue(models.Model):
743 736
 
744 737
     For example: number_of_pages = 295
745 738
     """
746
-    attribute = models.ForeignKey('catalogue.ProductAttribute',
747
-                                  verbose_name=_("Attribute"))
739
+    attribute = models.ForeignKey(
740
+        'catalogue.ProductAttribute', verbose_name=_("Attribute"))
748 741
     product = models.ForeignKey(
749 742
         'catalogue.Product', related_name='attribute_values',
750 743
         verbose_name=_("Product"))
@@ -758,15 +751,18 @@ class AbstractProductAttributeValue(models.Model):
758 751
     value_option = models.ForeignKey(
759 752
         'catalogue.AttributeOption', blank=True, null=True,
760 753
         verbose_name=_("Value Option"))
761
-    value_entity = models.ForeignKey(
762
-        'catalogue.AttributeEntity', blank=True, null=True,
763
-        verbose_name=_("Value Entity"))
764 754
     value_file = models.FileField(
765 755
         upload_to=settings.OSCAR_IMAGE_FOLDER, max_length=255,
766 756
         blank=True, null=True)
767 757
     value_image = models.ImageField(
768 758
         upload_to=settings.OSCAR_IMAGE_FOLDER, max_length=255,
769 759
         blank=True, null=True)
760
+    value_entity = GenericForeignKey(
761
+        'entity_content_type', 'entity_object_id')
762
+    entity_content_type = models.ForeignKey(
763
+        ContentType, null=True, blank=True, editable=False)
764
+    entity_object_id = models.PositiveIntegerField(
765
+        null=True, blank=True, editable=False)
770 766
 
771 767
     def _get_value(self):
772 768
         return getattr(self, 'value_%s' % self.attribute.type)
@@ -810,6 +806,14 @@ class AbstractProductAttributeValue(models.Model):
810 806
     def _richtext_as_text(self):
811 807
         return strip_tags(self.value)
812 808
 
809
+    @property
810
+    def _entity_as_text(self):
811
+        """
812
+        Returns the unicode representation of the related model. You likely
813
+        want to customise this (and maybe _entity_as_html) if you use entities.
814
+        """
815
+        return unicode(self.value)
816
+
813 817
     @property
814 818
     def value_as_html(self):
815 819
         """
@@ -868,53 +872,6 @@ class AbstractAttributeOption(models.Model):
868 872
         verbose_name_plural = _('Attribute Options')
869 873
 
870 874
 
871
-class AbstractAttributeEntity(models.Model):
872
-    """
873
-    Provides an attribute type to enable relationships with other models
874
-    """
875
-    name = models.CharField(_("Name"), max_length=255)
876
-    slug = models.SlugField(
877
-        _("Slug"), max_length=255, unique=False, blank=True)
878
-    type = models.ForeignKey(
879
-        'catalogue.AttributeEntityType', related_name='entities',
880
-        verbose_name=_("Type"))
881
-
882
-    def __unicode__(self):
883
-        return self.name
884
-
885
-    class Meta:
886
-        abstract = True
887
-        verbose_name = _('Attribute Entity')
888
-        verbose_name_plural = _('Attribute Entities')
889
-
890
-    def save(self, *args, **kwargs):
891
-        if not self.slug:
892
-            self.slug = slugify(self.name)
893
-        super(AbstractAttributeEntity, self).save(*args, **kwargs)
894
-
895
-
896
-class AbstractAttributeEntityType(models.Model):
897
-    """
898
-    Provides the name of the model involved in an entity relationship
899
-    """
900
-    name = models.CharField(_("Name"), max_length=255)
901
-    slug = models.SlugField(
902
-        _("Slug"), max_length=255, unique=False, blank=True)
903
-
904
-    def __unicode__(self):
905
-        return self.name
906
-
907
-    class Meta:
908
-        abstract = True
909
-        verbose_name = _('Attribute Entity Type')
910
-        verbose_name_plural = _('Attribute Entity Types')
911
-
912
-    def save(self, *args, **kwargs):
913
-        if not self.slug:
914
-            self.slug = slugify(self.name)
915
-        super(AbstractAttributeEntityType, self).save(*args, **kwargs)
916
-
917
-
918 875
 class AbstractOption(models.Model):
919 876
     """
920 877
     An option that can be selected for a particular item when the product

+ 0
- 8
oscar/apps/catalogue/admin.py Voir le fichier

@@ -2,8 +2,6 @@ from django.contrib import admin
2 2
 from oscar.core.loading import get_model
3 3
 from treebeard.admin import TreeAdmin
4 4
 
5
-AttributeEntity = get_model('catalogue', 'AttributeEntity')
6
-AttributeEntityType = get_model('catalogue', 'AttributeEntityType')
7 5
 AttributeOption = get_model('catalogue', 'AttributeOption')
8 6
 AttributeOptionGroup = get_model('catalogue', 'AttributeOptionGroup')
9 7
 Category = get_model('catalogue', 'Category')
@@ -71,10 +69,6 @@ class AttributeOptionGroupAdmin(admin.ModelAdmin):
71 69
     inlines = [AttributeOptionInline, ]
72 70
 
73 71
 
74
-class AttributeEntityAdmin(admin.ModelAdmin):
75
-    list_display = ('name', )
76
-
77
-
78 72
 class CategoryAdmin(TreeAdmin):
79 73
     pass
80 74
 
@@ -84,8 +78,6 @@ admin.site.register(Product, ProductAdmin)
84 78
 admin.site.register(ProductAttribute, ProductAttributeAdmin)
85 79
 admin.site.register(ProductAttributeValue, ProductAttributeValueAdmin)
86 80
 admin.site.register(AttributeOptionGroup, AttributeOptionGroupAdmin)
87
-admin.site.register(AttributeEntity, AttributeEntityAdmin)
88
-admin.site.register(AttributeEntityType)
89 81
 admin.site.register(Option, OptionAdmin)
90 82
 admin.site.register(ProductImage)
91 83
 admin.site.register(Category, CategoryAdmin)

+ 185
- 0
oscar/apps/catalogue/migrations/0024_auto__del_attributeentity__del_attributeentitytype__del_field_producta.py Voir le fichier

@@ -0,0 +1,185 @@
1
+# -*- coding: utf-8 -*-
2
+from south.utils import datetime_utils as datetime
3
+from south.db import db
4
+from south.v2 import SchemaMigration
5
+from django.db import models
6
+
7
+
8
+class Migration(SchemaMigration):
9
+
10
+    def forwards(self, orm):
11
+        # Deleting model 'AttributeEntity'
12
+        db.delete_table(u'catalogue_attributeentity')
13
+
14
+        # Deleting model 'AttributeEntityType'
15
+        db.delete_table(u'catalogue_attributeentitytype')
16
+
17
+        # Deleting field 'ProductAttributeValue.value_entity'
18
+        db.delete_column(u'catalogue_productattributevalue', 'value_entity_id')
19
+
20
+        # Adding field 'ProductAttributeValue.entity_content_type'
21
+        db.add_column(u'catalogue_productattributevalue', 'entity_content_type',
22
+                      self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True, blank=True),
23
+                      keep_default=False)
24
+
25
+        # Adding field 'ProductAttributeValue.entity_object_id'
26
+        db.add_column(u'catalogue_productattributevalue', 'entity_object_id',
27
+                      self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True),
28
+                      keep_default=False)
29
+
30
+        # Deleting field 'ProductAttribute.entity_type'
31
+        db.delete_column(u'catalogue_productattribute', 'entity_type_id')
32
+
33
+
34
+    def backwards(self, orm):
35
+        # Adding model 'AttributeEntity'
36
+        db.create_table(u'catalogue_attributeentity', (
37
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, blank=True)),
38
+            ('type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='entities', to=orm['catalogue.AttributeEntityType'])),
39
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
40
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
41
+        ))
42
+        db.send_create_signal(u'catalogue', ['AttributeEntity'])
43
+
44
+        # Adding model 'AttributeEntityType'
45
+        db.create_table(u'catalogue_attributeentitytype', (
46
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, blank=True)),
47
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
48
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
49
+        ))
50
+        db.send_create_signal(u'catalogue', ['AttributeEntityType'])
51
+
52
+        # Adding field 'ProductAttributeValue.value_entity'
53
+        db.add_column(u'catalogue_productattributevalue', 'value_entity',
54
+                      self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeEntity'], null=True, blank=True),
55
+                      keep_default=False)
56
+
57
+        # Deleting field 'ProductAttributeValue.entity_content_type'
58
+        db.delete_column(u'catalogue_productattributevalue', 'entity_content_type_id')
59
+
60
+        # Deleting field 'ProductAttributeValue.entity_object_id'
61
+        db.delete_column(u'catalogue_productattributevalue', 'entity_object_id')
62
+
63
+        # Adding field 'ProductAttribute.entity_type'
64
+        db.add_column(u'catalogue_productattribute', 'entity_type',
65
+                      self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.AttributeEntityType'], null=True, blank=True),
66
+                      keep_default=False)
67
+
68
+
69
+    models = {
70
+        u'catalogue.attributeoption': {
71
+            'Meta': {'object_name': 'AttributeOption'},
72
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': u"orm['catalogue.AttributeOptionGroup']"}),
73
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74
+            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
75
+        },
76
+        u'catalogue.attributeoptiongroup': {
77
+            'Meta': {'object_name': 'AttributeOptionGroup'},
78
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
80
+        },
81
+        u'catalogue.category': {
82
+            'Meta': {'ordering': "['full_name']", 'object_name': 'Category'},
83
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
84
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
85
+            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
86
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
87
+            'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
88
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
89
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
90
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
91
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'})
92
+        },
93
+        u'catalogue.option': {
94
+            'Meta': {'object_name': 'Option'},
95
+            'code': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
96
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
97
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
98
+            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
99
+        },
100
+        u'catalogue.product': {
101
+            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
102
+            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.ProductAttribute']", 'through': u"orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
103
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Category']", 'through': u"orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
104
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
105
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
106
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
107
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
108
+            'is_discountable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
109
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': u"orm['catalogue.Product']"}),
110
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'products'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['catalogue.ProductClass']"}),
111
+            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
112
+            'rating': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
113
+            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Product']", 'symmetrical': 'False', 'through': u"orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
114
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
115
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
116
+            'upc': ('oscar.models.fields.NullCharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
117
+        },
118
+        u'catalogue.productattribute': {
119
+            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
120
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128'}),
121
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
123
+            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
124
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': u"orm['catalogue.ProductClass']"}),
125
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
126
+            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
127
+        },
128
+        u'catalogue.productattributevalue': {
129
+            'Meta': {'unique_together': "(('attribute', 'product'),)", 'object_name': 'ProductAttributeValue'},
130
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.ProductAttribute']"}),
131
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
132
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
133
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
134
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_values'", 'to': u"orm['catalogue.Product']"}),
135
+            'value_boolean': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
136
+            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
137
+            'value_file': ('django.db.models.fields.files.FileField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
138
+            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
139
+            'value_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
140
+            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
141
+            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
142
+            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
143
+            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
144
+        },
145
+        u'catalogue.productcategory': {
146
+            'Meta': {'ordering': "['product', 'category']", 'unique_together': "(('product', 'category'),)", 'object_name': 'ProductCategory'},
147
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Category']"}),
148
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
149
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
150
+        },
151
+        u'catalogue.productclass': {
152
+            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
153
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
154
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
155
+            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
156
+            'requires_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
157
+            'slug': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
158
+            'track_stock': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
159
+        },
160
+        u'catalogue.productimage': {
161
+            'Meta': {'ordering': "['display_order']", 'unique_together': "(('product', 'display_order'),)", 'object_name': 'ProductImage'},
162
+            'caption': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
163
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
164
+            'display_order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
165
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
166
+            'original': ('django.db.models.fields.files.ImageField', [], {'max_length': '255'}),
167
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'images'", 'to': u"orm['catalogue.Product']"})
168
+        },
169
+        u'catalogue.productrecommendation': {
170
+            'Meta': {'ordering': "['primary', '-ranking']", 'unique_together': "(('primary', 'recommendation'),)", 'object_name': 'ProductRecommendation'},
171
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
172
+            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': u"orm['catalogue.Product']"}),
173
+            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
174
+            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
175
+        },
176
+        u'contenttypes.contenttype': {
177
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
178
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
179
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
180
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
181
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
182
+        }
183
+    }
184
+
185
+    complete_apps = ['catalogue']

+ 0
- 8
oscar/apps/catalogue/models.py Voir le fichier

@@ -40,14 +40,6 @@ class AttributeOption(AbstractAttributeOption):
40 40
     pass
41 41
 
42 42
 
43
-class AttributeEntity(AbstractAttributeEntity):
44
-    pass
45
-
46
-
47
-class AttributeEntityType(AbstractAttributeEntityType):
48
-    pass
49
-
50
-
51 43
 class Option(AbstractOption):
52 44
     pass
53 45
 

+ 11
- 9
oscar/apps/dashboard/catalogue/forms.py Voir le fichier

@@ -224,10 +224,11 @@ def _attr_multi_option_field(attribute):
224 224
 
225 225
 
226 226
 def _attr_entity_field(attribute):
227
-    return forms.ModelChoiceField(
228
-        label=attribute.name,
229
-        required=attribute.required,
230
-        queryset=attribute.entity_type.entities.all())
227
+    # Product entities don't have out-of-the-box supported in the ProductForm.
228
+    # There is no ModelChoiceField for generic foreign keys, and there's no
229
+    # good default behaviour anyway; offering a choice of *all* model instances
230
+    # is hardly useful.
231
+    return None
231 232
 
232 233
 
233 234
 def _attr_numeric_field(attribute):
@@ -329,11 +330,12 @@ class ProductForm(forms.ModelForm):
329 330
 
330 331
     def add_attribute_fields(self, is_parent=False):
331 332
         for attribute in self.instance.product_class.attributes.all():
332
-            self.fields['attr_%s' % attribute.code] \
333
-                = self.get_attribute_field(attribute)
334
-            # Attributes are not required for a parent product
335
-            if is_parent:
336
-                self.fields['attr_%s' % attribute.code].required = False
333
+            field = self.get_attribute_field(attribute)
334
+            if field:
335
+                self.fields['attr_%s' % attribute.code] = field
336
+                # Attributes are not required for a parent product
337
+                if is_parent:
338
+                    self.fields['attr_%s' % attribute.code].required = False
337 339
 
338 340
     def get_attribute_field(self, attribute):
339 341
         return self.FIELD_FACTORIES[attribute.type](attribute)

+ 1
- 1
oscar/test/newfactories.py Voir le fichier

@@ -26,7 +26,7 @@ __all__ = ["UserFactory", "CountryFactory", "UserAddressFactory",
26 26
            "BasketFactory", "VoucherFactory", "ProductFactory",
27 27
            "StockRecordFactory", "ProductAttributeFactory",
28 28
            "ProductAttributeValueFactory", "AttributeOptionGroupFactory",
29
-           "AttributeOptionFactory"]
29
+           "AttributeOptionFactory", "PartnerFactory"]
30 30
 
31 31
 
32 32
 class UserFactory(factory.DjangoModelFactory):

+ 53
- 30
sites/sandbox/fixtures/variants.json Voir le fichier

@@ -57,7 +57,7 @@
57 57
         "parent": null, 
58 58
         "title": "Oscar T-shirt", 
59 59
         "date_updated": "2013-12-12T16:33:57.426Z", 
60
-        "upc": null, 
60
+        "upc": "", 
61 61
         "is_discountable": true, 
62 62
         "date_created": "2013-12-12T16:33:57.426Z", 
63 63
         "product_options": [], 
@@ -74,7 +74,7 @@
74 74
         "parent": 1, 
75 75
         "title": "", 
76 76
         "date_updated": "2013-12-12T16:34:14.023Z", 
77
-        "upc": null, 
77
+        "upc": "", 
78 78
         "is_discountable": true, 
79 79
         "date_created": "2013-12-12T16:34:14.023Z", 
80 80
         "product_options": [], 
@@ -91,7 +91,7 @@
91 91
         "parent": 1, 
92 92
         "title": "", 
93 93
         "date_updated": "2013-12-12T16:34:32.170Z", 
94
-        "upc": null, 
94
+        "upc": "", 
95 95
         "is_discountable": true, 
96 96
         "date_created": "2013-12-12T16:34:32.170Z", 
97 97
         "product_options": [], 
@@ -108,7 +108,7 @@
108 108
         "parent": 1, 
109 109
         "title": "", 
110 110
         "date_updated": "2013-12-12T17:32:15.016Z", 
111
-        "upc": null, 
111
+        "upc": "", 
112 112
         "is_discountable": true, 
113 113
         "date_created": "2013-12-12T17:32:15.016Z", 
114 114
         "product_options": [], 
@@ -125,7 +125,7 @@
125 125
         "parent": null, 
126 126
         "title": "commandlinefu T-shirt", 
127 127
         "date_updated": "2013-12-13T11:34:21.810Z", 
128
-        "upc": null, 
128
+        "upc": "", 
129 129
         "is_discountable": true, 
130 130
         "date_created": "2013-12-13T11:34:21.810Z", 
131 131
         "product_options": [], 
@@ -142,7 +142,7 @@
142 142
         "parent": 5, 
143 143
         "title": "", 
144 144
         "date_updated": "2013-12-13T11:37:08.138Z", 
145
-        "upc": null, 
145
+        "upc": "", 
146 146
         "is_discountable": true, 
147 147
         "date_created": "2013-12-13T11:37:08.138Z", 
148 148
         "product_options": [], 
@@ -159,7 +159,7 @@
159 159
         "parent": 5, 
160 160
         "title": "", 
161 161
         "date_updated": "2013-12-13T11:37:45.834Z", 
162
-        "upc": null, 
162
+        "upc": "", 
163 163
         "is_discountable": true, 
164 164
         "date_created": "2013-12-13T11:37:45.834Z", 
165 165
         "product_options": [], 
@@ -176,7 +176,7 @@
176 176
         "parent": null, 
177 177
         "title": "Tangent T-shirt", 
178 178
         "date_updated": "2013-12-13T11:38:15.107Z", 
179
-        "upc": null, 
179
+        "upc": "", 
180 180
         "is_discountable": true, 
181 181
         "date_created": "2013-12-13T11:38:15.107Z", 
182 182
         "product_options": [], 
@@ -193,7 +193,7 @@
193 193
         "parent": 8, 
194 194
         "title": "", 
195 195
         "date_updated": "2013-12-13T11:38:49.769Z", 
196
-        "upc": null, 
196
+        "upc": "", 
197 197
         "is_discountable": true, 
198 198
         "date_created": "2013-12-13T11:38:49.769Z", 
199 199
         "product_options": [], 
@@ -210,7 +210,7 @@
210 210
         "parent": 8, 
211 211
         "title": "", 
212 212
         "date_updated": "2013-12-13T11:39:12.859Z", 
213
-        "upc": null, 
213
+        "upc": "", 
214 214
         "is_discountable": true, 
215 215
         "date_created": "2013-12-13T11:39:12.859Z", 
216 216
         "product_options": [], 
@@ -227,7 +227,7 @@
227 227
         "parent": 8, 
228 228
         "title": "", 
229 229
         "date_updated": "2013-12-13T11:39:53.073Z", 
230
-        "upc": null, 
230
+        "upc": "", 
231 231
         "is_discountable": true, 
232 232
         "date_created": "2013-12-13T11:39:53.073Z", 
233 233
         "product_options": [], 
@@ -239,11 +239,10 @@
239 239
     "pk": 1, 
240 240
     "model": "catalogue.productattribute", 
241 241
     "fields": {
242
-        "code": "size", 
242
+        "required": true, 
243 243
         "product_class": 1, 
244
-        "entity_type": null, 
245 244
         "option_group": 1, 
246
-        "required": true, 
245
+        "code": "size", 
247 246
         "type": "option", 
248 247
         "name": "Size"
249 248
     }
@@ -256,12 +255,15 @@
256 255
         "value_integer": null, 
257 256
         "product": 2, 
258 257
         "value_float": null, 
258
+        "entity_content_type": null, 
259 259
         "value_text": null, 
260 260
         "attribute": 1, 
261 261
         "value_richtext": null, 
262
-        "value_entity": null, 
262
+        "entity_object_id": null, 
263
+        "value_image": "", 
263 264
         "value_option": 1, 
264
-        "value_boolean": null
265
+        "value_boolean": null, 
266
+        "value_file": ""
265 267
     }
266 268
 },
267 269
 {
@@ -272,12 +274,15 @@
272 274
         "value_integer": null, 
273 275
         "product": 3, 
274 276
         "value_float": null, 
277
+        "entity_content_type": null, 
275 278
         "value_text": null, 
276 279
         "attribute": 1, 
277 280
         "value_richtext": null, 
278
-        "value_entity": null, 
281
+        "entity_object_id": null, 
282
+        "value_image": "", 
279 283
         "value_option": 2, 
280
-        "value_boolean": null
284
+        "value_boolean": null, 
285
+        "value_file": ""
281 286
     }
282 287
 },
283 288
 {
@@ -288,12 +293,15 @@
288 293
         "value_integer": null, 
289 294
         "product": 4, 
290 295
         "value_float": null, 
296
+        "entity_content_type": null, 
291 297
         "value_text": null, 
292 298
         "attribute": 1, 
293 299
         "value_richtext": null, 
294
-        "value_entity": null, 
300
+        "entity_object_id": null, 
301
+        "value_image": "", 
295 302
         "value_option": 3, 
296
-        "value_boolean": null
303
+        "value_boolean": null, 
304
+        "value_file": ""
297 305
     }
298 306
 },
299 307
 {
@@ -304,12 +312,15 @@
304 312
         "value_integer": null, 
305 313
         "product": 6, 
306 314
         "value_float": null, 
315
+        "entity_content_type": null, 
307 316
         "value_text": null, 
308 317
         "attribute": 1, 
309 318
         "value_richtext": null, 
310
-        "value_entity": null, 
319
+        "entity_object_id": null, 
320
+        "value_image": "", 
311 321
         "value_option": 1, 
312
-        "value_boolean": null
322
+        "value_boolean": null, 
323
+        "value_file": ""
313 324
     }
314 325
 },
315 326
 {
@@ -320,12 +331,15 @@
320 331
         "value_integer": null, 
321 332
         "product": 7, 
322 333
         "value_float": null, 
334
+        "entity_content_type": null, 
323 335
         "value_text": null, 
324 336
         "attribute": 1, 
325 337
         "value_richtext": null, 
326
-        "value_entity": null, 
338
+        "entity_object_id": null, 
339
+        "value_image": "", 
327 340
         "value_option": 2, 
328
-        "value_boolean": null
341
+        "value_boolean": null, 
342
+        "value_file": ""
329 343
     }
330 344
 },
331 345
 {
@@ -336,12 +350,15 @@
336 350
         "value_integer": null, 
337 351
         "product": 9, 
338 352
         "value_float": null, 
353
+        "entity_content_type": null, 
339 354
         "value_text": null, 
340 355
         "attribute": 1, 
341 356
         "value_richtext": null, 
342
-        "value_entity": null, 
357
+        "entity_object_id": null, 
358
+        "value_image": "", 
343 359
         "value_option": 1, 
344
-        "value_boolean": null
360
+        "value_boolean": null, 
361
+        "value_file": ""
345 362
     }
346 363
 },
347 364
 {
@@ -352,12 +369,15 @@
352 369
         "value_integer": null, 
353 370
         "product": 10, 
354 371
         "value_float": null, 
372
+        "entity_content_type": null, 
355 373
         "value_text": null, 
356 374
         "attribute": 1, 
357 375
         "value_richtext": null, 
358
-        "value_entity": null, 
376
+        "entity_object_id": null, 
377
+        "value_image": "", 
359 378
         "value_option": 2, 
360
-        "value_boolean": null
379
+        "value_boolean": null, 
380
+        "value_file": ""
361 381
     }
362 382
 },
363 383
 {
@@ -368,12 +388,15 @@
368 388
         "value_integer": null, 
369 389
         "product": 11, 
370 390
         "value_float": null, 
391
+        "entity_content_type": null, 
371 392
         "value_text": null, 
372 393
         "attribute": 1, 
373 394
         "value_richtext": null, 
374
-        "value_entity": null, 
395
+        "entity_object_id": null, 
396
+        "value_image": "", 
375 397
         "value_option": 3, 
376
-        "value_boolean": null
398
+        "value_boolean": null, 
399
+        "value_file": ""
377 400
     }
378 401
 },
379 402
 {

+ 9
- 1
tests/integration/catalogue/product_tests.py Voir le fichier

@@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError
5 5
 
6 6
 from oscar.apps.catalogue.models import (Product, ProductClass,
7 7
                                          ProductAttribute,
8
-                                         AttributeOptionGroup,
9 8
                                          AttributeOption)
10 9
 from oscar.test import factories
11 10
 
@@ -127,3 +126,12 @@ class ProductAttributeCreationTests(TestCase):
127 126
         invalid_option = AttributeOption(option='invalid option')
128 127
         self.assertRaises(
129 128
             ValidationError, pa.validate_value, invalid_option)
129
+
130
+    def test_entity_attributes(self):
131
+        unrelated_object = factories.PartnerFactory()
132
+        attribute = factories.ProductAttributeFactory(type='entity')
133
+
134
+        attribute_value = factories.ProductAttributeValueFactory(
135
+            attribute=attribute, value_entity=unrelated_object)
136
+
137
+        self.assertEqual(attribute_value.value, unrelated_object)

Chargement…
Annuler
Enregistrer