Django: fusionar objetos

Tengo tal modelo:

class Place(models.Model): name = models.CharField(max_length=80, db_index=True) city = models.ForeignKey(City) address = models.CharField(max_length=255, db_index=True) # and so on 

Como los estoy importando de muchas fonts, y los usuarios de mi sitio web pueden agregar nuevos sitios, necesito una forma de fusionarlos desde una interfaz de administración. El problema es que el nombre no es muy confiable ya que se pueden deletrear de muchas maneras diferentes, etc. Estoy acostumbrado a usar algo como esto:

 class Place(models.Model): name = models.CharField(max_length=80, db_index=True) # canonical city = models.ForeignKey(City) address = models.CharField(max_length=255, db_index=True) # and so on class PlaceName(models.Model): name = models.CharField(max_length=80, db_index=True) place = models.ForeignKey(Place) 

consulta como esta

 Place.objects.get(placename__name='St Paul\'s Cathedral', city=london) 

y fusionarse asi

 class PlaceAdmin(admin.ModelAdmin): actions = ('merge', ) def merge(self, request, queryset): main = queryset[0] tail = queryset[1:] PlaceName.objects.filter(place__in=tail).update(place=main) SomeModel1.objects.filter(place__in=tail).update(place=main) SomeModel2.objects.filter(place__in=tail).update(place=main) # ... etc ... for t in tail: t.delete() self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main) merge.short_description = "Merge places" 

Como puede ver, tengo que actualizar todos los otros modelos con FK para colocar con nuevos valores. Pero no es una buena solución ya que tengo que agregar cada nuevo modelo a esta lista.

¿Cómo “actualizo en cascada” todas las claves externas a algunos objetos antes de eliminarlos?

O tal vez hay otras soluciones para hacer / evitar la fusión

Si alguien se interpuso, aquí hay un código realmente genérico para esto:

 def merge(self, request, queryset): main = queryset[0] tail = queryset[1:] related = main._meta.get_all_related_objects() valnames = dict() for r in related: valnames.setdefault(r.model, []).append(r.field.name) for place in tail: for model, field_names in valnames.iteritems(): for field_name in field_names: model.objects.filter(**{field_name: place}).update(**{field_name: main}) place.delete() self.message_user(request, "%s is merged with other places, now you can give it a canonical name." % main) 

Basado en el fragmento proporcionado en los comentarios en la respuesta aceptada, pude desarrollar lo siguiente. Este código no maneja GenericForeignKeys. No atribuyo a su uso, ya que creo que indica un problema con el modelo que está utilizando.

Usé una lista de muchos códigos para hacer esto en esta respuesta, pero he actualizado mi código para usar el django-super-deduper mencionado aquí . En ese momento, django-super-deduper no manejaba los modelos no administrados de una buena manera. Presenté un problema y parece que se corregirá pronto. También uso django-audit-log, y no quiero combinar esos registros. Guardé la firma y el decorador @transaction.atomic() . Esto es útil en caso de un problema.

 from django.db import transaction from django.db.models import Model, Field from django_super_deduper.merge import MergedModelInstance class MyMergedModelInstance(MergedModelInstance): """ Custom way to handle Issue #11: Ignore models with managed = False Also, ignore auditlog models. """ def _handle_o2m_related_field(self, related_field: Field, alias_object: Model): if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name: return super()._handle_o2m_related_field(related_field, alias_object) def _handle_m2m_related_field(self, related_field: Field, alias_object: Model): if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name: return super()._handle_m2m_related_field(related_field, alias_object) def _handle_o2o_related_field(self, related_field: Field, alias_object: Model): if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name: return super()._handle_o2o_related_field(related_field, alias_object) @transaction.atomic() def merge(primary_object, alias_objects): if not isinstance(alias_objects, list): alias_objects = [alias_objects] MyMergedModelInstance.create(primary_object, alias_objects) return primary_object 

Ahora existen dos bibliotecas con funciones de fusión de modelos actualizadas que incorporan modelos relacionados:

El comando de administración merge_model_instances de Django Extensions.

Django Super Deduper

Probado en Django 1.10. Espero que pueda servir.

 def merge(primary_object, alias_objects, model): """Merge 2 or more objects from the same django model The alias objects will be deleted and all the references towards them will be replaced by references toward the primary object """ if not isinstance(alias_objects, list): alias_objects = [alias_objects] if not isinstance(primary_object, model): raise TypeError('Only %s instances can be merged' % model) for alias_object in alias_objects: if not isinstance(alias_object, model): raise TypeError('Only %s instances can be merged' % model) for alias_object in alias_objects: # Get all the related Models and the corresponding field_name related_models = [(o.related_model, o.field.name) for o in alias_object._meta.related_objects] for (related_model, field_name) in related_models: relType = related_model._meta.get_field(field_name).get_internal_type() if relType == "ForeignKey": qs = related_model.objects.filter(**{ field_name: alias_object }) for obj in qs: setattr(obj, field_name, primary_object) obj.save() elif relType == "ManyToManyField": qs = related_model.objects.filter(**{ field_name: alias_object }) for obj in qs: mtmRel = getattr(obj, field_name) mtmRel.remove(alias_object) mtmRel.add(primary_object) alias_object.delete() return True 

Estaba buscando una solución para fusionar registros en Django Admin, y encontré un paquete que lo está haciendo ( https://github.com/saxix/django-adminactions ).

Cómo utilizar:

Instalar paquete: pip install django-adminactions

Añade adminacciones a tus INSTALLED_APPS:

 INSTALLED_APPS = ( 'adminactions', 'django.contrib.admin', 'django.contrib.messages', ) 

Agrega acciones a admin.py :

 from django.contrib.admin import site import adminactions.actions as actions actions.add_to_site(site) 

Agregue el servicio url a su urls.py: url(r'^adminactions/', include('adminactions.urls')),

Lo intenté justo ahora, funciona para mí.