Джанго: слияние объектов - PullRequest
6 голосов
/ 03 августа 2010

У меня есть такая модель:

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 для размещения с новыми значениями. Но это не очень хорошее решение, так как мне приходится добавлять каждую новую модель в этот список.

Как «каскадно обновить» все внешние ключи для некоторых объектов перед их удалением?

Или, может быть, есть другие решения, которые можно сделать / избежать слияния

Ответы [ 5 ]

6 голосов
/ 06 августа 2010

Если кто-то заинтересовался, вот действительно общий код для этого:

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)
3 голосов
/ 22 декабря 2016

На основе фрагмента, приведенного в комментариях к принятому ответу, я смог разработать следующее.Этот код не обрабатывает GenericForeignKeys.Я не приписываю их использованию, поскольку полагаю, что это указывает на проблему с используемой моделью.

В этом ответе я использовал список большого количества кода, но я обновил свой код для использованияДжанго-супер-дедупер упомянул здесь .В то время django-super-deduper не очень хорошо справлялся с неуправляемыми моделями.Я отправил вопрос, и похоже, что он скоро будет исправлен.Я также использую django-audit-log и не хочу объединять эти записи.Я сохранил подпись и @transaction.atomic() декоратор.Это полезно в случае возникновения проблемы.

from django.db import transaction
from django.db.models import Model, Field
from django_super_deduper.merge import MergedModelInstance


class MyMergedModelInstance(MergedModelInstance):
    """
        Custom way to handle Issue #11: Ignore models with managed = False
        Also, ignore auditlog models.
    """
    def _handle_o2m_related_field(self, related_field: Field, alias_object: Model):
        if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
            return super()._handle_o2m_related_field(related_field, alias_object)

    def _handle_m2m_related_field(self, related_field: Field, alias_object: Model):
        if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
            return super()._handle_m2m_related_field(related_field, alias_object)

    def _handle_o2o_related_field(self, related_field: Field, alias_object: Model):
        if not alias_object._meta.managed and "auditlog" not in alias_object._meta.model_name:
            return super()._handle_o2o_related_field(related_field, alias_object)


@transaction.atomic()
def merge(primary_object, alias_objects):
    if not isinstance(alias_objects, list):
        alias_objects = [alias_objects]
    MyMergedModelInstance.create(primary_object, alias_objects)
    return primary_object
2 голосов
/ 16 декабря 2017

В настоящее время существуют две библиотеки с современными функциями слияния моделей, которые включают связанные модели:

Расширения Django merge_model_instances команда управления.

Джанго Супер Дедупер

1 голос
/ 09 июня 2018

Я искал решение для объединения записей в Django Admin и нашел пакет, который делает это (https://github.com/saxix/django-adminactions).

Как использовать:

Установить пакет: pip install django-adminactions

Добавить администрацию к вашему INSTALLED_APPS:

INSTALLED_APPS = (
    'adminactions',
    'django.contrib.admin',
    'django.contrib.messages',
)

Добавить действия к admin.py:

from django.contrib.admin import site
import adminactions.actions as actions

actions.add_to_site(site)

Добавить URL службы к вашим URL.py: url(r'^adminactions/', include('adminactions.urls')),

Попробовал только сейчас, у меня работает.

1 голос
/ 07 ноября 2017

Проверено на Джанго 1.10.Надеюсь, что это может служить.

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
...