Django - .delete() method is called, but instance remains in DB - python

got a little problem here using the Django-framework and its
.delete() method.
Im using the following model
class Message(models.Model):
...
sender = models.ForeignKey(User, related_name="%(class)s_sender")
user = models.ManyToManyField(User, related_name="%(class)s_recip")
trash = models.ManyToManyField(User, related_name="%(class)s_trash", blank=True)
...
So my aim is to check if both M2M fields "user" and "trash" do not contain objects/keys anymore, and if so, the corresponding Message instance should be deleted from the database.
To archieve this I'm using m2m_changed signals as follows
def set_recip_names(sender, instance, action,**kwargs):
print instance
if action == "post_remove":
if instance.user.all().count() == 0 and instance.trash.all().count() == 0:
print instance.id
instance.delete()
def msg_del(sender, **kwargs):
print "Message deleted"
m2m_changed.connect(set_recip_names, sender=Message.user.through)
m2m_changed.connect(set_recip_names, sender=Message.trash.through)
post_delete.connect(msg_del, sender=Message)
The first two print statements are for debug purpous of course, and both print exactly what Im expecting: instance IS the corresponding instance to be deleted, and instance.id confirms this, as well as telling me that Django actually will execute the .delete() method.
The second post_delete signal is for debugging purpous too, and it is called as expected, so Im getting the output "Message deleted".
So the big problem is, that the "to-be-deleted" instance remains in the database. Ive searched the internet for an hour now but I dont see anything is wrong with my code.
Any suggestions are welcomed, or if you know any better way to do what Im trying to do, please let me know.
Solved
Problem was actually pretty obvious. One of my code snippets, where on user interaction elements from my M2M fields are removed looks like
...
try:
message = Message.objects.get(id=msg)
if usr not in message.trash.all():
continue
if usr != message.sender:
message.user.remove(usr) # m2m_changed triggered
message.trash.remove(usr) # m2m_changed triggered
# if instance was removed by now, this .save() will
# insert the python-local object in the db, so it looked
# like the same instance is still there, but I saw that
# the "old" instance suddenly had a new id
message.save()
except Message.DoesNotExist:
continue
...

You can delete the record directly by doing this:
YourModel.objects.get(id=instance.id).delete()
If the record exists, then it will be deleted in the database.
Your instance is saved in the RAM and may not be deleted, you can do that manually by deleting it from the RAM: del instance

Related

Invoke Django Signal only if the record is newly created - Django

I am using Django post_save signal, which will trigger whenever the new record is created. This signal is called every time a save action takes place. Even though it has the created boolean field, this function is executed all the time.
The problem here is, at each request I am updating the User table last_login and last_active fields. Hence on each request this signal is getting executed. even though we are having created Boolean field, the function call is happened. which may cause performance impact when we have million request at a time.
I am looking for a solution, the signal should get executed only if the new record is created in the User table.
Sample code:
#receiver(post_save, sender=User, dispatch_uid="call_method")
def call_method(sender, instance, created, **kwargs):
print ('>>>>>>>>>> Signal Called ', instance)
if created:
print ('$$$$$$$$$$$ User created')
Kindly help me to find the better solution.
If you call save on an instance model there is really no way around not hooking into the signal.
However if you were to change the way you update your user instance you could skip the signal.
This for instance won't trigger the post_save event
User.objects.filter(id=1).update(last_login=timestamp)
Consider moving the logic implemented by your signal into custom_save() of your User class or rewrite the default save() function...
The trick is to place your instructions behind the condition that a primary key exists on the object being saved.
So, when the custom_save() or save() method is called, your logic behind it will only be executed on the first record (i.e. the creation)
Practical example:
class User(models.Model)
...
email = models.Charfield(max_lenght=250, unique=True)
# Overwrite Save method
def save(self, *args, **kwargs):
...
if not self.pk: # Means that it is the first record of this object
print (f'{self.email} User created')
super().save(*args, **kwargs)
You can use pre_save signal for this.
pre_save:- To execute some code dealing with another part of your application before the object gets saved to the database, you have to use a pre_save signal.
#receiver(pre_save, sender=User, dispatch_uid="call_method")
def trigger_emails_to_employees(sender, instance, **kwargs):
if not instance.pk: # A newly created object (not denotes that the instance does not exists till now)
print("Send email, trigger msgs, block previous records")
print("Do whatever you want before creating the new object")
------------------------------
if instance.pk: # Invokes only when object is being edited.
print("Trigger editing object things before the object get updated")
print("Do whatever you want before editing the existing object")

Django Add list generated from the text of one field to many to many field

Having a bit of trouble trying to bulk add a list of items to a many to many field and though having tried various things have no clue on how to approach this. I've looked at the Django documentation and cant seem to find what I'm looking for.
Here is the code for my models:
class Subject(models.Model):
noun = models.CharField(max_length=30, null=True, blank=True)
class Knowledge(models.Model):
item_text = models.TextField()
item_subjects = models.ManyToManyField(Subject, null=True, blank=True)
def add_subjects(sender, instance, *args, **kwargs):
if instance.item_info:
item_subjects = classifier.predict_subjects(instance.item_info)
if item_subjects:
....
post_save.connect(add_subjects, sender=Knowledge)
The list is being generated by the classifer.predict_subjects function.
I have tried using the m2m_changed connector and the pre_save and post_save connect. I'm not even sure the many to many field is the right option would it be better to do make a foreign key relationship.
in place of the '...' I have tried this but it doesn't create the relationship between and only saves the last one.
for sub in item_subjects:
subject = Subject(id=instance.id, noun=sub)
subject.save()
I've also tried
instance.item_subjects = item_subjects
and a load more things that I can't really remember, I don't really think I'm in the right ballpark to be honest. Any suggestions?
edit:
ok, so I have got it adding all of the list items but still haven't managed to link these items to the many to many field.
for sub in item_subjects:
subject = Subject.objects.get_or_create(noun=sub)
edit 2:
So doing pretty much exactly the same thing outside of the loop in the Django shell seems to be working and saves the entry but it doesn't inside the function.
>>> k[0].item_subjects.all()
<QuerySet []>
>>> d, b = Subject.objects.get_or_create(noun="cats")
<Subject: cats>
>>> k[0].item_subjects.add(d)
>>> k[0].item_subjects.all()
<QuerySet [<Subject: cats>]>
edit 3
So I took what Robert suggested and it works in the shell just like above just not when using it in the admin interface. The print statements in my code show the array item being updated but it just dosen't persist. I read around and this seems to be a problem to do with the admin form clearing items before saving.
def sub_related_changed(sender, instance, *args, **kwargs):
print instance.item_subjects.all()
if instance.item_info:
item_subjects = classifier.predict_subjects(instance.item_info)
if item_subjects:
for sub in item_subjects:
subject, created = Subject.objects.get_or_create(noun=sub)
instance.item_subjects.add(subject)
print instance.item_subjects.all()
post_save.connect(sub_related_changed, sender=Knowledge)
I have tried using the function as m2m_changed signal as follows:
m2m_changed.connect(model_saved, sender=Knowledge.item_subjects.through)
But this either generates a recursive loop or doesn't fire.
Once you have the subject objects (as you have in your edit), you can add them with
for sub in item_subjects:
subject, created = Subject.objects.get_or_create(noun=sub)
instance.item_subjects.add(subject)
The "item_subjects" attribute is a way of managing the related items. The through relationships are created via the "add" method.
Once you've done this, you can do things like instance.item_subjects.filter(noun='foo') or instance.item_subjects.all().delete() and so on
Documentation Reference: https://docs.djangoproject.com/en/1.11/topics/db/examples/many_to_many/
EDIT
Ahh I didn't realize that this was taking place in the Django Admin. I think you're right that that's the issue. Upon save, the admin calls two methods: The first is model_save() which calls the model's save() method (where I assume this code lives). The second method it calls is "save_related" which first clears out ManyToMany relationships and then saves them based on the submitted form data. In your case, there is no valid form data because you're creating the objeccts on save.
If you put the relevant parts of this code into the save_related() method of the admin, the changes should persist.
I can be more specific about where it should go if you'll post both your < app >/models.py and your < app >/admin.py files.
Reference from another SO question:
Issue with ManyToMany Relationships not updating inmediatly after save

Using save method to extend M2M relationship

I have a model like this
class Authority(models.Model):
name=models.CharField(max_length=100)
country=models.ForeignKey(Country)
category=models.ForeignKey(Category)
competitors=models.ManyToManyField("self",related_name="competitors")
I want authorities having the same country and category and itself to be automatically give an M2M relationship,so i did this
def save(self,*args,**kwargs):
z=Authority.objects.filter(country=self.country).filter(category=self.category)
this_authority=Authority.objects.get(id=self.id)
for a in z:
this_authority.competitors.add(a)
super(Authority,self).save(*args,**kwargs)
It wasn't working and not bringing any error,I also tries this below
def save(self,*args,**kwargs):
z=Authority.objects.filter(country=self.country).filter(category=self.category)
this_authority=Authority.objects.get(id=self.id)
self.competitors=z
super(Authority,self).save(*args,**kwargs)
What might be wrong with my code?Thanks in advance.
The reason this isn't working the way you expect is because of how Django handles creating m2m relationships in the database. Long story very short, when you save something like a new Authority to the database, Django first writes the new object then goes back in and writes the m2m relationships for that new object. As a result, it's tough to do anything useful to m2m relationships in a custom save method.
A post-save signal may do the trick here. kwargs['created'] = True if we're creating a new object and kwargs['instance'] is the instance whose save fired off the signal receiver.
#receiver(post_save, sender = Authority)
def update_m2m_relationships(sender, **kwargs):
if kwargs['created']: #only fire when creating new objects
competitors_to_add = Authority.objects.filter(
country = kwargs['instance'].country,
category = kwargs['instance'].category
)
for c in competitors_to_add:
c.competitors.add(kwargs['instance'])
c.save() #not creating a new object; this receiver does not fire here
kwargs['instance'].competitors.add(c)
#all competitors have been added to the instance's m2m field
kwargs['instance'].save()
It's important to only fire this when creating new objects. If you don't include that restriction, then the receiver will trigger itself as you update other objects in your for loop.
I haven't tested this out but I think it'll work. Let me know if it doesn't and I'll do my best to help.

Prevent m2m_changed from firing when creating an object

When using a Django signal like post_save you can prevent it from firing when an object is first created by doing something like:
#receiver(post_save,sender=MyModel)
def my_signal(sender, instance, created,**kwargs):
if not created:
pass # Do nothing, as the item is new.
else:
logger.INFO("The item changed - %s"%(instance) )
However, ManyToMany relations are applied after an item is initially created, so no such argument is passed in, making it difficult to suppress in these cases.
#receiver(m2m_changed,sender=MyModel.somerelation.though)
def my_signal(sender, instance, created,**kwargs):
if __something__: # What goes here?
pass # Do nothing, as the item is new.
else:
logger.INFO("The item changed - %s"%(instance) )
Is there an easy way to suppress an m2m_changed signal when its being done on an object that has just been created?
I think there is no easy way to do that.
As the Django doc says, you can't associate an item with a relation until it's been saved. Example from the doc:
>>> a1 = Article(headline='...')
>>> a1.publications.add(p1)
Traceback (most recent call last):
...
ValueError: 'Article' instance needs to have a primary key value before a many-to-many relationship can be used.
# should save Article first
>>> a1.save()
# the below statement never know it's just following a creation or not
>>> a1.publications.add(p1)
It's logically not possible for a relation record to know whether it is added to "a just created item" or "an item that already exists for some time", without external info.
Some workarounds I came up with:
Solution 1. add a DatetimeField in MyModel to indicate creation time. m2m_changed handler uses the creation time to check when is the item created. It work practically in some cases, but cannot guarantee correctness
Solution 2. add a 'created' attribute in MyModel, either in a post_save handler or in other codes. Example:
#receiver(post_save, sender=Pizza)
def pizza_listener(sender, instance, created, **kwargs):
instance.created = created
#receiver(m2m_changed, sender=Pizza.toppings.through)
def topping_listener(sender, instance, action, **kwargs):
if action != 'post_add':
# as example, only handle post_add action here
return
if getattr(instance, 'created', False):
print 'toppings added to freshly created Pizza'
else:
print 'toppings added to modified Pizza'
instance.created = False
Demo:
p1 = Pizza.objects.create(name='Pizza1')
p1.toppings.add(Topping.objects.create())
>>> toppings added to freshly created Pizza
p1.toppings.add(Topping.objects.create())
>>> toppings added to modified Pizza
p2 = Pizza.objects.create(name='Pizza2')
p2.name = 'Pizza2-1'
p2.save()
p2.toppings.add(Topping.objects.create())
>>> toppings added to modified Pizza
But be careful using this solution. Since 'created' attribute was assigned to Python instance, not saved in DB, things can go wrong as:
p3 = Pizza.objects.create(name='Pizza3')
p3_1 = Pizza.objects.get(name='Pizza3')
p3_1.toppings.add(Topping.objects.create())
>>> toppings added to modified Pizza
p3.toppings.add(Topping.objects.create())
>>> toppings added to freshly created Pizza
That's all about the answer. Then, caught you here! I'm zhang-z from github django-notifications group :)
#ZZY's answer basically helped me realise that this wasn't possible without storing additional fields. Fortunately, I'm using django-model-utils which includes a TimeStampedModel which includes a created field.
Providing a small enough delta, it was relatively easy to check against the created time when catching the signal.
#receiver(m2m_changed,sender=MyModel.somerelation.though)
def my_signal(sender, instance, created,**kwargs):
if action in ['post_add','post_remove','post_clear']:
created = instance.created >= timezone.now() - datetime.timedelta(seconds=5)
if created:
logger.INFO("The item changed - %s"%(instance) )
For an easier and short way of checking in the object is created or not is using the _state.adding attribute:
def m2m_change_method(sender, **kwargs):
instance = kwargs.pop('instance', None)
if instance:
if instance.adding: #created object
pk_set = list(kwargs.pop('pk_set')) #ids of object added to m2m relation
else:
# do something if the instance not newly created or changed
# if you want to check if the m2m objects is new use pk_set query if exists()
m2m_change.connect(m2m_change_method, sender=YourModel.many_to_many_field.through)

updating user profile fields django

Should be a simple answer but I can't figure out what's wrong here...
I have a user profile with a couple of simple fields. I'm trying to update them like so:
if data['dob'] != None:
request.user.profile.dob = data['dob']
request.user.profile.save()
This doesn't seem to have any effect at all though.
p.s. i am using a nice little trick in my UserProfile class that looks like this:
User.profile = property(lambda u: UserProfile.objects.get_or_create(user=u)[0])
Could this be part of the problem?
Think about what happens in your code.
If there's a dob in your data, you call request.user.profile. This calls your property, which makes a request to the database and gets or creates a Profile instance.
Next, you call request.user.profile again. Guess what this does? Makes a fresh call to the database, and gets an instance of the Profile again. But of course this is a new instance, even though it's referring to the same database row, so it won't have the value for dob you just set on the last version.
Now, potentially you could solve this by storing the profile in a local variable:
profile = request.user.profile
profile.dob = data['dob']
profile.save()
But to be honest, I'd drop the whole hacking around with the profile property. It's going to cause you all sorts of problems.
It might be easier to use the suggested method of tying a profile to a django user:
https://docs.djangoproject.com/en/dev/topics/auth/#storing-additional-information-about-users
In the meantime, remove the [0] at the end of the UserProfile.objects.get_or_create(user=u) as that method only returns a single object regardless

Categories

Resources