2010-08-03 5 views
5

J'ai ce modèle:Django: objets fusion

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 

Depuis que je suis les importer de nombreuses sources, et les utilisateurs de mon site sont en mesure d'ajouter de nouveaux endroits, je besoin d'un moyen de les fusionner à partir d'un interface d'administration. Le problème est, le nom n'est pas très fiable car ils peuvent être orthographié de plusieurs façons différentes, etc Je suis habitué à utiliser quelque chose comme ceci:

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) 

requête comme ceci

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

et de fusion comme comme vous pouvez le voir, je dois mettre à jour tous les autres modèles avec FK à placer avec de nouvelles valeurs. Mais ce n'est pas une très bonne solution puisque je dois ajouter chaque nouveau modèle à cette liste. Comment puis-je "mettre à jour en cascade" toutes les clés étrangères de certains objets avant de les supprimer?

Ou peut-être il y a d'autres solutions pour faire/éviter la fusion

Répondre

6

Si quelqu'un intersted, ici est vraiment code générique pour cela:

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 je trouve cet exemple plus complet: http://djangosnippets.org/snippets/2283/ – dpn

+1

Snippet ne semble pas fonctionner pour moi plus, échoue sur ForeignKey. La transaction Plus est dépréciée en faveur de l'atomique. De plus iteritems() est devenu items() dans python3. (Les deux derniers numéros étaient faciles à résoudre, le premier pas). – gabn88

+0

En résolvant le premier problème, j'ai découvert que le problème est probablement lié à la propriété groupobject du django guardian. Impossible de le résoudre si :( – gabn88

2

Sur la base de l'extrait fourni dans les commentaires dans la réponse acceptée , J'ai été capable de développer ce qui suit. Ce code ne gère pas GenericForeignKeys. Je n'attribue pas à leur utilisation car je crois que cela indique un problème avec le modèle que vous utilisez.

Ce code gère les contraintes unique_together qui empêchaient les transactions atomiques de se terminer avec d'autres extraits trouvés. Certes, c'est un peu hackish dans sa mise en œuvre. J'utilise aussi django-audit-log, et je ne veux pas fusionner ces enregistrements avec le changement. Je veux également modifier les champs créés et modifiés de manière appropriée. Ce code fonctionne avec Django 1.10 et l'API Model _meta plus récente.

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

Testé sur Django 1.10. J'espère que ça peut 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