summaryrefslogtreecommitdiff
path: root/ishtar_common/model_merging.py
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@proxience.com>2014-11-28 11:30:02 +0100
committerÉtienne Loks <etienne.loks@proxience.com>2014-11-28 11:30:02 +0100
commita62abdcb0c6daa71bc8bef293c20d911a63ccc60 (patch)
treefc3cb300358ed90b4d08dac9a274bcc124dcec01 /ishtar_common/model_merging.py
parent9dd0ba91bdd9c76b42f4d6b4791633d7eac95b4a (diff)
downloadIshtar-a62abdcb0c6daa71bc8bef293c20d911a63ccc60.tar.bz2
Ishtar-a62abdcb0c6daa71bc8bef293c20d911a63ccc60.zip
Merge action implementation (person and organization)
Diffstat (limited to 'ishtar_common/model_merging.py')
-rw-r--r--ishtar_common/model_merging.py128
1 files changed, 128 insertions, 0 deletions
diff --git a/ishtar_common/model_merging.py b/ishtar_common/model_merging.py
new file mode 100644
index 000000000..b8c145fcb
--- /dev/null
+++ b/ishtar_common/model_merging.py
@@ -0,0 +1,128 @@
+# from https://djangosnippets.org/snippets/2283/
+
+from django.db import transaction
+from django.db.models import get_models, Model
+from django.contrib.contenttypes.generic import GenericForeignKey
+
+@transaction.commit_on_success
+def merge_model_objects(primary_object, alias_objects=[], keep_old=False):
+ """
+ Use this function to merge model objects (i.e. Users, Organizations, Polls,
+ etc.) and migrate all of the related fields from the alias objects to the
+ primary object.
+
+ Usage:
+ from django.contrib.auth.models import User
+ primary_user = User.objects.get(email='good_email@example.com')
+ duplicate_user = User.objects.get(email='good_email+duplicate@example.com')
+ merge_model_objects(primary_user, duplicate_user)
+ """
+ MERGE_FIELDS = ('merge_candidate', 'merge_exclusion')
+
+ if not isinstance(alias_objects, list):
+ alias_objects = [alias_objects]
+
+ # check that all aliases are the same class as primary one and that
+ # they are subclass of model
+ primary_class = primary_object.__class__
+
+ if not issubclass(primary_class, Model):
+ raise TypeError('Only django.db.models.Model subclasses can be merged')
+
+ for alias_object in alias_objects:
+ if not isinstance(alias_object, primary_class):
+ raise TypeError('Only models of same class can be merged')
+
+ # Get a list of all GenericForeignKeys in all models
+ # TODO: this is a bit of a hack, since the generics framework should provide
+ # a similar
+ # method to the ForeignKey field for accessing the generic related fields.
+ generic_fields = []
+ for model in get_models():
+ for field_name, field in filter(lambda x: isinstance(x[1],
+ GenericForeignKey),
+ model.__dict__.iteritems()):
+ generic_fields.append(field)
+
+ blank_local_fields = set()
+ for field in primary_object._meta.local_fields:
+ value = getattr(primary_object, field.attname)
+ # string fields with only spaces are empty fields
+ if isinstance(value, unicode) or isinstance(value, str):
+ value = value.strip()
+ if value in [None, '']:
+ blank_local_fields.add(field.attname)
+
+ # Loop through all alias objects and migrate their data to the primary object.
+ for alias_object in alias_objects:
+ # Migrate all foreign key references from alias object to primary object.
+ for related_object in alias_object._meta.get_all_related_objects():
+ # The variable name on the alias_object model.
+ alias_varname = related_object.get_accessor_name()
+ # The variable name on the related model.
+ obj_varname = related_object.field.name
+ related_objects = getattr(alias_object, alias_varname)
+ for obj in related_objects.all():
+ setattr(obj, obj_varname, primary_object)
+ obj.save()
+
+ # Migrate all many to many references from alias object to primary object.
+ related_many_objects = \
+ alias_object._meta.get_all_related_many_to_many_objects()
+ related_many_object_names = set()
+ for related_many_object in related_many_objects:
+ alias_varname = related_many_object.get_accessor_name()
+ obj_varname = related_many_object.field.name
+ if alias_varname in MERGE_FIELDS or obj_varname in MERGE_FIELDS:
+ continue
+
+ if alias_varname is not None:
+ # standard case
+ related_many_objects = getattr(alias_object, alias_varname).all()
+ related_many_object_names.add(alias_varname)
+ else:
+ # special case, symmetrical relation, no reverse accessor
+ related_many_objects = getattr(alias_object, obj_varname).all()
+ related_many_object_names.add(obj_varname)
+ for obj in related_many_objects.all():
+ getattr(obj, obj_varname).remove(alias_object)
+ getattr(obj, obj_varname).add(primary_object)
+
+ # Migrate local many to many references from alias object to primary object.
+ for many_to_many_object in alias_object._meta.many_to_many:
+ alias_varname = many_to_many_object.get_attname()
+ if alias_varname in related_many_object_names or \
+ alias_varname in MERGE_FIELDS:
+ continue
+
+ many_to_many_objects = getattr(alias_object, alias_varname).all()
+ if alias_varname in blank_local_fields:
+ blank_local_fields.pop(alias_varname)
+ for obj in many_to_many_objects.all():
+ getattr(alias_object, alias_varname).remove(obj)
+ getattr(primary_object, alias_varname).add(obj)
+
+ # Migrate all generic foreign key references from alias object to
+ # primary object.
+ for field in generic_fields:
+ filter_kwargs = {}
+ filter_kwargs[field.fk_field] = alias_object._get_pk_val()
+ filter_kwargs[field.ct_field] = field.get_content_type(alias_object)
+ for generic_related_object in field.model.objects.filter(
+ **filter_kwargs):
+ setattr(generic_related_object, field.name, primary_object)
+ generic_related_object.save()
+
+ # Try to fill all missing values in primary object by values of duplicates
+ filled_up = set()
+ for field_name in blank_local_fields:
+ val = getattr(alias_object, field_name)
+ if val not in [None, '']:
+ setattr(primary_object, field_name, val)
+ filled_up.add(field_name)
+ blank_local_fields -= filled_up
+
+ if not keep_old:
+ alias_object.delete()
+ primary_object.save()
+ return primary_object