Django clear ManyToManyField associations on update - django

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)

Related

Django why model foreign key cascade will not trigger delete?

there two basic ways to do something when an instance gets deleted:
Overwrite Model.delete
Signal
I used to reckon both of them serve the same purpose, just provides different ways of writing, but works exactly.
However, in this occasion, I realise I was wrong:
class Human(models.Model):
name = models.CharField(max_length=20)
class Pet(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Human, related_name="pet", on_delete=models.CASCADE)
def delete(self, *args, **kwargs):
print('------- Pet.delete is called')
return super().delete(*args, **kwargs)
h = Human(name='jason')
h.save()
p = Pet(name="dog", owner=h)
p.save()
h.delete()
# nothing is shown
Why Pet.delete Is not firing at Human.delete By the foreign cascade? Does I have to apply a signal on this? If so, would it cost more performance?
I am building something very heavy, comment system, filter decent records and delete when the commented target get deleted, the comment model has many null-able foreign key fields, with models.CASCADE Set, only one of them is assigned with value. But in product delete view, I call product.delete Then triggers cascade, but comment.delete Is not firing.
Currently, the project has delete Defined on many models, with assumption that it is always triggered when the instance get removed from database, and it is tremendous work to rewrite it in signal. Is there a way to call delete When at cascading? (I know it is likely impossible since it is a database field specification)
I implement a mix-in for Commendable models with extra methods defined, therefore, I decided to modify delete method to signal to something like this:
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import pre_delete
# Create your models here.
class Base:
def __init_subclass__(cls):
#receiver(pre_delete, sender=cls)
def pet_pre_delete1(sender, instance, **kwargs):
print('pet pre delete 1 is called')
#receiver(pre_delete, sender=cls)
def pet_pre_delete2(sender, instance, **kwargs):
print('pet pre delete 2 is called')
class Human(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return f'<human>{self.name}'
class Pet(Base, models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Human, related_name="pet", on_delete=models.CASCADE)
def __str__(self):
return f'<pet>{self.name}'
# ------- Pet.delete is called
# pet pre delete 1 is called
# pet pre delete 2 is called
it works fine in testing, I wonder if there is any risk using this, would it be garbage collected?

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.

How to exclude django model fields during a save?

I've got a fairly complicated Django model that includes some fields that should only be saved under certain circumstances. As a simple example,
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=200)
counter = models.IntegerField(default=0)
def increment_counter(self):
self.counter = models.F('counter') + 1
self.save(update_fields=['counter'])
Here I'm using F expressions to avoid race conditions while incrementing the counter. I'll generally never want to save the value of counter outside of the increment_counter function, as that would potentially undo an increment called from another thread or process.
So the question is, what's the best way to exclude certain fields by default in the model's save function? I've tried the following
def save(self, **kwargs):
if update_fields not in kwargs:
update_fields = set(self._meta.get_all_field_names())
update_fields.difference_update({
'counter',
})
kwargs['update_fields'] = tuple(update_fields)
super().save(**kwargs)
but that results in ValueError: The following fields do not exist in this model or are m2m fields: id. I could of course just add id and any m2m fields in the difference update, but that then starts to seem like an unmaintainable mess, especially once other models start to reference this one, which will add additional names in self._meta.get_all_field_names() that need to be excluded from update_fields.
For what it's worth, I mostly need this functionality for interacting with the django admin site; every other place in the code could relatively easily call model_obj.save() with the correct update_fields.
I ended up using the following:
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=200)
counter = models.IntegerField(default=0)
default_save_fields = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default_save_fields is None:
# This block should only get called for the first object loaded
default_save_fields = {
f.name for f in self._meta.get_fields()
if f.concrete and not f.many_to_many and not f.auto_created
}
default_save_fields.difference_update({
'counter',
})
self.__class__.default_save_fields = tuple(default_save_fields)
def increment_counter(self):
self.counter = models.F('counter') + 1
self.save(update_fields=['counter'])
def save(self, **kwargs):
if self.id is not None and 'update_fields' not in kwargs:
# If self.id is None (meaning the object has yet to be saved)
# then do a normal update with all fields.
# Otherwise, make sure `update_fields` is in kwargs.
kwargs['update_fields'] = self.default_save_fields
super().save(**kwargs)
This seems to work for my more complicated model which is referenced in other models as a ForeignKey, although there might be some edge cases that it doesn't cover.
I created a mixin class to make it easy to add to a model, inspired by clwainwright's answer. Though it uses a second mixin class to track which fields have been changed, inspired by this answer.
https://gitlab.com/snippets/1746711

Infinite loop in many-to-many relation

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.)

Django call function when an object gets added

Hay, i have a simple model
class Manufacturer(models.Model):
name = models.CharField()
car_count = models.IntegerField()
class Car(models.Model):
maker = ForeignKey(Manufacturer)
I want to update the car_count field when a car is added to a manufacturer, I'm aware i could just count the Manufacturer.car_set() to get the value, but i want the value to be stored within that car_count field.
How would i do this?
EDIT
Would something like this work?
def save(self):
if self.id:
car_count = self.car_set.count()
self.save()
The best way make something happen when a model is saved it to use a signal. Django's documentation does a good job of describing what signals are and how to use them: http://docs.djangoproject.com/en/dev/topics/signals/
I'm not sure why you need to make it a field in the model though. Databases are very good at counting rows, so you could add a model method to count the cars which would use a very fast COUNT() query.
class Manufacturer(models.Model):
name = models.CharField()
def car_count(self):
return Car.objects.filter(maker=self).count()
class Car(models.Model):
maker = ForeignKey(Manufacturer)
In light of the requirement added by your comment, you're back to updating a field on the Manufacturer model whenever a Car is saved. I would still recommend using the count() method to ensure the car_count field is accurate. So your signal handler could look something like this:
def update_car_count(sender, **kwargs):
instance = kwargs['instance']
manufacturer = instance.maker
manufacturer.car_count = Car.objects.filter(maker=self).count()
manufacturer.save()
Then you would connect it to both the post_save and post_delete signals of the Car model.
post_save.connect(update_car_count, sender=Car)
post_delete.connect(update_car_count, sender=Car)
The proper way to let the database show how many cars a manufacturer has, is to let the database calculate it in the view using aggregations.
from django.db.models import Count
Manufacturer.objects.all().annotate(car_count=Count(car)).order_by('car_count')
Databases are very efficient at that sort of thing, and you can order by the result as seen above.
I'm a tiny bit confused.
.. when a car is added to a manufacturer ..
In the code shown in your question, I'd guess, you save a car with some manufacturer, e.g.
car.maker = Manufacturer.objects.get(name='BMW')
car.save()
Then the save method of the Car class would need to update the car_count of the manufacturer (see Overriding predefined model methods for more details).
def save(self, *args, **kwargs):
if self.id:
self.maker.car_count = len(self.maker.car_set.all())
super(Car, self).save(*args, **kwargs)
Since this isn't the most elegant code, I'd suggest as #Josh Wright to look into signals for that matter.
P.S. You could also add a method on the Manufacturer class, but I guess, you want this attribute to live in the database.
class Manufacturer(models.Model):
name = models.CharField()
def _car_count(self):
return len(self.car_set.all())
car_count = property(_car_count)
...
The override in MYYN's answer won't work, since Car.id won't be set (and probably not included in the Manufacturer's car_set) until it's saved. Instead, I'd do something like:
def save(self, *args, **kwargs):
super(Car, self).save(*args, **kwargs)
self.maker.car_count = len(self.maker.car_set.all())
self.maker.save()
Which is untested, but should work.
Of course, the best way is to use Josh's solution, since that's going 'with the grain' of Django.