Skip to content
This repository
Browse code

Reworked categories to enfore uniqueness at python-level.

To work around MySQL bug with fields of > 255 chars not being allowed to
have unique indexes.
  • Loading branch information...
commit 166d7be192798391074801ff413926399bee96d3 1 parent c117c43
David Winterbottom authored July 09, 2012
12  oscar/apps/catalogue/abstract_models.py
@@ -56,7 +56,7 @@ class AbstractCategory(MP_Node):
56 56
     name = models.CharField(max_length=255, db_index=True)
57 57
     description = models.TextField(blank=True, null=True)
58 58
     image = models.ImageField(upload_to='categories', blank=True, null=True)
59  
-    slug = models.SlugField(max_length=1024, db_index=True, editable=False, unique=True)
  59
+    slug = models.SlugField(max_length=1024, db_index=True, editable=False)
60 60
     full_name = models.CharField(max_length=1024, db_index=True, editable=False)
61 61
 
62 62
     _slug_separator = '/'
@@ -76,6 +76,16 @@ def save(self, update_slugs=True, *args, **kwargs):
76 76
             else:
77 77
                 self.slug = slug
78 78
                 self.full_name = self.name
  79
+
  80
+        # Enforce slug uniqueness here as MySQL can't handle a unique index on
  81
+        # the slug field
  82
+        try:
  83
+            match = self.__class__.objects.get(slug=self.slug)
  84
+        except self.__class__.DoesNotExist:
  85
+            pass
  86
+        else:
  87
+            if match.id != self.id:
  88
+                raise ValidationError(_("A category with slug '%(slug)s' already exists") % {'slug': self.slug})
79 89
         super(AbstractCategory, self).save(*args, **kwargs)
80 90
 
81 91
     def move(self, target, pos=None):
161  oscar/apps/catalogue/migrations/0005_auto__add_unique_category_slug.py
... ...
@@ -1,161 +0,0 @@
1  
-# encoding: utf-8
2  
-import datetime
3  
-from south.db import db
4  
-from south.v2 import SchemaMigration
5  
-from django.db import models
6  
-
7  
-class Migration(SchemaMigration):
8  
-
9  
-    def forwards(self, orm):
10  
-        
11  
-        # Adding unique constraint on 'Category', fields ['slug']
12  
-        db.create_unique('catalogue_category', ['slug'])
13  
-
14  
-
15  
-    def backwards(self, orm):
16  
-        
17  
-        # Removing unique constraint on 'Category', fields ['slug']
18  
-        db.delete_unique('catalogue_category', ['slug'])
19  
-
20  
-
21  
-    models = {
22  
-        'catalogue.attributeentity': {
23  
-            'Meta': {'object_name': 'AttributeEntity'},
24  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
25  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
26  
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
27  
-            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
28  
-        },
29  
-        'catalogue.attributeentitytype': {
30  
-            'Meta': {'object_name': 'AttributeEntityType'},
31  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
33  
-            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
34  
-        },
35  
-        'catalogue.attributeoption': {
36  
-            'Meta': {'object_name': 'AttributeOption'},
37  
-            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
38  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
39  
-            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
40  
-        },
41  
-        'catalogue.attributeoptiongroup': {
42  
-            'Meta': {'object_name': 'AttributeOptionGroup'},
43  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
44  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
45  
-        },
46  
-        'catalogue.category': {
47  
-            'Meta': {'ordering': "['full_name']", 'object_name': 'Category'},
48  
-            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
49  
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
50  
-            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
51  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
52  
-            'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
53  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
54  
-            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
55  
-            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
56  
-            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '1024', 'db_index': 'True'})
57  
-        },
58  
-        'catalogue.contributor': {
59  
-            'Meta': {'object_name': 'Contributor'},
60  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
61  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
62  
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
63  
-        },
64  
-        'catalogue.contributorrole': {
65  
-            'Meta': {'object_name': 'ContributorRole'},
66  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
67  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
68  
-            'name_plural': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
69  
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
70  
-        },
71  
-        'catalogue.option': {
72  
-            'Meta': {'object_name': 'Option'},
73  
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
74  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
75  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
76  
-            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
77  
-        },
78  
-        'catalogue.product': {
79  
-            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
80  
-            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
81  
-            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
82  
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
83  
-            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
84  
-            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
85  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86  
-            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
87  
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
88  
-            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
89  
-            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
90  
-            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
91  
-            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
92  
-            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
93  
-            'status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'}),
94  
-            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
95  
-            'upc': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
96  
-        },
97  
-        'catalogue.productattribute': {
98  
-            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
99  
-            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
100  
-            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
101  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
102  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
103  
-            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
104  
-            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
105  
-            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
106  
-            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
107  
-        },
108  
-        'catalogue.productattributevalue': {
109  
-            'Meta': {'object_name': 'ProductAttributeValue'},
110  
-            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
111  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
112  
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_values'", 'to': "orm['catalogue.Product']"}),
113  
-            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
114  
-            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
115  
-            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
116  
-            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
117  
-            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
118  
-            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
119  
-            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
120  
-            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
121  
-        },
122  
-        'catalogue.productcategory': {
123  
-            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
124  
-            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
125  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
126  
-            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
127  
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
128  
-        },
129  
-        'catalogue.productclass': {
130  
-            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
131  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
132  
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
133  
-            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
134  
-            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
135  
-        },
136  
-        'catalogue.productcontributor': {
137  
-            'Meta': {'object_name': 'ProductContributor'},
138  
-            'contributor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Contributor']"}),
139  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
140  
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"}),
141  
-            'role': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ContributorRole']", 'null': 'True', 'blank': 'True'})
142  
-        },
143  
-        'catalogue.productimage': {
144  
-            'Meta': {'ordering': "['display_order']", 'unique_together': "(('product', 'display_order'),)", 'object_name': 'ProductImage'},
145  
-            'caption': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
146  
-            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
147  
-            'display_order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
148  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
149  
-            'original': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
150  
-            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'images'", 'to': "orm['catalogue.Product']"})
151  
-        },
152  
-        'catalogue.productrecommendation': {
153  
-            'Meta': {'object_name': 'ProductRecommendation'},
154  
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
155  
-            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
156  
-            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
157  
-            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
158  
-        }
159  
-    }
160  
-
161  
-    complete_apps = ['catalogue']
0  ...ttributevalue_value_boolean__add_field_product.py → ...ttributevalue_value_boolean__add_field_product.py
File renamed without changes
20  oscar/templates/dashboard/catalogue/category_list.html
@@ -24,17 +24,15 @@
24 24
 
25 25
 {% block dashboard_content %}
26 26
 
27  
-<div class="well well-info">
28  
-	<p>You are editing:
29  
-    <a href="{% url dashboard:catalogue-category-list %}">Home</a>
30  
-    {% if ancestors %}
31  
-	    &gt;
32  
-        {% for ancestor in ancestors %}
33  
-            <a href="{% url dashboard:catalogue-category-detail-list pk=ancestor.pk %}">{{ ancestor.name }}</a>{% if not forloop.last %} > {% endif %}
34  
-        {% endfor %}
35  
-    {% endif %}
36  
-	</p>
37  
-</div>
  27
+<p>You are editing:
  28
+<a href="{% url dashboard:catalogue-category-list %}">Home</a>
  29
+{% if ancestors %}
  30
+	&gt;
  31
+	{% for ancestor in ancestors %}
  32
+		<a href="{% url dashboard:catalogue-category-detail-list pk=ancestor.pk %}">{{ ancestor.name }}</a>{% if not forloop.last %} > {% endif %}
  33
+	{% endfor %}
  34
+{% endif %}
  35
+</p>
38 36
 
39 37
 <table class="table table-striped table-bordered">
40 38
 	<thead>
6  tests/unit/catalogue_tests.py
@@ -124,6 +124,12 @@ def test_supports_has_children_method(self):
124 124
         root.add_child(name="Books")
125 125
         self.assertTrue(root.has_children())
126 126
 
  127
+    def test_enforces_slug_uniqueness(self):
  128
+        root = Category.add_root(name="Products")
  129
+        root.add_child(name="Books")
  130
+        with self.assertRaises(ValidationError):
  131
+            root.add_child(name="Books")
  132
+
127 133
 
128 134
 class ProductTests(TestCase):
129 135
 

0 notes on commit 166d7be

Please sign in to comment.
Something went wrong with that request. Please try again.