Django - 如何通过 post_save 信号保存 m2m 数据?

发布于 2024-10-07 09:32:47 字数 1678 浏览 3 评论 0原文

(Django 1.1)我有一个项目模型,它使用 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_repsales_mgrproject_mgrUser添加到成员中,以便更轻松地跟踪项目权限。到目前为止,这种方法非常有效。

我现在处理的问题是当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 信号无法正常工作(成员保持不变)。

我认为彼得·罗威尔在这种情况下的诊断是正确的。如果我从管理表单中删除“成员”字段,则 post_save 信号可以正常工作。当包含该字段时,它会根据保存时表单中存在的值保存旧成员。无论我在保存项目时对成员 m2m 字段进行什么更改(无论是信号还是自定义保存方法),它始终会被保存之前表单中存在的成员覆盖。感谢您指出这一点!

(Django 1.1) I have a Project model that keeps track of its members using a m2m field. It looks like this:

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

When the project is created, the selected sales_rep, sales_mgr, project_mgr, etc Users are added to members to make it easier to keep track of project permissions. This approach has worked very well so far.

The issue I am dealing with now is how to update the project's membership when one of the User FK fields is updated via the admin. I've tried various solutions to this problem, but the cleanest approach seemed to be a post_save signal like the following:

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)  

However, the Project still has the same members even if I change one of the fields via the admin! I have had success updating members m2m fields using my own views in other projects, but I never had to make it play nice with the admin as well.

Is there another approach I should take other than a post_save signal to update membership? Thanks in advance for your help!

UPDATE:

Just to clarify, the post_save signal works correctly when I save my own form in the front end (old members are removed, and new ones added). However, the post_save signal does NOT work correctly when I save the project via the admin (members stay the same).

I think Peter Rowell's diagnosis is correct in this situation. If I remove the "members" field from the admin form the post_save signal works correctly. When the field is included, it saves the old members based on the values present in the form at the time of the save. No matter what changes I make to the members m2m field when project is saved (whether it be a signal or custom save method), it will always be overwritten by the members that were present in the form prior to the save. Thanks for pointing that out!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

黑寡妇 2024-10-14 09:32:47

遇到同样的问题,我的解决方案是使用 m2m_changed 信号。您可以在两个地方使用它,如以下示例所示。

保存后管理员将继续:

  • 保存模型字段,
  • 为每个 m2m发出 post_save 信号
    • 发出 pre_clear
    • 清除关系
    • 发出 post_clear
    • 发出 pre_add
    • 再次填充
    • 发出 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_removepost_removepre_clearpost_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 信号更新一个列表。

Having had the same problem, my solution is to use the m2m_changed signal. You can use it in two places, as in the following example.

The admin upon saving will proceed to:

  • save the model fields
  • emit the post_save signal
  • for each m2m:
    • emit pre_clear
    • clear the relation
    • emit post_clear
    • emit pre_add
    • populate again
    • emit post_add

Here you have a simple example that changes the content of the saved data before actually saving it.

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)

You can also listen to pre_remove, post_remove, pre_clear and post_clear. In my case I am using them to filter one list ('active things') within the contents of another ('enabled things') independent of the order in which lists are saved:

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

If the "enabled" ones are updated (cleared/removed + added back, like in admin) then the "active" ones are cached and cleared in the first pass ('pre_clear') and then added back from the cache after the second pass ('post_add').

The trick was to update one list on the m2m_changed signals of the other.

墨落画卷 2024-10-14 09:32:47

我看不出您的代码有任何问题,但我很困惑为什么您认为管理员的工作方式应该与任何其他应用程序不同。

但是,我必须说我认为你的模型结构是错误的。我认为您需要摆脱所有这些外键字段,而只拥有一个 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)

I can't see anything wrong with your code, but I'm confused as to why you think the admin should work any different from any other app.

However, I must say I think your model structure is wrong. I think you need to get rid of all those ForeignKey fields, and just have a ManyToMany - but use a through table to keep track of the roles.

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)
送君千里 2024-10-14 09:32:47

当我需要从一组项目中查找通过 m2m_field 连接到模型的最新项目时,我陷入了困境。

根据 Saverio 的回答,以下代码解决了我的问题:

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)

I've stuck on situation, when I needed to find latest item from set of items, that connected to model via m2m_field.

Following Saverio's answer, following code solved my issue:

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)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文