Django - Как сохранить данные m2m через сигнал post_save?
(Django 1.1) У меня есть модель Project, которая отслеживает ее членов, используя поле m2m. Это выглядит так:
class Project(models.Model):
members = models.ManyToManyField(User)
sales_rep = models.ForeignKey(User)
sales_mgr = models.ForeignKey(User)
project_mgr = models.ForeignKey(User)
... (more FK user fields) ...
Когда проект создается, к ним добавляются выбранные sales_rep
, sales_mgr
, project_mgr
и т.д. User
, чтобы упростить отслеживание разрешений проекта. Этот подход очень хорошо зарекомендовал себя.
Проблема, с которой я сейчас имею в виду, - это обновление членства в проекте, когда один из полей User
FK обновляется через admin. Я пробовал различные решения этой проблемы, но самый чистый подход представлял собой сигнал post_save
, как показано ниже:
def update_members(instance, created, **kwargs):
"""
Signal to update project members
"""
if not created: #Created projects are handled differently
instance.members.clear()
members_list = []
if instance.sales_rep:
members_list.append(instance.sales_rep)
if instance.sales_mgr:
members_list.append(instance.sales_mgr)
if instance.project_mgr:
members_list.append(instance.project_mgr)
for m in members_list:
instance.members.add(m)
signals.post_save.connect(update_members, sender=Project)
Однако, Project
все еще имеет одни и те же элементы, даже если я сменил одно из полей с помощью администратора! Я успешно обновлял поля m2m, используя свои собственные представления в других проектах, но мне никогда не приходилось так хорошо играть с администратором.
Есть ли другой подход, который я должен использовать вместо сообщения post_save для обновления членства? Заранее благодарим за помощь!
UPDATE:
Чтобы уточнить, сигнал post_save работает правильно, когда я сохраняю свою собственную форму в интерфейсе (старые элементы удаляются, а новые добавляются). Однако сигнал post_save НЕ работает правильно, когда я сохраняю проект через администратора (члены остаются неизменными).
Я думаю, что диагноз Питера Роуэлла правилен в этой ситуации. Если я удаляю поле "члены" из формы администратора, сигнал post_save работает правильно. Когда поле включено, оно сохраняет старые элементы на основе значений, присутствующих в форме во время сохранения. Независимо от того, какие изменения я внес в поле m2m членов при сохранении проекта (будь то сигнал или пользовательский метод сохранения), он всегда будет перезаписан членами, которые присутствовали в форме до сохранения. Спасибо, что указали это!
Ответы
Ответ 1
Имея ту же проблему, мое решение заключается в использовании сигнала m2m_changed. Вы можете использовать его в двух местах, как в следующем примере.
Администратор при сохранении перейдет к:
- сохранить поля модели
- испускать сигнал post_save
- для каждого m2m:
- emit pre_clear
- очистить отношение
- emit post_clear
- emit pre_add
- снова заполнить
- emit post_add
Здесь у вас есть простой пример, который изменяет содержимое сохраненных данных до его фактического сохранения.
class MyModel(models.Model):
m2mfield = ManyToManyField(OtherModel)
@staticmethod
def met(sender, instance, action, reverse, model, pk_set, **kwargs):
if action == 'pre_add':
# here you can modify things, for instance
pk_set.intersection_update([1,2,3])
# only save relations to objects 1, 2 and 3, ignoring the others
elif action == 'post_add':
print pk_set
# should contain at most 1, 2 and 3
m2m_changed.connect(receiver=MyModel.met, sender=MyModel.m2mfield.through)
Вы также можете слушать pre_remove
, post_remove
, pre_clear
и post_clear
. В моем случае я использую их для фильтрации одного списка ( "активные вещи" ) в содержимом другого ( "включенные вещи" ) независимо от порядка, в котором сохраняются списки:
def clean_services(sender, instance, action, reverse, model, pk_set, **kwargs):
""" Ensures that the active services are a subset of the enabled ones.
"""
if action == 'pre_add' and sender == Account.active_services.through:
# remove from the selection the disabled ones
pk_set.intersection_update(instance.enabled_services.values_list('id', flat=True))
elif action == 'pre_clear' and sender == Account.enabled_services.through:
# clear everything
instance._cache_active_services = list(instance.active_services.values_list('id', flat=True))
instance.active_services.clear()
elif action == 'post_add' and sender == Account.enabled_services.through:
_cache_active_services = getattr(instance, '_cache_active_services', None)
if _cache_active_services:
instance.active_services.add(*list(instance.enabled_services.filter(id__in=_cache_active_services)))
delattr(instance, '_cache_active_services')
elif action == 'pre_remove' and sender == Account.enabled_services.through:
# de-default any service we are disabling
instance.active_services.remove(*list(instance.active_services.filter(id__in=pk_set)))
Если обновленные "обновленные" (очищенные/удаленные + добавленные назад, как и у администратора), "активные" кэшируются и очищаются в первом проходе ( "pre_clear" ), а затем добавляются обратно из кеша после второй проход (post_add).
Хитрость заключалась в том, чтобы обновить один список на сигналах m2m_changed другого.
Ответ 2
Я не вижу ничего плохого в вашем коде, но я в замешательстве, почему вы думаете, что администратор должен работать иначе, чем любое другое приложение.
Однако, я должен сказать, что ваша структура модели неправильная. Я думаю, вам нужно избавиться от всех полей ForeignKey и просто иметь ManyToMany, но использовать сквозную таблицу для отслеживания ролей.
class Project(models.Model):
members = models.ManyToManyField(User, through='ProjectRole')
class ProjectRole(models.Model):
ROLES = (
('SR', 'Sales Rep'),
('SM', 'Sales Manager'),
('PM', 'Project Manager'),
)
project = models.ForeignKey(Project)
user = models.ForeignKey(User)
role = models.CharField(max_length=2, choices=ROLES)
Ответ 3
Я застрял в ситуации, когда мне нужно было найти последний элемент из набора элементов, связанный с моделью через m2m_field.
После ответа Саверио, следующий код решил мою проблему:
def update_item(sender, instance, action, **kwargs):
if action == 'post_add':
instance.related_field = instance.m2m_field.all().order_by('-datetime')[0]
instance.save()
m2m_changed.connect(update_item, sender=MyCoolModel.m2m_field.through)