Hi, I’m pretty new to Django so I’m probably being dumb but I’ve been looking at this for so long now I’ve gone snow-blind.
So, a small part of the app I’m trying to create handles the movement of backup tapes (a model called Tape) from one location to another. I have a Movement model with a many2many relationship with tapes, so I can move multiple tapes at once. I’ve created my views and they work as expected, so when I create a new movement and check a bunch of tapes, a movement record gets created and also an entry in the associated m2m table, so the data in the underlying db is good. However, as part of the save operation, I want to update the location field on each tape that was included in the movement and I just can’t see how to do that.
I tried writing my own save() method on the Movement object like so:
class Movement(models.Model):
movement_date = models.DateField()
tapes = models.ManyToManyField(Tape)
location = models.ForeignKey(StorageLocation, on_delete=models.RESTRICT)
comment = models.TextField(default='')
...stuff
def save(self, **kwargs):
super().save(**kwargs)
# Update the location on all moved tapes...
print(f"INSERTED ID: {self.id}")
print(f"TAPES: {self.tapes}")
...stuff
class Tape(models.Model):
label = models.CharField(max_length=30)
media_type = models.ForeignKey(MediaType, on_delete=models.RESTRICT)
location = models.ForeignKey(StorageLocation, on_delete=models.RESTRICT, default=-1)
date_moved = models.DateTimeField(auto_now_add=True, null=True)
As you can see, all I’m trying to do at the moment is access the data for the tapes that were moved, but what gets printed on the console is:
[19/Jun/2025 13:48:56] "GET /movement/add/ HTTP/1.1" 200 4967
INSERTED ID: 31
TAPES: Doo.Tape.None
[19/Jun/2025 13:49:09] "POST /movement/add/ HTTP/1.1" 302 0
The id of the new movement record is printed correctly but I’ve tried many variations of self.tapes
to get at the related tapes, to no avail.
What am I doing wrong?
Thanks very much!
As far as I remember, many-to-many (m2m) relationships are established after saving the object. Like this:
object.save()
{object.m2m.update process}
What you’re currently doing is checking before finishing object.save()
, so the relationships likely haven’t been established yet."
1 Like
Welcome @znac049 !
The ManyToManyField in a model is not a direct reference to the related objects - it’s actually a related object manager. (See https://docs.djangoproject.com/en/5.2/ref/models/relations/#related-objects-reference for more details.)
This means you do not access the Tapes
in the model as self.tapes
. You access them as a queryset via an expression like self.tapes.all()
.
1 Like
Hi Ken,
Thanks for that. So updating my code as follows:
def save(self, **kwargs):
super().save(**kwargs)
# Update the location on all moved tapes...
print(f"INSERTED ID: {self.id}")
print(f"TAPES: {self.tapes.all()}")
produces:
[20/Jun/2025 10:42:55] "GET /movement/add/ HTTP/1.1" 200 4967
INSERTED ID: 33
TAPES: <QuerySet []>
I was hoping the query set would not be empty. After more googling I’ve seen a few people say the same as @white-seolpyo above, that m2m data is saved some time after the save operation, so I guess what I now need to figure out is how do I leverage that so that I can do my extra work after the m2m data has been saved? What do I need to override or what signal do I need to listen for?
Please post the forms and views associated with this process. It’s a lot easier to diagnose real code than to try and address this abstractly.
Also see the docs at Creating forms from models | Django documentation | Django for more information.
Okay, here’s the view associated with creating a tape movement:
class CreateMovementView(CreateView):
model = Movement
fields = ['movement_date', 'tapes', 'location', 'comment']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["side_menu"] = TapeViewHelper().get_side_menu()
context["action"] = "New movement"
return context
the associated form:
{% extends "site_page.html" %}
{% block view_content %}
<h1>{{ action }}</h1>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save">
</form>
{% endblock %}
Most of what I’ve done so far comes from the official Django docs, no bells and whistles, so I’m hoping my basic approach is right.
I have something that works!
I’m not saying it’s the best solution, indeed, I’m sure it’s not, but I found that by listening for a couple of signals,“post_save” and “m2m_changed” and sharing some information between them, I can get the job done. Here’s what I have:
# Is this really how you do static variables in Python?
this = sys.modules[__name__]
this.movement_date = None
this.location = None
@receiver(post_save, sender=Movement)
def tape_movement_saved(sender, instance, created, **kwargs):
if created:
# Save when and where so it can be used by the m2m_changed
# signal handler
this.location = instance.location
this.movement_date = instance.movement_date
@receiver(m2m_changed)
def movement_m2m_changed(sender, instance, action, model, pk_set, **kwargs):
instance_name = instance.__class__.__name__
if action == 'post_add' and instance_name == 'Movement':
for tape_id in pk_set:
tape = Tape.objects.get(id=tape_id)
tape.date_moved = this.movement_date
tape.location = this.location
tape.save()
Is this “the right” approach? Can anyone suggest a better way? For example, there must be a better way to share data between the two signal handlers.
Thanks again!
<opinion>
No. I’d forget about using signals here. I see no reason, nor would find any benefit to using a signal in this process.
</opinion>
I would override the form_valid
method of the view and do this processing within that method.
If you want these functions to exist externally from the view, you can call them directly from the form_valid
function in the view.
In the general case, I consider the use of signals within Django to be an anti-pattern. Yes, sometimes they are necessary, and when necessary, you do need to use them. This is not one of those times.
1 Like
Thanks, Ken, that’s good to know. I’ll have another go using the approach you suggest and see how I get on.
So I read the manual on forms properly this time. I’d discounted using the form_valid function as I thought it was called to check if form data is valid as a precursor to saving the data. It turns out, and it’s right there in the docs, that it also takes care of actually saving the data, so your suggestion, @KenWhitesell, was spot-on. All the old code has been thrown away and replaced by this one extra function on the view:
def form_valid(self, form):
res = super().form_valid(form)
tapes = form.cleaned_data['tapes']
for tape in tapes:
tape.date_moved = form.cleaned_data['movement_date']
tape.location = form.cleaned_data['location']
tape.save()
return res
Happy days!
If you’re using a ModelForm, wouldn’t the many-to-many relationship be applied after saving the form?
obj = form.save()
obj.m2m.all