2010-08-03 4 views
5

ho tale modello:Django: fusione oggetti

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 

Dato che io li sto importazione da molte fonti, e gli utenti del mio sito sono in grado di aggiungere nuovi luoghi, ho bisogno di un modo per unirle da un interfaccia di amministrazione. Il problema è che il nome non è molto affidabile in quanto possono essere scritte in molti modi diversi, ecc io sono abituato a usare qualcosa di simile:

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) 

query come questa

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

e unire come questo

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" 

come si può vedere, devo aggiornare tutti gli altri modelli con FK per posizionare con nuovi valori. Ma non è una soluzione molto buona dato che devo aggiungere ogni nuovo modello a questa lista.

Come faccio a "sovrapporre in cascata" tutte le chiavi esterne ad alcuni oggetti prima di eliminarli?

O forse ci sono altre soluzioni per fare/evitare la fusione

risposta

6

Se qualcuno tirammo fuori, qui è davvero codice generico per questo:

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 Ho trovato questo esempio più completo: http://djangosnippets.org/snippets/2283/ – dpn

+1

Snippet non sembra funzionare per me più, non riesce in ForeignKey. La transazione Plus è ammortizzata in favore di atomico. Inoltre iteritems() diventa items() in python3. (gli ultimi due numeri erano facili da risolvere, il primo no). – gabn88

+0

Nel risolvere il primo numero ho scoperto che il problema è probabile con il groupobjectpermissions di django guardian. Non è stato possibile risolverlo però :( – gabn88

2

Sulla base del frammento fornite nei commenti in risposta accettata , Sono stato in grado di sviluppare il seguente. Questo codice non gestisce GenericForeignKeys. Non attribuisco il loro uso poiché credo che indichi un problema con il modello che stai utilizzando.

Questo codice gestisce i vincoli unique_together che impedivano il completamento delle transazioni atomiche con altri snippet che ho trovato. Certo, è un po 'hacker nella sua implementazione. Uso anche django-audit-log e non voglio unire quei record con la modifica. Voglio anche modificare i campi creati e modificati in modo appropriato. Questo codice funziona con Django 1.10 e il più recente modello _meta API.

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

Testato su Django 1.10. Spero che possa servire.

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