2010-08-03 14 views
5

que tengo de estos modelos:Django: objetos que se fusionan

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 

Dado que los estoy importando de muchas fuentes, y los usuarios de mi sitio web son capaces de añadir nuevos sitios, que necesitan una manera de unirlos de una interfaz de administrador. El problema es que el nombre no es muy fiable, ya que pueden ser escritos 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 combinar como esto

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 puedes ver, tengo que actualizar todos los otros modelos con FK para colocar con nuevos valores. Pero no es una solución muy buena ya que tengo que agregar cada nuevo modelo a esta lista.

¿Cómo puedo "actualizar en cascada" todas las claves externas a algunos objetos antes de eliminarlas?

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

Respuesta

6

Si alguien intersted, aquí es realmente código 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) 
+6

Fwiw me encontré con este ejemplo más completo: http://djangosnippets.org/snippets/2283/ – dpn

+1

no parece fragmentos que trabajar más para mí, falla en ForeignKey. Además, la transacción se deprecia a favor de atómica. Además iteritems() se convirtieron en items() en python3. (los dos últimos números fueron fáciles de resolver, el primero no). – gabn88

+0

Al resolver el primer problema, descubrí que el problema es probable con el groupobjectpermissions de django guardian. Sin embargo, no se pudo resolver :( – gabn88

2

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

Este código maneja las restricciones unique_together que impidieron que las transacciones atómicas se completen con otros fragmentos que encontré. Es cierto que es un poco hackish en su implementación. También uso django-audit-log, y no quiero fusionar esos registros con el cambio. También quiero modificar los campos creados y modificados de forma adecuada. Este código funciona con Django 1.10 y la API más reciente de Model _meta.

from django.db import transaction 
from django.utils import timezone 
from django.db.models import Model 

def flatten(l, a=None): 
    """Flattens a list.""" 
    if a is None: 
     a = [] 
    for i in l: 
     if isinstance(i, Iterable) and type(i) != str: 
      flatten(i, a) 
     else: 
      a.append(i) 
    return a 


@transaction.atomic() 
def merge(primary_object, alias_objects=list()): 
    """ 
    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. This does not look at GenericForeignKeys. 

    Usage: 
    from django.contrib.auth.models import User 
    primary_user = User.objects.get(email='[email protected]') 
    duplicate_user = User.objects.get(email='[email protected]') 
    merge_model_objects(primary_user, duplicate_user) 
    """ 
    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') 

    for alias_object in alias_objects: 
     if alias_object != primary_object: 
      for attr_name in dir(alias_object): 
       if 'auditlog' not in attr_name: 
        attr = getattr(alias_object, attr_name, None) 
        if attr and "RelatedManager" in type(attr).__name__: 
         if attr.exists(): 
          if type(attr).__name__ == "ManyRelatedManager": 
           for instance in attr.all(): 
            getattr(alias_object, attr_name).remove(instance) 
            getattr(primary_object, attr_name).add(instance) 
          else: 
           # do an update on the related model 
           # we have to stop ourselves from violating unique_together 
           field = attr.field.name 
           model = attr.model 
           unique = [f for f in flatten(model._meta.unique_together) if f != field] 
           updater = model.objects.filter(**{field: alias_object}) 
           if len(unique) == 1: 
            to_exclude = { 
             "%s__in" % unique[0]: model.objects.filter(
              **{field: primary_object} 
             ).values_list(unique[0], flat=True) 
            } 
           # Concat requires at least 2 arguments 
           elif len(unique) > 1: 
            casted = {"%s_casted" % f: Cast(f, TextField()) for f in unique} 
            to_exclude = { 
             'checksum__in': model.objects.filter(
              **{field: primary_object} 
             ).annotate(**casted).annotate(
              checksum=Concat(*casted.keys(), output_field=TextField()) 
             ).values_list('checksum', flat=True) 
            } 
            updater = updater.annotate(**casted).annotate(
             checksum=Concat(*casted.keys(), output_field=TextField()) 
            ) 
           else: 
            to_exclude = {} 

           # perform the update 
           updater.exclude(**to_exclude).update(**{field: primary_object}) 

           # delete the records that would have been duplicated 
           model.objects.filter(**{field: alias_object}).delete() 

      if hasattr(primary_object, "created"): 
       if alias_object.created and primary_object.created: 
        primary_object.created = min(alias_object.created, primary_object.created) 
       if primary_object.created: 
        if primary_object.created == alias_object.created: 
         primary_object.created_by = alias_object.created_by 
       primary_object.modified = timezone.now() 

      alias_object.delete() 

    primary_object.save() 
    return primary_object 
0

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 
Cuestiones relacionadas