2010-08-03 14 views
5

Ich habe solches Modell:Django: Zusammenführen von Objekten

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 

Da ich sie aus vielen Quellen bin importieren, und die Nutzer meiner Website sind in der Lage, neue Orte hinzufügen, ich brauche einen Weg, um sie von einer verschmelzen Admin-Schnittstelle. Das Problem ist, Name nicht sehr zuverlässig ist, da sie auf viele verschiedene Weisen geschrieben werden können, etc Ich bin es gewohnt, so etwas zu verwenden:

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) 

Abfrage wie folgt

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

und verschmelzen wie Diese

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" 

wie Sie sehen können, muss ich alle anderen Modelle mit FK aktualisieren, um mit neuen Werten zu platzieren. Aber es ist keine sehr gute Lösung, da ich jedes neue Modell zu dieser Liste hinzufügen muss.

Wie kaskadiere ich alle Fremdschlüssel für einige Objekte, bevor sie gelöscht werden?

Oder vielleicht gibt es andere Lösungen zu tun/vermeiden

verschmelzenden

Antwort

6

Wenn jemand intersted, hier ist wirklich generischer Code dafür:

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 ich dieses Beispiel umfassender gefunden: http://djangosnippets.org/snippets/2283/ – dpn

+1

Snippet nicht für mich scheint mehr zu arbeiten, nicht auf ForeignKey. Die Plus-Transaktion wird zugunsten der Atomkraft abgeschrieben. Plus iteritems() wurde Artikel() in python3. (die letzten beiden Ausgaben waren leicht zu lösen, die erste nicht). – gabn88

+0

in der ersten Ausgabe der Lösung fand ich heraus, dass das Problem mit den groupobjectpermissions von django Wächter wahrscheinlich ist. Könnte es allerdings nicht lösen :( – gabn88

2

Basierend auf dem Snippet in den Kommentaren in der akzeptierten Antwort zur Verfügung gestellt Ich konnte folgendes entwickeln. Dieser Code behandelt nicht GenericForeignKeys. Ich schreibe ihre Verwendung nicht zu, da ich glaube, dass es auf ein Problem mit dem Modell hinweist, das Sie verwenden.

Dieser Code behandelt unique_together Zwänge, die die atomaren Transaktionen von dem Abschließen mit anderen Schnipseln verhinderten ich gefunden hat. Zugegeben, es ist ein wenig hackisch in seiner Umsetzung. Ich benutze auch django-audit-log, und ich möchte diese Datensätze nicht mit der Änderung zusammenführen. Ich möchte auch die erstellten und geänderten Felder entsprechend modifizieren. Dieser Code funktioniert mit Django 1.10 und dem neueren Modell _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

Getestet auf Django 1.10. Hoffe es kann dienen.

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 
Verwandte Themen