""" Handles the merging of two or more model instances. Adapted from the django-extensions package: https://github.com/django-extensions/django-extensions """ from django.apps import apps from django.contrib.contenttypes.fields import GenericForeignKey from django.db import transaction from uniauth.utils import get_setting def _get_generic_fields(): """ Return a list of all GenericForeignKeys in all models. """ generic_fields = [] for model in apps.get_models(): for field_name, field in model.__dict__.items(): if isinstance(field, GenericForeignKey): generic_fields.append(field) return generic_fields @transaction.atomic() def merge_model_instances(primary_object, alias_objects, field_trace=[]): """ Merge several model instances into one, the `primary_object`. Use this function to merge model objects and migrate all of the related fields from the alias objects into the primary object. Performs recursive merging of related One-to-One fields. """ generic_fields = _get_generic_fields() # get related fields related_fields = list(filter( lambda x: x.is_relation is True, primary_object._meta.get_fields())) many_to_many_fields = list(filter( lambda x: x.many_to_many is True, related_fields)) related_fields = list(filter( lambda x: x.many_to_many is False, related_fields)) # Loop through all alias objects and migrate their references to the # primary object deleted_objects = [] deleted_objects_count = 0 for alias_object in alias_objects: # Migrate all foreign key references from alias object to primary # object. for many_to_many_field in many_to_many_fields: alias_varname = many_to_many_field.name related_objects = getattr(alias_object, alias_varname, None) if related_objects is None: continue for obj in related_objects.all(): try: # Handle regular M2M relationships. getattr(alias_object, alias_varname).remove(obj) getattr(primary_object, alias_varname).add(obj) except AttributeError: # Handle M2M relationships with a 'through' model. # This does not delete the 'through model. through_model = getattr(alias_object, alias_varname).through kwargs = { many_to_many_field.m2m_reverse_field_name(): obj, many_to_many_field.m2m_field_name(): alias_object, } through_model_instances = through_model.objects.filter(**kwargs) for instance in through_model_instances: # Re-attach the through model to the primary_object setattr( instance, many_to_many_field.m2m_field_name(), primary_object) instance.save() for related_field in related_fields: if related_field.one_to_many: alias_varname = related_field.get_accessor_name() related_objects = getattr(alias_object, alias_varname, None) if related_objects is None: continue for obj in related_objects.all(): field_name = related_field.field.name setattr(obj, field_name, primary_object) obj.save() elif related_field.one_to_one or related_field.many_to_one: alias_varname = related_field.name related_object = getattr(alias_object, alias_varname, None) primary_related_object = getattr(primary_object, alias_varname, None) if related_object is None: continue elif primary_related_object is None: setattr(primary_object, alias_varname, related_object) primary_object.save() elif related_field.one_to_one: # Perform recursive merging for one-to-one fields if get_setting("UNIAUTH_PERFORM_RECURSIVE_MERGING"): if related_field in field_trace: continue updated_trace = field_trace + [related_field, related_field.remote_field] merge_model_instances(primary_related_object, [related_object], updated_trace) else: related_object.delete() 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) related_objects = field.model.objects.filter(**filter_kwargs) for generic_related_object in related_objects: setattr(generic_related_object, field.name, primary_object) generic_related_object.save() if alias_object.id: deleted_objects += [alias_object] alias_object.delete() deleted_objects_count += 1 return primary_object, deleted_objects, deleted_objects_count