Delete images on foreign key from disk - python

models.py:
class Car(models.Model):
...
class Pictures(models.Model):
car = models.ForeignKey(Car, related_name='pictures')
width = models.PositiveIntegerField(editable=False, default=780)
height = models.PositiveIntegerField(editable=False, default=585)
image = models.ImageField(upload_to = get_file_path, max_length=64, height_field='height', width_field='width')
def __unicode__(self):
return str(self.id)
def delete(self, *args, **kwargs):
storage, path = self.image.storage, self.image.path
super(Pictures, self).delete(*args, **kwargs)
storage.delete(path)
It works nice (I delete one picture from admin panel and this picture is automatically deleted from disk).
But when I deleted Car object through admin panel, images are not removed from disk.
How to fix that?
Thanks!

I'm sure the problem here is that the ORM uses ON DELETE CASCADE to have the database handle removing the relations, meaning your delete method won't get called.
You could probably just apply the same technique you used here and do:
class Car(models.Model):
...
def delete(self, *args, **kwargs):
for picture in self.pictures.all():
storage, path = picture.image.storage, picture.image.path
storage.delete(path)
super(Car, self).delete(*args, **kwargs)
However, you are better off using signals instead of overriding the delete methods https://docs.djangoproject.com/en/dev/ref/signals/#post-delete
Note that the delete() method for an object is not necessarily called when deleting objects in bulk using a QuerySet. To ensure customized delete logic gets executed, you can use pre_delete and/or post_delete signals.

I started looking into this and found some interesting things regarding the admin and deletion of objects.
When deleting an object from the admin, the following function is called,
django/contrib/admin/options.py -> delete_model()
which in turn calls obj.delete() with obj being the current object being deleted.
The delete method for an object then runs the following code,
collector = Collector(using=using)
collector.collect([self])
collector.delete()
The collector object now has an attribute 'data' which contains all of the related objects.
When collector.delete() gets run, it executes the function query.delete_batch(pk_list, self.using) which does a bulk deletion using the argument pk_list which is a list of primary keys for related objects being deleted. The bulk deletion function in turn doesn't call the delete method of each related object being deleted.
The good thing here is that the pre_save and post_save signals do get called for all related objects so we could move your custom deletion code into either a pre_save or post_save signal for the Pictures model.
This should work I'm thinking but I haven't had a chance to test.

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 TimeStamped mixin with different created/modified fields

Here is the classic mixin used to know when a Django object is created or modified:
class TimeStampable(models.Model):
created = models.DateTimeField(auto_now_add=True, editable=False)
modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
The problem (it's not really a problem for most of us I guess) is that created and modified fields are not equal at the first creation (there is a tiny delta between them).
How would you improve this mixin to solve that specific issue?
I checked django-model-utils source code but found nothing.
I guess we would need to override __init__ method?
If you want 2 timezone objects to be equal, they must be created the exact same instance in time. That is virtually impossible, especially when you make the call to timezone.now() in series.
Essentially what happens at TimeStampable object creation time is that:
created gets a timezone.now instance.
modified gets a timezone.now which is created a tiny time fraction after the created's one.
We can override model's .save() method to solve this problem:
We will use model's _state.adding() method which is an instance of ModelState, to define if an object is yet unsaved (newly created).
If it is newly created, we need to take one (and only one) instance
of timezone.now and pass it to created and modified fields.
If the object just got modified, we must not forget to pass an instance of timezone.now to the modified field.
class MyTimestampableModel(Timestampable):
...
def save(self, *args, **kwargs):
timezone_now = timezone.now()
if self._state.adding:
self.created = timezone_now
self.modified = timezone_now
super(MyTimestampableModel, self).save(*args, **kwargs)

Accessing "self" in save method of class-based model

I have two models that look like this:
class ModelOne(models.Model):
foo = models.CharField(max_length=25)
def save(self,*args,**kwargs):
a = ModelTwo.objects.get(pk=arbitrary_pk)
a.somefield.add(self) # I am worried about this line here
super(ModelOne,self).save(*args,**kwargs)
class ModelTwo(models.Model):
somefield = models.ManyToManyField(ModelOne)
The line where I am adding self to a.somefield is the line I am worried about. How can I do this without error? Currently, I am getting:
ValueError: Cannot add "<ModelOne>": the value for field "modelone" is None
Thanks in advance
You can't do that because when you call .add() you have yet to save your model. That means that the model may not have been created (so it doesn't have an ID yet).
Basically you're telling Django to update the Foreign Key with something that doesn't exist yet (NULL), which will error out. You need to make sure the model has been created before you can set the foreign key.
try moving the a.somefield.add(self) to AFTER the super() call.
You cannot save many to may field before calling actual save method, you modify code like,
def save(self,*args,**kwargs):
super(ModelOne,self).save(*args,**kwargs) # Here your self has been saved
a = ModelTwo.objects.get(pk=arbitrary_pk)
a.somefield.add(self) # Now your self can be add as ManyToMany as it is already saved in db
I hope this help.
Add the instance to the many to many field after calling the save method.
class ModelOne(models.Model):
foo = models.CharField(max_length=25)
def save(self,*args,**kwargs):
super(ModelOne,self).save(*args,**kwargs)
a = ModelTwo.objects.get(pk=arbitrary_pk)
a.somefield.add(self) #add self to the object manytomany.
a.save() #save the object.
class ModelTwo(models.Model):
somefield = models.ManyToManyField(ModelOne)
You need to save the self object first. The many to many relation needs to have the related object saved in the database first, inorder to define the relationship. Then, define the relationship using a.somefield.add(self). Then, save the a object. Otherwise, the relation won't be committed in the database.
I ended up utilizing post_save to get this to work.

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.

Manipulate m2m records in django admin's save_model method

I have two models,
class Course(models.Model):
#bunch of fields
students = models.ManyToManyField(Student)
class Student(models.Model):
#bunch of fields
Now, instead of using the "regular" m2m widget in the Course admin screen, I added a file upload field in CourseAdminForm, to upload a text file with student IDs. I process the file in CourseAdmin.save_model method, iterating over a list of students like
def save_model(self, request, obj, form, change):
#some other stuff
#...
obj.save()
#obtain student_ids from uploaded file
#...
for id in student_ids:
s = Student.objects.get(pk=id)
course.students.add(s)
obj.save() #not sure this second one is needed
As it turns out, this doesn't work: the m2m relations are not saved. From what I've read, I understand this is because the whole save_model method happens within a transaction and after it is committed, the m2m relations are CLEARED and saved again, IF they come from a m2m widget. My problem thus is that I need to manipulate those relation by hand, and I just couldn't find a way (or a place) to do it.
The question thus is: how could I achieve the intended result of programmatically adding those m2m relations?
You can either connect to the m2m_changed signal https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed, or use Django 1.4's new "save_related" - https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_related which closed ticket 16115 https://code.djangoproject.com/ticket/16115
See also https://stackoverflow.com/a/8462541/640759

Categories

Resources