From e8cc050f2be1e3cff4b905ef09df52ed42250a28 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Fri, 29 Oct 2010 10:46:35 -0400 Subject: [PATCH] Switched Attribute ManyToManyValues to have a ManyToManyField to ForeignKeyValues instead of a CommaSeparatedIntegerField. This allows joined queries and referential integrity. In the long run, it also simplifies adding support for non-integer primary keys. --- ...o__del_field_manytomanyvalue_object_ids.py | 139 ++++++++++++++++++ models/base.py | 34 +++-- 2 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 migrations/0008_auto__del_field_manytomanyvalue_object_ids.py diff --git a/migrations/0008_auto__del_field_manytomanyvalue_object_ids.py b/migrations/0008_auto__del_field_manytomanyvalue_object_ids.py new file mode 100644 index 0000000..db0451d --- /dev/null +++ b/migrations/0008_auto__del_field_manytomanyvalue_object_ids.py @@ -0,0 +1,139 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting field 'ManyToManyValue.object_ids' + db.delete_column('philo_manytomanyvalue', 'object_ids') + + # Adding M2M table for field values on 'ManyToManyValue' + db.create_table('philo_manytomanyvalue_values', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('manytomanyvalue', models.ForeignKey(orm['philo.manytomanyvalue'], null=False)), + ('foreignkeyvalue', models.ForeignKey(orm['philo.foreignkeyvalue'], null=False)) + )) + db.create_unique('philo_manytomanyvalue_values', ['manytomanyvalue_id', 'foreignkeyvalue_id']) + + + def backwards(self, orm): + + # Adding field 'ManyToManyValue.object_ids' + db.add_column('philo_manytomanyvalue', 'object_ids', self.gf('django.db.models.fields.CommaSeparatedIntegerField')(max_length=300, null=True, blank=True), keep_default=False) + + # Removing M2M table for field values on 'ManyToManyValue' + db.delete_table('philo_manytomanyvalue_values') + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.collection': { + 'Meta': {'object_name': 'Collection'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.collectionmember': { + 'Meta': {'object_name': 'CollectionMember'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.contentlet': { + 'Meta': {'object_name': 'Contentlet'}, + 'content': ('philo.models.fields.TemplateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"}) + }, + 'philo.contentreference': { + 'Meta': {'object_name': 'ContentReference'}, + 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"}) + }, + 'philo.file': { + 'Meta': {'object_name': 'File'}, + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.foreignkeyvalue': { + 'Meta': {'object_name': 'ForeignKeyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'foreign_key_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.jsonvalue': { + 'Meta': {'object_name': 'JSONValue'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'value': ('philo.models.fields.JSONField', [], {}) + }, + 'philo.manytomanyvalue': { + 'Meta': {'object_name': 'ManyToManyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'many_to_many_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.page': { + 'Meta': {'object_name': 'Page'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.redirect': { + 'Meta': {'object_name': 'Redirect'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'philo.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'philo.template': { + 'Meta': {'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}) + } + } + + complete_apps = ['philo'] diff --git a/models/base.py b/models/base.py index 0c2c975..0077250 100644 --- a/models/base.py +++ b/models/base.py @@ -46,7 +46,12 @@ def unregister_value_model(model): class AttributeValue(models.Model): - attribute = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id') + attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id') + + @property + def attribute(self): + return self.attribute_set.all()[0] + def apply_data(self, data): raise NotImplementedError @@ -81,7 +86,7 @@ class JSONValue(AttributeValue): class ForeignKeyValue(AttributeValue): - content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True) + content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True) object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True) value = generic.GenericForeignKey() @@ -104,15 +109,14 @@ class ForeignKeyValue(AttributeValue): class ManyToManyValue(AttributeValue): - # TODO: Change object_ids to object_pks. - content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True) - object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True) + content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True) + values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True) def get_object_id_list(self): - if not self.object_ids: + if not self.values.count(): return [] else: - return self.object_ids.split(',') + return self.values.values_list('object_id', flat=True) def get_value(self): if self.content_type is None: @@ -122,10 +126,22 @@ class ManyToManyValue(AttributeValue): def set_value(self, value): # Value is probably a queryset - but allow any iterable. + + # These lines shouldn't be necessary; however, if value is an EmptyQuerySet, + # the code won't work without them. Unclear why... + if not value: + value = [] + if isinstance(value, models.query.QuerySet): value = value.values_list('id', flat=True) - self.object_ids = ','.join([str(v) for v in value]) + self.values.filter(~models.Q(object_id__in=value)).delete() + current = self.get_object_id_list() + + for v in value: + if v in current: + continue + self.values.create(content_type=self.content_type, object_id=v) value = property(get_value, set_value) @@ -141,7 +157,7 @@ class ManyToManyValue(AttributeValue): else: self.content_type = cleaned_data.get('content_type', None) # If there is no value set in the cleaned data, clear the stored value. - self.object_ids = "" + self.value = [] class Meta: app_label = 'philo' -- 2.20.1