Given the (simplified) model, used within a Django Admin app:
models.py
class Appointment(models.Model):
customer = models.ForeignKey(
Customer, blank=False, null=True, on_delete=models.CASCADE
)
pianos = models.ManyToManyField(Piano, blank=True)
def save(self, *args, **kwargs):
# Is it a new Appointment, not an existing one being re-saved?
newAppointment = self.id is None
try:
super().save(*args, **kwargs) # Call the "real" save() method.
if newAppointment:
# Returns empty queryset
for piano in self.pianos.all():
print(piano)
except:
pass
I want to access 'pianos'. If it's a newly created Appointment,
self.pianos
returns
<django.db.models.fields.related_descriptors.create_forward_many_to_many_manager..ManyRelatedManager object at 0x7f6f875209a0>
and self.pianos.all()
returns an empty queryset, even though the pianos are displayed in the template form that was submitted to initiate the save.
However, if it's an updating of an existing Appointment
, 'pianos' returns the data as expected.
Apparently that ManyToMany
field doesn't get saved to the db immediately when save()
is called. So how can I access the data as shown below? Note that 'pianos' are not instantiated here, they already exist in the database, and only need Appointment to point to one or more of them in its m2m field, as directed from a horizontal_filter
defined in admin.py
.
I also tried the alternative method of using a post_save
signal, with exactly the same result:
@receiver(signals.post_save, sender=Appointment)
def createServiceHistory(sender, instance, created, **kwargs):
if created:
for piano in instance.pianos.all(): #empty queryset
print(piano)
Update: modified to catch m2m_changed
instead of post_save:
@receiver(signals.m2m_changed, sender=Appointment)
def createServiceHistory(sender, instance, action, **kwargs):
print(action)
But this signal isn't received.
The docs say that ManyToMany
fields are saved using add()
rather than save()
, but I don't see how that would be applied in this case.
A ManyToMany
association can not be created until both instances are saved, that's why it is not yet created while you are in the Appointment.save() method
.
The Form.save()
method (or the Save
button, if you are using the Django Admin Interface) saves the Appointment
, then associates it with the Piano
instance(s) using .add()
. In this case, you can use the m2m_changed
signal as @IainShelvington suggested:
Sent when a
ManyToManyField
is changed on a model instance. Strictly speaking, this is not a model signal since it is sent by theManyToManyField
, but since it complements thepre_save
/post_save
andpre_delete
/post_delete
when it comes to tracking changes to models, it is included here.
Note that the sender
for this signal won't be Appointment
anymore. You should use Appointment.pianos.through
as mentioned in the docs:
Arguments sent with this signal:
sender
The intermediate model class describing the
ManyToManyField
. This class is automatically created when a many-to-many field is defined; you can access it using thethrough
attribute on the many-to-many field.
A simplified example would be:
@receiver(signals.m2m_changed, sender=Appointment.pianos.through)
def associateAppointmentWithPiano(sender, instance, action, **kwargs):
print(f"{sender=} {instance=} {action=})
Additional comments from the OP:
For the benefit of later readers, the action will be
pre_add
first, then a second signal arrives with action beingpost_add
. Only at that point is the data in this example available via:Appointment.pianos.all()
.