Browse Source

Use an AutoSlug field with offer ranges

This prevents an IntegrityError from slugs clashing after renaming a
range to avoid the checked-for name clash.

Fixes #1424
master
David Winterbottom 11 years ago
parent
commit
a0802b52af

+ 4
- 0
docs/source/releases/v0.8.rst View File

@@ -496,6 +496,10 @@ Migrations
496 496
       and do entity attribute changes and model deletions.
497 497
     - ``0025`` & ``0026`` - Schema & data migration to determine and save Product structure.
498 498
 
499
+* Offer:
500
+
501
+    - ``0033`` - Use an ``AutoSlug`` field for ``Range`` models
502
+
499 503
 * Order:
500 504
 
501 505
     - ``0029`` - Add ``unique_together`` to ``PaymentEventQuantity`` and ``ShippingEventQuantity``

+ 190
- 0
oscar/apps/offer/migrations/0033_auto__chg_field_range_slug.py View File

@@ -0,0 +1,190 @@
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
+
12
+        # Changing field 'Range.slug'
13
+        db.alter_column(u'offer_range', 'slug', self.gf('oscar.models.fields.autoslugfield.AutoSlugField')(allow_duplicates=False, max_length=128, separator=u'-', default='', unique=True, populate_from='name', overwrite=False))
14
+
15
+    def backwards(self, orm):
16
+
17
+        # Changing field 'Range.slug'
18
+        db.alter_column(u'offer_range', 'slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128, null=True))
19
+
20
+    models = {
21
+        u'catalogue.attributeoption': {
22
+            'Meta': {'object_name': 'AttributeOption'},
23
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': u"orm['catalogue.AttributeOptionGroup']"}),
24
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
25
+            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
26
+        },
27
+        u'catalogue.attributeoptiongroup': {
28
+            'Meta': {'object_name': 'AttributeOptionGroup'},
29
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
31
+        },
32
+        u'catalogue.category': {
33
+            'Meta': {'ordering': "['full_name']", 'object_name': 'Category'},
34
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
35
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
36
+            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
37
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
38
+            'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
39
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
40
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
41
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
42
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'})
43
+        },
44
+        u'catalogue.option': {
45
+            'Meta': {'object_name': 'Option'},
46
+            'code': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
47
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
48
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
49
+            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
50
+        },
51
+        u'catalogue.product': {
52
+            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
53
+            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.ProductAttribute']", 'through': u"orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
54
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Category']", 'through': u"orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
55
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
56
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
57
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
58
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
59
+            'is_discountable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
60
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['catalogue.Product']"}),
61
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'products'", 'null': 'True', 'on_delete': 'models.PROTECT', 'to': u"orm['catalogue.ProductClass']"}),
62
+            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
63
+            'rating': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
64
+            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Product']", 'symmetrical': 'False', 'through': u"orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
65
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
66
+            'structure': ('django.db.models.fields.CharField', [], {'default': "'standalone'", 'max_length': '10'}),
67
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
68
+            'upc': ('oscar.models.fields.NullCharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
69
+        },
70
+        u'catalogue.productattribute': {
71
+            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
72
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128'}),
73
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
75
+            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
76
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': u"orm['catalogue.ProductClass']"}),
77
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
78
+            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
79
+        },
80
+        u'catalogue.productattributevalue': {
81
+            'Meta': {'unique_together': "(('attribute', 'product'),)", 'object_name': 'ProductAttributeValue'},
82
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.ProductAttribute']"}),
83
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
84
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
85
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_values'", 'to': u"orm['catalogue.Product']"}),
87
+            'value_boolean': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
88
+            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
89
+            'value_file': ('django.db.models.fields.files.FileField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
90
+            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
91
+            'value_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
92
+            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
93
+            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
94
+            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
95
+            'value_text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
96
+        },
97
+        u'catalogue.productcategory': {
98
+            'Meta': {'ordering': "['product', 'category']", 'unique_together': "(('product', 'category'),)", 'object_name': 'ProductCategory'},
99
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Category']"}),
100
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
101
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
102
+        },
103
+        u'catalogue.productclass': {
104
+            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
105
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
106
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
107
+            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
108
+            'requires_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
109
+            'slug': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
110
+            'track_stock': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
111
+        },
112
+        u'catalogue.productrecommendation': {
113
+            'Meta': {'ordering': "['primary', '-ranking']", 'unique_together': "(('primary', 'recommendation'),)", 'object_name': 'ProductRecommendation'},
114
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
115
+            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': u"orm['catalogue.Product']"}),
116
+            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
117
+            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"})
118
+        },
119
+        u'contenttypes.contenttype': {
120
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
121
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
122
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
124
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
125
+        },
126
+        u'offer.benefit': {
127
+            'Meta': {'object_name': 'Benefit'},
128
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
129
+            'max_affected_items': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
130
+            'proxy_class': ('oscar.models.fields.NullCharField', [], {'default': 'None', 'max_length': '255', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
131
+            'range': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offer.Range']", 'null': 'True', 'blank': 'True'}),
132
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
133
+            'value': ('oscar.models.fields.PositiveDecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'})
134
+        },
135
+        u'offer.condition': {
136
+            'Meta': {'object_name': 'Condition'},
137
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
138
+            'proxy_class': ('oscar.models.fields.NullCharField', [], {'default': 'None', 'max_length': '255', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
139
+            'range': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offer.Range']", 'null': 'True', 'blank': 'True'}),
140
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
141
+            'value': ('oscar.models.fields.PositiveDecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'})
142
+        },
143
+        u'offer.conditionaloffer': {
144
+            'Meta': {'ordering': "['-priority']", 'object_name': 'ConditionalOffer'},
145
+            'benefit': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offer.Benefit']"}),
146
+            'condition': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offer.Condition']"}),
147
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
148
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
149
+            'end_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
150
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
151
+            'max_basket_applications': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
152
+            'max_discount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
153
+            'max_global_applications': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
154
+            'max_user_applications': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
155
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
156
+            'num_applications': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
157
+            'num_orders': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
158
+            'offer_type': ('django.db.models.fields.CharField', [], {'default': "'Site'", 'max_length': '128'}),
159
+            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
160
+            'redirect_url': ('oscar.models.fields.ExtendedURLField', [], {'max_length': '200', 'blank': 'True'}),
161
+            'slug': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'}),
162
+            'start_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
163
+            'status': ('django.db.models.fields.CharField', [], {'default': "'Open'", 'max_length': '64'}),
164
+            'total_discount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '12', 'decimal_places': '2'})
165
+        },
166
+        u'offer.range': {
167
+            'Meta': {'object_name': 'Range'},
168
+            'classes': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'classes'", 'blank': 'True', 'to': u"orm['catalogue.ProductClass']"}),
169
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
170
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
171
+            'excluded_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'excludes'", 'blank': 'True', 'to': u"orm['catalogue.Product']"}),
172
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
173
+            'included_categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'to': u"orm['catalogue.Category']"}),
174
+            'included_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'through': u"orm['offer.RangeProduct']", 'to': u"orm['catalogue.Product']"}),
175
+            'includes_all_products': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
176
+            'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
177
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
178
+            'proxy_class': ('oscar.models.fields.NullCharField', [], {'default': 'None', 'max_length': '255', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
179
+            'slug': ('oscar.models.fields.autoslugfield.AutoSlugField', [], {'allow_duplicates': 'False', 'max_length': '128', 'separator': "u'-'", 'blank': 'True', 'unique': 'True', 'populate_from': "'name'", 'overwrite': 'False'})
180
+        },
181
+        u'offer.rangeproduct': {
182
+            'Meta': {'unique_together': "(('range', 'product'),)", 'object_name': 'RangeProduct'},
183
+            'display_order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
184
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
185
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['catalogue.Product']"}),
186
+            'range': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['offer.Range']"})
187
+        }
188
+    }
189
+
190
+    complete_apps = ['offer']

+ 3
- 6
oscar/apps/offer/models.py View File

@@ -744,7 +744,8 @@ class Range(models.Model):
744 744
     Represents a range of products that can be used within an offer
745 745
     """
746 746
     name = models.CharField(_("Name"), max_length=128, unique=True)
747
-    slug = models.SlugField(_('Slug'), max_length=128, unique=True, null=True)
747
+    slug = fields.AutoSlugField(
748
+        _("Slug"), max_length=128, unique=True, populate_from="name")
748 749
 
749 750
     description = models.TextField(blank=True)
750 751
 
@@ -793,11 +794,7 @@ class Range(models.Model):
793 794
         return reverse('catalogue:range', kwargs={
794 795
             'slug': self.slug})
795 796
 
796
-    def save(self, *args, **kwargs):
797
-        if not self.slug:
798
-            self.slug = slugify(self.name)
799
-
800
-        # Save Range
797
+    def _save(self, *args, **kwargs):
801 798
         super(Range, self).save(*args, **kwargs)
802 799
 
803 800
     def add_product(self, product, display_order=None):

+ 9
- 0
tests/integration/offer/range_tests.py View File

@@ -73,3 +73,12 @@ class TestPartialRange(TestCase):
73 73
         self.range.classes.add(self.prod.get_product_class())
74 74
         self.range.excluded_products.add(self.prod)
75 75
         self.assertFalse(self.range.contains_product(self.prod))
76
+
77
+
78
+class TestRangeModle(TestCase):
79
+
80
+    def test_ensures_unique_slugs_are_used(self):
81
+        first_range = models.Range.objects.create(name="Foo")
82
+        first_range.name = "Bar"
83
+        first_range.save()
84
+        models.Range.objects.create(name="Foo")

Loading…
Cancel
Save