This is my models.py:
class College(models.Model):
name = models.CharField(unique=True, max_length=50,
help_text='Name of the college.'
)
slug = models.SlugField(unique=True)
description = models.TextField(blank = True)
image = models.ImageField(upload_to='site-media/media/college_images/',
default = 'site-media/media/college_images/default.jpeg'
)
user = models.ForeignKey(User)
def get_absolute_url(self):
return "/%s/" % self.slug
def create_thumbnail(self):
if not self.image:
return
THUMBNAIL_SIZE = (250,193)
image = Image.open(StringIO(self.image.read()))
thumb = ImageOps.fit(image, THUMBNAIL_SIZE, Image.ANTIALIAS)
temp_handle = StringIO()
thumb.convert('RGB').save(temp_handle, 'jpeg')
temp_handle.seek(0)
suf = SimpleUploadedFile(os.path.split(self.image.name)[-1],
temp_handle.read(), content_type='image/jpeg')
self.image.save('%s_college_.%s'%(os.path.splitext(suf.name)[0],'jpeg'), suf, save=False)
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
self.create_thumbnail()
super(College, self).save(*args, **kwargs)
I have presented the user with a form to edit just the description. When the description 'POST' is made the 'save()' method above is called. The problem with this is that the thumbnail is created over and over again with a bigger name every time. And, also the previous thumbnail is not deleted from the hard disk. Is it possible, that this 'thumbnail' method doesn't get called over and over again with each edit of the 'description'.
You can check whether you are sending image file in you request post or not. For this You need to call your save in view with one argument request like : college.save(request)
def save(self, request=False, *args, **kwargs):
self.slug = slugify(self.name)
if request and request.FILES.get('image',False):
self.create_thumbnail()
super(College, self).save(*args, **kwargs)
OR
you can differentiate your save and edit using
if self.pk is not None
But it can create problem if you edit your image.
So its your choice How you want to go with it.
There are two reasonable paths I see to handle this. Neither are ideal, so I'll be interested to see if anyone has a better option to offer.
One is to save the filename of the most recent image for which you created a thumbnail as a model field. Then in your save method you can check the filename of your image field against it and create a new thumbmail if it has changed. This has the disadvantage of requiring a new model field, but is more universal in its application.
The other is to override the save method of the form class. You can then check the old image filename by looking at self.instance.image and comparing that against self.cleaned_data['image']. This has the disadvantage of only affecting views that use that form class, but doesn't require changing your data model. You can pass a custom form to the admin, if you're using it, or override the admin class's save_model method.
The simple solution is to NOT try to create the thumbnail at this stage, but only when it's needed, ie (pseudocode example):
class Whatever(models.Model):
image = models.ImageField(...)
#property
def thumbnail(self):
thumb = do_i_have_a_thumbnail_yet(self.image)
if not thumb:
thumb = ok_then_make_me_a_thumbnail_and_store_it_please(self.image)
return thumb
Implementing do_i_have_a_thumbnail_yet() and ok_then_make_me_a_thumbnail_and_store_it_please() is left as an exercise to the reader, but there are librairies or django apps providing such services.
Another - and even better imho - solution is to delegate thumbnails handling to a templatetag or template filter. Why ? because
thumbnails are mostly presentation stuff and do not belong to the model
most of the time you'll need different thumbnails sizes from a same image
the front-end developer may want to be free to change the thumbnail's size without having to ask you to change your backend code and write a migration script to recompute all existing thumbnails.
Related
I am building a web page where a blog author can write content and upload images.
Using an image field, I am allowing the author to upload multiple images and with some Javascript logic, I am displaying the images before upload. Under each image, I have a checkbox that indicates if that is the main image of the post (e.g. the first to show in the slideshow and used as thumbnail of the post itself).
On this page I am showing two forms with a shared submit button and it works fine.
My problems begin when I try to save the image and also indicate if it is the main one.
My images model has multiple helpful methods, thumbnail property for Django admin and a save method that will set all images for a post to false, before editing/adding a new main_image. I had to include a commit argument logic because of my experiments. Please ignore it. The model includes 3 fields: the image, the main flag and a foreign key to the post as follows:
class PostImages(models.Model):
"""
Post images for the blog app.
"""
image = models.ImageField(
verbose_name=_("Изображение"),
upload_to='blog/images/',
blank=False,
null=False,
default='blog/images/default.jpg'
)
post = models.ForeignKey(
to=Post,
on_delete=models.CASCADE,
verbose_name=_('Статия')
)
main_image = models.BooleanField(
default=False,
verbose_name=_('Основно изображение')
)
class Meta:
verbose_name = _('Изображение')
verbose_name_plural = _('Изображения')
permissions = [
('can_manage_post_image', _('Може да управлява изображенията на публикациите')),
('can_add_post_image', _('Може да добавя изображения към публикациите')),
]
def __str__(self):
return f"{self.post.title} - {self.image}"
def get_absolute_url(self):
return f"/blog/post/image/{self.id}"
def get_edit_url(self):
return f"/blog/post/image/{self.id}/edit"
def get_delete_url(self):
return f"/blog/post/image/{self.id}/delete"
def get_image(self):
return mark_safe(f'<img src="{self.image.url}" width="100" height="100" />')
def get_image_url(self):
return self.image.url
#property
def thumbnail_preview(self):
if self.image:
return mark_safe('<img src="{}" width="400" height="auto" />'.format(self.image.url))
return ''
def save(self, *args, **kwargs):
if self.main_image and kwargs.get("commit", True):
PostImages.objects.filter(post=self.post).update(main_image=False)
kwargs.pop("commit", None)
super().save(*args, **kwargs)
I am also using a Model form. In the model form I do not ask for the post, because it is not yet created. My idea was to fill the post in the save phase following a process of:
save post
save images
For me this is a logical move, but knowing how much time it costed me so far -> probably not the best move.
class PostImagesForm(forms.ModelForm):
"""
Form for uploading images for posts.
"""
image = forms.ImageField(
label='Снимка',
widget=forms.ClearableFileInput(
attrs={
'class': 'form-control',
'multiple': True,
}
)
)
main_image = forms.BooleanField(
label='Основна снимка',
required=False,
widget=forms.CheckboxInput(
# attrs={
# 'class': 'form-control',
# }
)
)
class Meta:
model = PostImages
fields = ['image', 'main_image']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['image'].required = False
def save(self, commit=True):
"""
Save the form and return the created or edited post.
"""
post_image = super().save(commit=False)
post_image.save(commit=commit)
return post_image
def clean(self):
"""
Clean the form and return the cleaned data.
"""
cleaned_data = super().clean()
return cleaned_data
So far, so .... good I assume.
So here are the questions:
How do I tell Django that I am uploading a pair of fields (file and checkbox)?
How to pass the post instance to the image?
Why is my request files empty?
How am I supposed to group the image-checkbox pairs? Is it something inside the html??
I tried to use the form itself, but it misses the post instance.
I tried to iterate through the files but they are empty.
I tried to iterate through the form fields, but I do not see all uploaded files.
If someone ever needs this:
I found multiple problems that I think might be useful to share.
I am using multiple forms under one submit button. At some point I played myself and deleted the enctype="multipart/form-data" from my form tag. (oops)
My javascript had a strange behaviour because it was pumping the image as "data:sjdfhsdkjbfksd" in my identifier for the checkbox. So again a rookie mistake. I detected it when I started slapping console.log() to everything an anything.
If you are like me (main language is Python and JS is something accidentally known), remember - there is a fantastic debugger for JS in the browser, under sources.
JS scopes are strange and they are not like in Python. While using the debugger, you will see that the loops doing stuff and calling event functions will always do everything outside event functions and then move to your event function. You can pass stuff by reassigning in var statements, but be ready to be screamed at by a JS guy.
I am building a Django app that saves an .stl file passed using a formulary and my goal is to open the file, extract some information with a script that is already tested, and save this information in the same register that the file.
I am doing this:
from stl import mesh # numpy-stl library
def informationGeneration(stl_route, *args, **kwargs):
# scripts that generates the information
myMesh = mesh.Mesh.from_file(stl_route) # here the error appears
return myMesh.areas.shape[0]
class Piece(models.Model):
"""Piece model."""
# ...
file = models.FileField(upload_to='pieces/files', default='NA')
# ...
information = models.IntegerField(default=0)
def save(self, *args, **kwargs):
"""Overriding the save method."""
self.information = informationGeneration(self.file)
super().save(*args, **kwargs)
def __str__(self):
# ...
The problem is that when I try to save a new instance, numpy-stl detects an error, self.file is not the .stl file, is an alement of the formulary.
Then, I use a form:
class PieceForm(forms.ModelForm):
"""Pieces model form."""
class Meta:
"""Form settings."""
model = Piece
fields = ('file')
How can I pass the file and not the route?
Piece.file is not a path, it's a models.FileField. To get the path, you have to use self.file.path.
Just beware that if there's actually no file for this field, self.file.path will raise an exception (ValueError, "the file attribute has no file associated with it"), so it's better to test before. models.FileField have a false value in a boolean context, so you want:
if self.file:
self.information = informationGeneration(self.file.path)
A couple notes:
1/ a function is an action, so it's name should be a verb (ie "extract_informations")
2/ you probably don't want to re-parse the file's content each and every time your object is saved, only when the file has changed. You can use a md5sum (stored in the model) to check this.
3/ I have not double-checked but I really dont think you should use a default for this field - if you want to make it optional, use blank=True and null=True.
This is my model and I want to limit the number of photos that a user can upload just 10. I want to do it one place so it works in the admin and user facing forms. Can someone help me out here?
class StarPhotos(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
PHOTO_CATEGORY = (
('HS', "Head Shot"),
('WP', "Western Party Wear"),
('IP', "Indian Party Wear"),
('SW', "Swim Wear"),
('CW', "Casual Wear"),
)
category = models.CharField(max_length=2, choices=PHOTO_CATEGORY, default='CW')
# This FileField should preferaby be changed to ImageField with pillow installed.
photos = models.FileField(max_length=200, upload_to='images/',)
def __str__(self):
return "Images for {0}".format(self.user)
You can use a checker function in your model to check whether User has uploaded 10 photos or not
def check_photo_count(self, user):
photo_count = self.object.filter(user=user).count()
return photo_count
This does not seem to be the best solution available but it should work.
Also remember to put a check for this in your views or admin. You can also return a Boolean value from this function saying that this user is allowed to upload more photos or not.
This same logic can be applied to manager if you don't want to put checks everywhere.
So you just have to put this check in the create method and if the check fails simply raise a error or return a false value saying that object is not created.
You can override save and bulk_create methods from StarPhotos, I don't check the code, but it's some like that:
class CheckPhotoModelManager(models.Manager):
def bulk_create(self, objs, batch_size=None):
photos = StarPhotos.object.filter(user=objs[0].user).count()
if photos < 10:
super(StarPhotos, self).bulk_create(...)
class StarPhotos(models.Model):
objects = CheckPhotoModelManager()
def save(self, *args, **kwargs):
photos = StarPhotos.object.filter(user=self.user).count()
if photos < 10:
super(StarPhotos, self).save(*args, **kwargs)
Please forgive my naiveté with Django.
I want to create my own method which works much like prepopulated_fields for my custom admin page. Basically, when you put the url of an image in one field, I'd like to populate another field with the name of the image, height and width via javascript.
What would be the best approach? Just override the change_form.html and include my own JS lib? Write a custom widget?
Using Javascript would be a probable move, if for some reason you want to show the attributes of the image.
See Javascript - Get Image height for an example of doing this.
If there's no need to show it at the form level but to simply populate the usually prefer to do this at the model level, such as
from PIL import Image
import StringIO
import urllib2
class MyModel(models.Model):
# ... fields 1,2,3 etc, and assuming the url field is called image_url
def pre_save():
# obtain attributes of image from url field
# save it to various fields
img = urllib2.urlopen(self.image_url).read()
im = Image.open(StringIO.StringIO(img))
self.image_width, self.image_height = im.size
def save(self, *args, **kwargs):
self.pre_save()
super(MyModel, self).save(*args, **kwargs)
Good luck!
In Django, if you have a ImageFile in a model, deleting will remove the associated file from disk as well as removing the record from the database.
Shouldn't replacing an image also remove the unneeded file from disk? Instead, I see that it keeps the original and adds the replacement.
Now deleting the object won't delete the original file only the replacement.
Are there any good strategies to doing this? I don't want to have a bunch of orphan files if my users replace their images frequently.
The best strategy I've found is to make a custom save method in the model:
class Photo(models.Model):
image = ImageField(...) # works with FileField also
def save(self, *args, **kwargs):
# delete old file when replacing by updating the file
try:
this = Photo.objects.get(id=self.id)
if this.image != self.image:
this.image.delete(save=False)
except: pass # when new photo then we do nothing, normal case
super(Photo, self).save(*args, **kwargs)
And beware, as with the updating which doesn't delete the back end file, deleting an instance model (here Photo) will not delete the back-end file, not in Django 1.3 anyway, you'll have to add more custom code to do that (or regularly do some dirty cron job).
Finally test all your update/delete cases with your ForeignKey, ManytoMany and others relations to check if the back-end files are correctly deleted. Believe only what you test.
Shouldn't replacing an image also remove the unneeded file from disk?
In the olden days, FileField was eager to clean up orphaned files. But that changed in Django 1.2:
In earlier Django versions, when a model instance containing a FileField was deleted, FileField took it upon itself to also delete the file from the backend storage. This opened the door to several potentially serious data-loss scenarios, including rolled-back transactions and fields on different models referencing the same file. In Django 1.2.5, FileField will never delete files from the backend storage.
The code in the following working example will, upon uploading an image in an ImageField, detect if a file with the same name exists, and in that case, delete that file before storing the new one.
It could easily be modified so that it deletes the old file regardless of the filename. But that's not what I wanted in my project.
Add the following class:
from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
def _save(self, name, content):
if self.exists(name):
self.delete(name)
return super(OverwriteStorage, self)._save(name, content)
def get_available_name(self, name):
return name
And use it with ImageField like so:
class MyModel(models.Model):
myfield = models.ImageField(
'description of purpose',
upload_to='folder_name',
storage=OverwriteStorage(), ### using OverwriteStorage here
max_length=500,
null=True,
blank=True,
height_field='height',
width_field='width'
)
height = models.IntegerField(blank=True, null=True)
width = models.IntegerField(blank=True, null=True)
If you don't use transactions or you don't afraid of loosing files on transaction rollback, you can use django-cleanup
There have been a number of tickets regarding this issue though it is likely this will not make it into the core. The most comprehensive is http://code.djangoproject.com/ticket/11663. The patches and ticket comments can give you some direction if you are looking for a solution.
You can also consider using a different StorageBackend such as the Overwrite File Storage System given by Django snippet 976. http://djangosnippets.org/snippets/976/. You can change your default storage to this backend or you can override it on each FileField/ImageField declaration.
Here is a code that can work with or without upload_to=... or blank=True, and when the submitted file has the same name as the old one.
(py3 syntax, tested on Django 1.7)
class Attachment(models.Model):
document = models.FileField(...) # or ImageField
def delete(self, *args, **kwargs):
self.document.delete(save=False)
super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
if self.pk:
old = self.__class__._default_manager.get(pk=self.pk)
if old.document.name and (not self.document._committed or not self.document.name):
old.document.delete(save=False)
super().save(*args, **kwargs)
Remember that this kind of solution is only applicable if you are in a non transactional context (no rollback, because the file is definitively lost)
I used a simple method with popen, so when i save my Info model i delete the former file before linking to the new:
import os
try:
os.popen("rm %s" % str(info.photo.path))
except:
#deal with error
pass
info.photo = nd['photo']
I save the original file and if it has changed - delete it.
class Document(models.Model):
document = FileField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._document = self.document
def save(self, *args, **kwargs):
if self.document != self._document:
self._document.delete()
super().save(*args, **kwargs)