Django - How to save m2m data via post_save signal?

妖精的绣舞 提交于 2020-01-19 05:13:25

问题


(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!


回答1:


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.




回答2:


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)



回答3:


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)


来源:https://stackoverflow.com/questions/4432385/django-how-to-save-m2m-data-via-post-save-signal

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!