Django delete multiple ImageField files on model delete - django

I have an Image model with two ImageFields (one for regular image and one for the thumbnail). I'm trying to delete both files together with a post_delete receiver but I'm having difficulty chaining the operations.
What I have throws a file does not exist error probably because only one delete saves.
class Image(models.Model):
path = models.ImageField(upload_to=photo_image_upload_path,blank=False, null=False)
thumb = models.ImageField(upload_to='', editable=False, default=None, null=True)
#receiver(post_delete, sender=Image)
def photo_delete(sender, instance, **kwargs):
# Pass false so FileField doesn't save the model.
if instance.thumb is not None:
instance.thumb.delete(True)
if instance.path is not None:
instance.path.delete(False)

Related

Checking if a field in model is modified and creating instance of another model

I have two models
Project Model
class Project(models.Model):
name = models.CharField(max_length=200)
workflow = models.ForeignKey("WorkflowType", null=True, blank=True, on_delete=models.SET_NULL)
created_on = models.DateTimeField(auto_now_add=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
def __str__(self):
return self.name
Workflow Instance Model
class WorkflowInstance(models.Model):
workflow_step = models.ForeignKey('WorkflowStep', null=True, blank=True, on_delete=models.CASCADE)
project = models.ForeignKey('Project', null=True, blank=True, on_delete=models.SET_NULL)
I want to check if the value of workflow field in "Project" models is added or changed for a particular project.
I am approaching the problem in following manner:
Checking if the previous and the new value of the "workflow" field in a project are different. If yes (modifies), then create the new instance of a project.
#receiver(pre_save, sender=Project)
def projectToBeUpdated(sender, instance, **kwargs):
if instance.id is None:
pass
else:
previous = Project.objects.get(id=instance.id)
if previous.workflow != instance.workflow:
print("workflow value modified. Please create a WorkflowInstance")
Problem: The comparison for previous and new value of the "workflow" field are happening in "pre_save" signal. But my new instance creation for workflowInstance is to be created in "post_save" signal. How can I do this?
Also, ideally I would like to store the previous value of workflow field in "pre_save" and get the new value of the field in "post_save". Reason being, save() method might fail for any reason, while I am comparing the previous and new value in "pre_save" method itself. Making changes in the database without confirming if the save() method executed successfully would be a wrong approach in my view.
You can override the model save method itself and use the following code logic, additionally if you want to identify what fields got modified you can use the dirtyfields package as well.
def save(self, *args, **kwargs):
if not self._state.adding:
changed_attr = self.get_dirty_fields()
else:
is_new = True
super().save(*args, **kwargs)```

django manytomany model relationship crashing admin on object create

I have an Event object in my postgres db, and created a new Collection object to group events by theme via a ManyToMany field relationship:
class Collection(models.Model):
event = models.ManyToManyField('Event', related_name='collections')
name = models.CharField(blank=True, max_length=280)
slug = AutoSlugField(populate_from='name')
image = models.ImageField(upload_to='collection_images/', blank=True)
description = models.TextField(blank=True, max_length=1000)
theme = models.ManyToManyField('common.Tag', related_name='themes')
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=False)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('collection', args=[self.slug])
def clean(self):
# because of the way db saves M2M relations, collection doesn't have a
# type at this time yet, so image inheritance is
# called from the signal which is triggered when M2M is created
# (that means if an image is later deleted, it won't inherit a new
# one when collection is saved)
if self.image:
validate_hero_image(self.image, 'image')
def save(self, *args, **kwargs):
try:
self.full_clean()
except ValidationError as e:
log.error('Collection validation error (name = %s): %s' % (self.name, e))
return super(Collection, self).save(*args, **kwargs)
in my admin, I'm defining and registering CollectionAdmin like this:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ( 'name', )
however, if I go into admin and attempt to create a Collection "GET /admin/app/collection/add/" 200, the request frequently times out and the query load on my database from the Event M2M relationship seems quite heavy from logging. For reference currently the db has ~100,000 events. are there better ways to (re)structure my admin fields so I can select specific events (by name or id) to add to a Collection without effectively requesting a QuerySet of all events when that view is loaded (or creating them in db via shell)? thanks
There are multiple ways to accomplish this. For example, you could override the form fields the admin uses and specify another widget to use, like NumberInput.
You could also add your event model field to the raw_id_fields attribute of ModelAdmin. By doing so, Django won't try to create a fully populated select input but will offer you a way to search for events manually if needed:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ('name', )
raw_id_fields = ('event', )

Using callable as path attribute of Django's FilePathField?

I have the following model that includes a file upload by a user.
def resume_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/resume/<filename>
return 'user_{0}/resume/{1}'.format(instance.student_user.id, filename)
class Resume(models.Model):
resume = models.FileField(upload_to=resume_path, blank=True, null=True)
pub_date = models.DateTimeField(default=timezone.now)
student_user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
Then, I would like to allow a user to select one of their uploaded files in a later form. So, I need to be able to dynamically set the path to the directory containing that user's files similar to the dynamic way that I set the upload_path in the original model.
I've tried the following according to this link:
def resume_directory_path(instance):
# returns the path: MEDIA_ROOT/user_<id>/resume/
return 'user_{0}/resume/'.format(instance.student_user.id)
class JobApplication(models.Model):
student_user = models.ForeignKey(StudentUser, on_delete = models.CASCADE)
resume = models.FilePathField(path=resume_directory_path, null=True)
However, looking at the documentation for FilePathField in Django 3.0, it doesn't look like it takes a callable for the path attribute. So, I'm not sure how the answer in the above link answers my question. What is the best way to achieve this functionality?
I'd like to do something like the below:
class CallableFilePathField(models.FilePathField):
def __init__(self, *args, **kwargs):
kwargs['path'] = resume_directory_path(instance)
super().__init__(*args, **kwargs)
class JobApplication(models.Model):
student_user = models.ForeignKey(StudentUser, on_delete = models.CASCADE)
resume = models.CallableFilePathField(path=resume_directory_path, null=True)
The trouble is that I don't know how to properly reference the model instance in this code (so instance is undefined). I've looked at the FileField code to try to see how they do it there, but I couldn't make sense of it.

How can make the admin for a ForeignKey('self') ban referring to itself?

I have a model with a forgein key to itself. For example:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
For my purposes, I never want parent_folder to refer to itself, but the default admin interface for this model does allow the user to choose its own instance. How can I stop that from happening?
Edit: If you're trying to do a hierarchical tree layout, like I was, another thing you need to watch out for is circular parent relationships. (For example, A's parent is B, B's parent is C, and C's parent is A.) Avoiding that is not part of this question, but I thought I would mention it as a tip.
I would personally do it at the model level, so if you reuse the model in another form, you would get an error as well:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
def clean(self):
if self.parent_folder == self:
raise ValidationError("A folder can't be its own parent")
If you use this model in a form, use a queryset so the model itself doesn't appear:
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ('name','parent_folder')
def __init__(self, *args, **kwargs):
super(FolderForm, self).__init__(*args, **kwargs)
if hasattr(self, 'instance') and hasattr(self.instance, 'id'):
self.fields['parent_folder'].queryset = Folder.objects.exclude(id=self.instance.id)
To make sure the user does not select the same instance when filling in the foreign key field, implement a clean_FIELDNAME method in the admin form that rejects that bad value.
In this example, the model is Folder and the foreign key is parent_folder:
from django import forms
from django.contrib import admin
from .models import Folder
class FolderAdminForm(forms.ModelForm):
def clean_parent_folder(self):
if self.cleaned_data["parent_folder"] is None:
return None
if self.cleaned_data["parent_folder"].id == self.instance.id:
raise forms.ValidationError("Invalid parent folder, cannot be itself", code="invalid_parent_folder")
return self.cleaned_data["parent_folder"]
class FolderAdmin(admin.ModelAdmin):
form = FolderAdminForm
admin.site.register(Folder, FolderAdmin)
Edit: Combine my answer with raphv's answer for maximum effectiveness.

Django ORM m2m limit to 1

# models.py
class Gallery(models.Model):
images = models.ManyToManyField(Image, null=True, blank=True)
class Image(models.Model):
image = models.ImageField()
# views.py
class GalleryIndex(ListView):
model = Gallery
I need to get thumbnail for every gallery, which is it's very first/last/whatever image.
How can I LIMIT image by 1 for any gallery as a custom attribute (to not override Gallery.images) without calling second SQL query?
A many-to-many acts as a descriptor for a normal queryset, so you can do my_gallery.images.all()[0] to limit the query to 1.
I don't think I understand correctly what you want to do, but doesn't below code work for you?
class Gallery(models.Model):
images = models.ManyToManyField(Image, null=True, blank=True)
def get_thumb(self):
return self.images.all()[0]
Or maybe other concept:
class Gallery(models.Model):
images = models.ManyToManyField(Image, null=True, blank=True)
thumbnail = models.ImageField()
def save(self, *args, **kwargs):
self.thumbnail = self.images.all()[0].image
(some exception catching here needed though)
I should've read docs better. Standard QuerySet API can't handle such cases efficiently (annotate() generates GROUP BY clause for each of parent fields which is slow) so I come with extra() method and raw subquery.
class GalleryIndex(ListView):
queryset = Gallery.objects.extra(select={'thumb':
"""
SELECT "image"
FROM "app_image", "app_gallery_images"
WHERE (
"app_gallery"."id" = "app_gallery_images"."gallery_id"
AND "app_gallery_images"."image_id" = "app_image"."id"
)
AND "app_image"."image" IS NOT NULL
LIMIT 1
"""
})
This queryset do exactly what I wanted, since SorlImageField (and ImageField) needs only filename to represent thumbnail in templates.