2010-08-03 55 views
5

我有这样的模式:Django的:合并对象

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 

由于我从许多来源导入他们,我的网站的用户能够添加新的地方,我需要一种方法来从他们合并管理界面。问题是,名字是不是很可靠的,因为他们可以在许多不同的方式拼写等 我已经习惯了用这样的:

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) 

这样的查询

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

和合并像这

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" 

正如你所看到的,我必须用FK更新所有其他模型以放置新的值。但这不是很好的解决方案,因为我必须将每个新模型添加到此列表中。

如何在删除某些对象之前“级联更新”所有外键?

或者,也许还有其他的解决方案做/避免合并

回答

6

如果有人位数的,这里真的是把这个通用代码:

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,我发现这个例子更全面:http://djangosnippets.org/snippets/2283/ – dpn 2013-04-24 05:44:31

+1

片段似乎并没有为我工作了,在失败的ForeignKey。 Plus交易贬值以支持原子。加iteritems()成为python3中的项目()。 (最后两个问题很容易解决,首先不是)。 – gabn88 2015-11-15 09:32:36

+0

在解决第一个问题时,我发现问题可能与django guardian的groupobjectpermissions有关。无法解决它:( – gabn88 2015-11-15 09:55:12

2

基于对接受的答案在评论中提供的片段,我能够开发以下内容。此代码不处理GenericForeignKeys。我不赞成使用它们,因为我认为这表示您使用的模型存在问题。

此代码处理unique_together限制,阻止原子事务与我发现的其他片段完成。无可否认,它的实施有点令人费解。我也使用django-audit-log,并且我不想将这些记录与更改合并。我也想适当修改创建和修改的字段。此代码可以与Django 1.10和更新的Model _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

在Django 1.10上测试。希望它可以服务。

Django的扩展merge_model_instances管理命令:

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