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 для размещения с новыми значениями. Но это не очень хорошее решение, так как я должен добавить модель в этот список.
как "каскадное обновление" всех внешних ключей к некоторым объектам до их удаления?
или может есть другие решения, чтобы сделать/избежать слияния
5 ответов
Если кто-то вмешался, вот действительно общий код для этого:
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)
основываясь на фрагменте, представленном в комментариях в принятом ответе, я смог разработать следующее. Этот код не обрабатывает GenericForeignKeys. Я не приписываю их использованию, поскольку считаю, что это указывает на проблему с используемой вами моделью.
этот код обрабатывает unique_together
ограничения, которые мешали атомарным транзакциям завершаться с другими фрагментами, которые я нашел. По общему признанию, он немного избит в своей реализации. Я также использую django-audit-log, и я не хочу объединять эти записи с изменениями. Я также хочу соответствующим образом изменить созданные и измененные поля. Этот код работает с Django 1.10 и более новой моделью _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='good_email@example.com')
duplicate_user = User.objects.get(email='good_email+duplicate@example.com')
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
протестировано на Django 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
теперь существуют две библиотеки с современными функциями слияния моделей, которые включают связанные модели:
Расширения Django'merge_model_instances команды управления.
Я искал решение для объединения записей в Django Admin и нашел пакет, который это делает (https://github.com/saxix/django-adminactions).
как использовать:
пакет установки :
pip install django-adminactions
добавить 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-адрес службы в urls.py:url(r'^adminactions/', include('adminactions.urls')),
пробовал это только сейчас, он работает для мне.