2010-08-03 9 views
5

tôi có mô hình như vậy:Django: sáp nhập đối tượng

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 

Kể từ khi tôi đang nhập khẩu chúng từ nhiều nguồn khác nhau, và người sử dụng trang web của tôi có thể thêm địa danh mới, tôi cần một cách để hợp nhất chúng lại từ một giao diện quản trị. Vấn đề là, tên không phải là rất đáng tin cậy vì chúng có thể được viết theo nhiều cách khác nhau, vv Tôi đang sử dụng để sử dụng một cái gì đó như thế này:

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) 

truy vấn như thế này

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

và hợp nhất như này

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" 

như bạn có thể thấy, tôi phải cập nhật tất cả các mô hình khác bằng FK để Đặt bằng giá trị mới. Nhưng nó không phải là giải pháp rất tốt vì tôi phải thêm mọi mô hình mới vào danh sách này.

Làm cách nào để "cập nhật xếp chồng" tất cả các khóa ngoài cho một số đối tượng trước khi xóa chúng?

Hoặc có thể có những giải pháp khác để làm/tránh hợp nhất

Trả lời

6

Nếu ai marry, đây thực sự là mã chung cho việc này:

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 tôi thấy ví dụ này toàn diện hơn: http://djangosnippets.org/snippets/2283/ – dpn

+1

Snippet dường như không làm việc cho tôi nữa, không thành công trên ForeignKey. Giao dịch cộng thêm được khấu hao để ủng hộ nguyên tử. Plus iteritems() trở thành các mục() trong python3. (hai vấn đề cuối cùng dễ giải quyết, trước hết là không). – gabn88

+0

Trong việc giải quyết vấn đề đầu tiên tôi phát hiện ra rằng vấn đề có khả năng với groupobjectpermissions của người giám hộ django. Không thể giải quyết nó mặc dù: ( – gabn88

2

Dựa trên đoạn cung cấp trong các chú thích trong câu trả lời được chấp nhận , Tôi đã có thể phát triển những điều sau đây. Mã này không xử lý GenericForeignKeys. Tôi không gán cho sử dụng của họ như tôi tin rằng nó chỉ ra một vấn đề với mô hình bạn đang sử dụng.

Mã này xử lý các ràng buộc unique_together ngăn các giao dịch nguyên tử hoàn tất với các đoạn mã khác mà tôi đã tìm thấy. Phải thừa nhận rằng, nó là một chút hackish trong việc thực hiện nó. Tôi cũng sử dụng django-audit-log, và tôi không muốn hợp nhất các bản ghi đó với sự thay đổi. Tôi cũng muốn sửa đổi các trường được tạo và sửa đổi một cách thích hợp. Mã này không hoạt động với Django 1.10 và API mô hình _meta mới hơn.

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

Đã thử nghiệm trên Django 1.10. Hy vọng nó có thể phục vụ.

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