Infinite loop in many-to-many relation - django

I would like to use a ManyToMany relation. I have the following models:
class Person(models.Model):
name = odels.CharField(max_length=200)
songs = models.ManyToManyField(Songs)
class Songs(models.Model):
name = odels.CharField(max_length=200)
Now, upon saving a Person instance, I would like to build a many-to-many relation:
class Person(models.Model):
def save(self, *args, **kwargs):
song_instance, created = models.Songs.objects.get_or_create(name = some_name)
self.songs.add(song_instance)
super(Person, self).save(*args, **kwargs)
The problem with the above code is that upon form submit in the admin panel I get error
'Person' instance needs to have a primary key value before a
many-to-many relationship can be used.
However, if I switch the order as follows:
super(Person, self).save(*args, **kwargs)
self.songs.add(song_instance)
I do have a pk, but i will have to add another call to self.save() in order for the songs to be added to the Person instance, and that will cause an infinte loop.
So how can I go forward and make this work? :)
Thanks,
Joel

There's no need to save after adding an item to a ManyToMany relation. That doesn't modify the instance itself - it just adds a row to the (explicit or implicit) intermediate table. (The same is true of a reverse ForeignKey relationship - calling add changes the item to be added, not the item being added to.)

Related

Trouble overriding save method on Django model with ManyToManyField

I'm having trouble overriding the save method on a Django model to check a restriction on a many-to-many field.
Say I have the following models:
class Person(models.Model):
name = models.CharField()
class ClothingItem(models.Model):
description = models.CharField()
owner = models.ForeignKey(Person)
class Outfit(models.Model):
name = models.CharField()
owner = models.ForeignKey(Person)
clothing_items = models.ManyToManyField(ClothingItem)
I would like to put a restriction on the save method of Outfit that ensures that each ClothingItem in a given outfit has the same owner as the Outfit itself.
I.e. I'd like to write:
class Outfit(models.Model):
name = models.CharField()
owner = models.ForeignKey(Person)
clothing_items = models.ManyToManyField(ClothingItem)
def save(self, *args, **kwargs):
for ci in self.clothing_items:
if ci.owner != self.owner:
raise ValueError('You can only put your own items in an outfit!)
super(Outfit, self).save(*args, **kwargs)
but when I try that I get an error about <Outfit: SundayBest>" needs to have a value for field "outfit" before this many-to-many relationship can be used.
Any ideas what's going wrong here?
There are two issues going on here. To directly answer your question, the error basically means: You cannot refer to any m2m relationship if the original object(an instance of Outfit here) is not saved in database.
Sounds like you are trying to do the validation in save() method, which is a pretty bad practice in django. The verification process should typically happen in Form that creates Outfit objects. To override default django form, please refer to django ModelAdmin.form. To understand how to do validation on django forms, check ModelForm validation.
If you want code to refer to for m2m validation, I found a good example from SO.

Dynamically adding many to many relationships in the save method in Django

My Content model has a many-to-many relationship to the Tag model. When I save a Content object, I want to add the relationships dynamically. Im doing this the following way.
# models.py
def tag_content(content_id):
obj = Content.objects.get(pk=content_id)
print obj # Checking
obj.tags = [1, 2, 3] # Adding the relationships using the Tag IDs
class Tag(models.Model):
name = models.CharField(max_length=255)
class Content(models.Model):
title = models.CharField(max_length=255)
is_tagged = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag, blank=True)
def save(self, *args, **kwargs):
super(Content, self).save(*args, **kwargs)
if not self.is_tagged:
tag_content(self.pk) # calling the tagging method
In other words, when a Content object is saved, its tags field is related to 3 different Tag object models. Just to let you know, I do have the Tags with pks = 1, 2, and 3 in database.
However, this simply doesn't work. The save method calls the tag_content method, since the print obj statement works. However, the many-to-many field is not set and remains empty. The funny thing is, If I run the following commands in shell, the tags field is set perfectly.
# python manage.py shell
from myapp.models import *
obj = Content.objects.get(pk=1)
tag_content(obj.pk)
So how come the shell version works, but the other one doesn't? Any help is appreciated.
You can't work on an m2m relationship in a custom save method because of the way Django writes those relationships to the database. When saving a model instance with an m2m relationship, Django first writes the object then goes in again and writes the appropriate m2m relationships. Since the m2m stuff comes "second," trying to work with the relationship in a custom save fails.
The solution is to use a post-save signal. Remove the custom save stuff and add this below your model definitions, making sure to import receiver and post_save:
#receiver(post_save, sender = Content)
def update_m2m_relationships_on_save(sender, **kwargs):
if not kwargs['instance'].is_tagged:
tag_content(kwargs['instance'].pk)
Your tag_content function should probably swap is_tagged to True and then save the instance; if that boolean is never flipped then this could just run in an endless loop. You can also just pass in the object instead of passing in the pk:
def tag_content(thing_to_tag):
thing_to_tag.tags.add([1,2,3])
thing_to_tag.is_tagged = True
thing_to_tag.save()
return thing_to_tag
Note the use of .add(), which is important when adding to an m2m relationship.

Listing only usable values in OneToOneField Django

I want to list only usable items in OneToOneField not all items, its not like filtering values in ChoiceField because we need to find out only values which can be used which is based on the principle that whether it has been used already or not.
I am having a model definition as following:
class Foo(models.Model):
somefield = models.CharField(max_length=12)
class Bar(models.Model):
somefield = models.CharField(max_length=12)
foo = models.OneToOneField(Foo)
Now I am using a ModelForm to create forms based on Bar model as:
class BarForm(ModelForm):
class Meta:
model = Bar
Now the problem is in the form it shows list of all the Foo objects available in database in the ChoiceField using the select widget of HTML, since the field is OneToOneField django will force to single association of Bar object to Foo object, but since it shows all usable and unusable items in the list it becomes difficult to find out which values will be acceptable in the form and users are forced to use hit/trial method to find out the right option.
How can I change this behavior and list only those items in the field which can be used ?
Although this is an old topic I came across it looking for the same answer.
Specifically for the OP:
Adjust your BarForm so it looks like:
class BarForm(ModelForm):
class Meta:
model = Bar
def __init__(self, *args, **kwargs):
super(BarForm, self).__init__(*args, **kwargs)
#only provide Foos that are not already linked to a Bar, plus the Foo that was already chosen for this Bar
self.fields['foo'].queryset = Foo.objects.filter(Q(bar__isnull=True)|Q(bar=self.instance))
That should do the trick. You overwrite the init function so you can edit the foo field in the form, supplying it with a more specific queryset of available Foo's AND (rather important) the Foo that was already selected.
For my own case
My original question was: How to only display available Users on a OneToOne relation?
The Actor model in my models.py looks like this:
class Actor(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name = 'peactor')
# lots of other fields and some methods here
In my admin.py I have the following class:
class ActorAdmin(admin.ModelAdmin):
# some defines for list_display, actions etc here
form = ActorForm
I was not using a special form before (just relying on the basic ModelForm that Django supplies by default for a ModelAdmin) but I needed it for the following fix to the problem.
So, finally, in my forms.py I have:
class ActorForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ActorForm, self).__init__(*args, **kwargs)
#only provide users that are not already linked to an actor, plus the user that was already chosen for this Actor
self.fields['user'].queryset = User.objects.filter(Q(peactor__isnull=True)|Q(peactor=self.instance))
So here I make an ActorForm and overwrite the __init__ method.
self.fields['user'].queryset =
Sets the queryset to be used by the user formfield. This formfield is a ModelChoiceField
by default for a OneToOneField (or ForeignKey) on a model.
Q(peactor__isnull=True)|Q(peactor=self.instance)
The Q is for Q-objects that help with "complex" queries like an or statement.
So this query says: where peactor is not set OR where peactor is the same as was already selected for this actor
peactor being the related_name for the Actor.
This way you only get the users that are available but also the one that is unavailable because it is already linked to the object you're currently editing.
I hope this helps someone with the same question. :-)
You need something like this in the init() method of your form.
def __init__(self, *args, **kwargs):
super(BarForm, self).__init__(*args, **kwargs)
# returns Bar(s) who are not in Foo(s).
self.fields['foo'].queryset = Bar.objects.exclude(id__in=Foo.objects.all().values_list(
'bar_id', flat=True))
PS: Code not tested.

Django clear ManyToManyField associations on update

On update, I'm trying to clear all associations from a model having ManyToMany association with another. However, the associations are still persisted.
To explain my point, consider the following 2 models:
class Teacher(models.Model):
school = models.ForeignKey(School)
students = models.ManyToManyField(Student)
class Student(models.Model):
school = models.ForeignKey(School)
Now, I wish to remove all associations of teacher to student, or vice-versa, whenever the shool changes for either model. So I decided to override the save() method for Teacher first.
class Teacher(models.Model):
...
...
def save(self, *args, **kwargs):
# after figuring out "school_id" changed
self.students.remove()
super(Teacher, self).save(*args, **kwargs)
However, once the Teacher object is persisted, I can see that all the Student associations are reinserted into the association table.
Am I missing out something here, or, is there some way to instruct the models.Model.save() not to reinsert the associations?
Thanks!
You need to use clear() method not remove(). According to docs, delete() removes a specified model objects from the related object set
>>> b = Blog.objects.get(id=1)
>>> e = Entry.objects.get(id=234)
>>> b.entry_set.remove(e) # Disassociates Entry e from Blog b.
But, that is not your case, you want to disassociate all of them not a specified one.
def save(self, *args, **kwargs):
# after figuring out "school_id" changed
self.students.clear()
super(Teacher, self).save(*args, **kwargs)

Django foreign keys - one class has the same relationship to multiple other classes

I have a model with three classes: PendingAuthorisation, Director, and Member. Director and Member instances are considered inactive if they have a corresponding PendingAuthorisation.
My problem is quite how to model this. I would ideally like PendingAuthorisation to have just one field, defers, which can refer to either a Director or a Member. If I create a foreign key in both Director and Member then I need to have two differently-named relations, and when using a PendingAuthorisation I would need to check both to find the object it is deferring. In no case should a PendingAuthorisation be deferring one object of each type.
Any suggestions on how to model this?
I'd recommend having two foreign keys (with a sanity check in your save method to make sure that they're not both set), and then a property to return back the object that's set.
Think about something like:
from django.db import models
class PendingAuthorization(models.Model):
director = models.ForeignKey(Director, null=True, blank=True)
member = models.ForeignKey(Member, null=True, blank=True)
def save(self, *args, **kwargs):
if self.director and self.member:
raise ValueError, 'Both a director and member are set; this is not allowed.'
return super(PendingAuthorization, self).save(*args, **kwargs)
#property
def defers(self):
if self.director:
return self.director
if self.member:
return self.member
return None