I know there has already been some discussion about adding signals to
the ManyRelatedManager so that add(), remove() and clear() on m2m
relations emit signals. This functionality, [ORM-18] in the v1.1
roadmap, would complement the behavior of pre/post_save and pre/
post_delete very nicely and provide a easy and transparent way to
track changes to m2m relationships.
Since I really like to have this functionality for a project I am
working on, I'm trying to push this feature for v1.1alpha. However,
some design decisions need to be made, which is why I am starting this
discussion.
The ticket for this functionality is
http://code.djangoproject.com/ticket/5390
which has a patch that works to some extent. It adds three signals:
m2m_add_items, m2m_remove_items and m2m_clear_items. Extra arguments
to the signal are the instance on which the field is accessed, the
field_name of the m2m field and the list of added/removed objects.
The main problem with the patch is the unintuitive sender of signals.
The signal is sent by the _related_ model, not the model containing
the ManyToManyField. For example:
class Car(models.Model)
parts = models.ManyToManyField('Part')
class Part(models.Model)
pass
Now if we want to handle the signal sent by some_car.parts.add
(some_part), we would have to connect the handler to Part:
signals.m2m_add_items(add_handler_func, Part)
To me, this is not very intuitive. I would like to connect to Car,
since that model has the 'parts' field, so this is something I'm
suggesting to change in the patch (not that hard to do).
Another issue that has come up while playing around with these
signals, is that only one end of the relation is handled by one
signal. So if you want to handle the reverse relation of 'parts',
some_part.car_set.add(some_car), you will have to connect to the Car
(or Part if the previously suggested change is made). But since this
is actually a change on the same relationship, in this case between
Cars and Parts, it might make sense to have these changes on the
reverse side _also_ be sent by the same sender (Car, ideally), with an
extra parameter 'reverse' that is True for changes to the reverse
side, indicating that not Parts are added to a Car, but Cars are added
to a Part. If you only want to handle one side, you can simply ignore
the case for reverse=True or False. I'm not sure if this is a good
solution, so any thoughts are welcome.
An issue with the current patch regarding the two sides of the
relation and the signals they sent, is that _both_ sides send the same
'field_name' argument to the signal. I would say that if you have two
signals, one for each side, then each side should send the appropriate
field_name: so 'parts' for one side and 'car_set' or a specific
related_name for the other side.
A final problem with the current implementation and one to which I do
not have an answer, is an inconsistency in the signal definitions and
emitting moments. Both the add_m2m_items and remove_m2m_items are
emitted after the changes have been applied (cursor.execute) and
include the list of added/removed objects in the 'objs' argument to
the signal. The clear_m2m_items however, is emitted _before_ the
cursor.execute is called, and contains no 'objs' argument. So with add
() and remove() the database is already in the changed state, while
with clear() the database is still in the state before clear. The
problem however is that if you emit the clear signal after the change,
there is no way of telling which objects have been removed from the
relationship without making an extra call to get these objects prior
to clearing the relation. The question is, is this extra call worth
it, when the result is only ever used by a signal handler. Or should
there be a pre/post variant for this signal that so handlers can save
this list on pre, and handle the changes after the database is up-to-
date?
I am willing to write a better patch, tests and documentation for this
feature, but before putting in a lot of time and effort, I would
appreciate some feedback. So, ideas anyone? These are the main issues:
* sender of signal, currently related model, should be model (so
some_car.parts.add() will be sent by Car). Ok?
* changes to reverse side of relation sent by same sender (both Car
in the example) with extra reverse=True/False?
* Should the field_name be the 'related_name'/<model_name>_set for
reverse side?
* difference between add/remove and clear signals
-Robin