Django - Как сохранить данные m2m с помощью сигнала post_save? - PullRequest
12 голосов
/ 13 декабря 2010

(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 обновляется через администратора.Я пробовал различные решения этой проблемы, но самым чистым подходом был сигнал 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, чтобы обновить членство?Заранее благодарим за помощь!

ОБНОВЛЕНИЕ:

Просто чтобы уточнить, сигнал post_save работает правильно, когда я сохраняю свою форму в интерфейсе (старые участникиудалены и добавлены новые).Однако сигнал post_save НЕ работает правильно, когда я сохраняю проект через администратора (участники остаются прежними).

Я думаю, диагноз Питера Роуэлла в этой ситуации верный.Если я удаляю поле "members" из формы администратора, сигнал post_save работает правильно.Когда поле включено, оно сохраняет старые элементы на основе значений, представленных в форме во время сохранения.Независимо от того, какие изменения я внесу в поле membersmm m2m при сохранении проекта (будь то сигнал или пользовательский метод сохранения), оно всегда будет перезаписываться элементами, присутствовавшими в форме до сохранения.Спасибо за указание на это!

Ответы [ 3 ]

7 голосов
/ 31 декабря 2010

Имея ту же проблему, я решил использовать сигнал m2m_changed. Вы можете использовать его в двух местах, как в следующем примере.

Администратор при сохранении перейдет к:

  • сохранить поля модели
  • испустить сигнал post_save
  • за каждый м2:
    • emit pre_clear
    • очистить отношение
    • испускать 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)))

Если «включенные» обновляются (очищаются / удаляются + добавляются обратно, как в admin), то «активные» кэшируются и очищаются при первом проходе («pre_clear»), а затем добавляются обратно из кеша после второй проход (post_add).

Хитрость заключалась в обновлении одного списка по сигналам m2m_changed другого.

4 голосов
/ 13 декабря 2010

Я не вижу ничего плохого в вашем коде, но меня смущает, почему вы считаете, что администратор должен работать иначе, чем любое другое приложение.

Однако я должен сказать, что считаю вашу модельструктура неверна.Я думаю, вам нужно избавиться от всех этих полей 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)
0 голосов
/ 20 февраля 2012

Я застрял в ситуации, когда мне нужно было найти последний элемент из набора элементов, который подключен к модели через 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)
...