Django helper function to rename user uploaded image - django

For a django project I have a model that features an image field. The idea is that the user uploads an image and django renames it according to a chosen pattern before storing it in the media folder.
To achieve that I have created a helper class in a separate file utils.py. The class will callable within the upload_to parameter of the django ImageField model. Images should be renamed by concatenating .name and .id properties of the created item. The problem with my solution is that if the image is uploaded upon creating anew the item object, then there is no .id value to use (just yet). So instead of having let's say: banana_12.jpg I get banana_None.jpg. If instead I upload an image on an already existing item object, then the image is renamed correctly.
Here is my solution. How can I improve the code to make it work on new item object too?
Is there a better way to do this?
# utils.py
from django.utils.deconstruct import deconstructible
import os
#deconstructible
class RenameImage(object):
"""Renames a given image according to pattern"""
def __call__(self, instance, filename):
"""Sets the name of an uploaded image"""
self.name, self.extension = os.path.splitext(filename)
self.folder = instance.__class__.__name__.lower()
image_name = f"{instance}_{instance.id}{self.extension}"
return os.path.join(self.folder, image_name)
rename_image = RenameImage()
# models.py
from .utils import rename_image
class Item(models.Model):
# my model for the Item object
name = models.CharField(max_length=30)
info = models.CharField(max_length=250, blank=True)
image = models.ImageField(upload_to=rename_image, blank=True, null=True) # <-- helper called here
# ...
I created the helper class RenameImage which for the reasons explained above works correctly only if the object already exists.
__EDIT
I have made some improvements based on the post_save advice. The user uploaded image gets resized and renamed as I want but then I get a RecursionError: maximum recursion depth exceeded. It seems that by calling the save() method on the instance I get into a signal loop...
Any idea on how to resolve this?
from django.db.models.signals import post_save
from .models import Item
from PIL import Image
import os
def resize_rename(sender, instance, created, max_height=300, max_width=300, **kwargs):
if instance.image: # only fire if there is an image
with open(instance.image.path, 'r+b') as f:
image = Image.open(f)
# Resize the image if larger than max values
if image.height > max_height or image.width > max_width:
output_size = (max_height, max_width)
image.thumbnail(output_size, Image.ANTIALIAS)
image.save(instance.image.path, quality=100)
# Grab the image extension
_name, extension = os.path.splitext(instance.image.name)
# Use old_name to create the desired new_name while keeping the same dirs
old_name = instance.image.name.split('/')[-1] # e.g. IMG_3402.jpg
new_name = instance.image.name.replace(old_name, f"{instance.name}_{instance.id}{extension}")
# Rename if name is not the right one already
if old_name != new_name:
old_path = instance.image.path
new_path = old_path.replace(old_name, f"{instance.name}_{instance.id}{extension}")
instance.image.name = new_name # Assign the new name
os.replace(old_path, new_path) # Replace with the new file
instance.save(update_fields=['image']) # Save the instance
post_save.connect(resize_rename, sender=Item)

It is not possible because the id is assigned after saving.
You have to use a post_save signal and then change the filename
To do this add signal.py under app
In the apps.py file, override the ready function to add the signals.py import statement
apps.py
from django.apps import AppConfig
class AppNameConfig(AppConfig):
name = 'app_name'
def ready(self):
import app_name.signals
On init.py set default app config
init.py
default_app_config = 'app_name.apps.AppNameConfig'
signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Item
#receiver(post_save, sender=Item)
def post_save(sender, instance: FilettoPasso, **kwargs):
if kwargs['created']:
print(instance.id)
instance.image.path = new path
# rename file in os path
...
instance.save()

Related

How to use pre_create_historical_record signal in django-simple-history?

I am using django-simple-history to save history of data. I want to save an extra field value to each history model before it is saved. I found the reference code in documentation mentioned above but cant use it. Please help.
from django.dispatch import receiver
from simple_history.signals import (
pre_create_historical_record,
post_create_historical_record
)
#receiver(pre_create_historical_record)
def pre_create_historical_record_callback(sender, **kwargs):
print("Sent before saving historical record")
#receiver(post_create_historical_record)
def post_create_historical_record_callback(sender, **kwargs):
print("Sent after saving historical record")
apps.py file
from django.apps import AppConfig
class LogAppConfig(AppConfig):
name = 'log_app'
def ready(self):
import log_app.signals
signals.py file
from simple_history.signals import (pre_create_historical_record, post_create_historical_record)
#receiver(pre_create_historical_record)
def pre_create_historical_record_callback(sender, **kwargs):
print("signal is running")
history_instance = kwargs['history_instance']

How do I automatically delete images in Django media file when I delete the objects in Admin?

please help!
I am using Django 3 and have a basic blog with a title, description and image for each entry.
In Django admin I can delete each blog post however, when I look in my media file in Atom all the images are still there and I have to manually delete them...
how do I get them to auto delete when I delete them in Admin?
Model.py
from django.db import models
class Blog(models.Model):
title = models.CharField(max_length=100)
description = models.TextField()
image = models.ImageField(upload_to='images/', default='images/road.jpg')
last_modified = models.DateField(auto_now=True)
Views.py
from django.shortcuts import render
from .models import Blog
def home (request):
blogs = Blog.objects.all()
return render (request,'blog/home.html', {'blogs':blogs})
You can use like this:
blog = Blog.objects.get(pk=1)
if blog.image:
if os.path.isfile(blog.image.path):
os.remove(blog.image.path)
Or You Can Use signlas:
from django.db.models.signals import pre_delete
from django.dispatch.dispatcher import receiver
#receiver(pre_delete, sender=Blog)
def mymodel_delete(sender, instance, **kwargs):
# Pass false so FileField doesn't save the model.
instance.file.delete(False)
Use django-cleanup app for automatically remove the media when you delete your objects.

Can't dynamically create path with upload_to

In my user model, I have an ImageField that contains an upload_to attribute that will create the image path dynamically based on the user's id.
avatar = models.ImageField(storage=OverwriteStorage(), upload_to=create_user_image_path)
def create_user_image_path(instance, image_name):
image_name = str(instance.user.id) + image_name[-4:]
return 'users/{0}/avatars/{1}'.format(instance.user.id, image_name)
When I sign up, I get an error that looks like this:
The system cannot find the path specified:
'C:\Users\xx\PycharmProjects\project_name\media\users\150\avatars'
If I remove the id and avatars and just include the image name (with no new directories) it works successfully and writes the image. I have tried doing chmod -R 777 on this directory, but it still doesn't create these new directories dynamically. I'm not sure what I'm doing wrong.
EDIT
class OverwriteStorage(FileSystemStorage):
def get_available_name(self, name, *args, **kwargs):
# Delete all avatars in directory before adding new avatar.
# (Sometimes we have different extension names, so we can't delete by name
file_path = os.path.dirname(name)
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, file_path))
return name
The problem I see is because of my OverwriteStorage function. When I remove it, it saves. How can I fix this function so that it overwrites if it exists?
I think your problem id with instance.user.id
I would suggest you try to import the User as below it may help
from django.contrib.auth.models import User
user = User.objects.get(id=user_id)
def create_user_image_path(instance, image_name):
image_name = str(user) + image_name[-4:]
return 'users/{0}/avatars/{1}'.format(user.id, image_name)
However django recommends to user AUTH_USER_MODEL
implementation
from django.contrib.auth import get_user_model
User = get_user_model()
Update
When you look into override storage, the variable name gets the name of the image you are about to upload. Your file_path = os.path.dirname(name), you are returning a folder with image name. When shutil.rmtree ties to find the folder, it returns error it cannot find the path. Use conditional statement i.e. if or try except block as shown below.
from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.core.files.storage import default_storage
import os
class OverwriteStorage(FileSystemStorage):
def get_available_name(self, name, max_length=None):
"""Returns a filename that's free on the target storage system, and
available for new content to be written to.
Found at http://djangosnippets.org/snippets/976/
This file storage solves overwrite on upload problem. Another
proposed solution was to override the save method on the model
like so (from https://code.djangoproject.com/ticket/11663):
def save(self, *args, **kwargs):
try:
this = MyModelName.objects.get(id=self.id)
if this.MyImageFieldName != self.MyImageFieldName:
this.MyImageFieldName.delete()
except: pass
super(MyModelName, self).save(*args, **kwargs)
"""
# If the filename already exists, remove it as if it was a true file system
if self.exists(name):
os.remove(os.path.join(settings.MEDIA_ROOT, name))
# default_storage.delete(os.path.join(settings.MEDIA_ROOT, name))
return name
Like in your case
class OverwriteStorage(FileSystemStorage):
def get_available_name(self, name, *args, **kwargs):
# Delete all avatars in directory before adding new avatar.
# (Sometimes we have different extension names, so we can't delete by name
if self.exists(name):
file_path = os.path.dirname(name)
shutil.rmtree(os.path.join(settings.MEDIA_ROOT, file_path))
return name

Django: change admin FileField output?

I've made a model with file that is uploaded to custom path (not in MEDIA_ROOT). So it's some kind like protected file.
Now I need to change it's representation in admin details. It shows a path relative to MEDIA_URL. I need to change that, to show a URL to an application view which generates a proper URL.
So, what is the best way to display link, and only in objects details in admin?
Here is the way I did it:
models.py
class SecureFile(models.Model):
upload_storage = FileSystemStorage(
location=settings.ABS_DIR('secure_file/files/'))
secure_file = models.FileField(verbose_name=_(u'file'),
upload_to='images', storage=upload_storage)
widgets.py
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
class AdminFileWidget(forms.FileInput):
"""A FileField Widget that shows secure file link"""
def __init__(self, attrs={}):
super(AdminFileWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
output = []
if value and hasattr(value, "url"):
url = reverse('secure_file:get_secure_file',
args=(value.instance.slug, ))
out = u'{}<br />{} '
output.append(out.format(url, _(u'Download'), _(u'Change:')))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
admin.py
class SecureFileAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SecureFileAdminForm, self).__init__(*args, **kwargs)
self.fields['secure_file'].widget = AdminFileWidget()
class Meta:
model = SecureFile
class SecureFileAdmin(admin.ModelAdmin):
form = SecureFileAdminForm

Mongoengine FileField saving to disk?

Mongoengine stores FileField and ImageField to GridFS. What's the easiest approach to replicate the functionality of the original File/Image Field?
EDIT:
So this is the class I have in place at the moment. I'm able to load files and save them to disk, Mongo holds the path to the file in database.
I'm falling over on "to_python" as I believe it needs to create an object of the proxy_class but I can't see how, if all I'm getting is a path to the file (as the value passed in).
import os
import datetime
from mongoengine.python_support import str_types
from django.db.models.fields.files import FieldFile
from django.core.files.base import File
from django.core.files.storage import default_storage
from mongoengine.base import BaseField
from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME
from django.utils.encoding import force_text
#from django.utils.encoding import force_str
class DJFileField(BaseField):
proxy_class = FieldFile
def __init__(self,
db_alias=DEFAULT_CONNECTION_NAME,
name=None,
upload_to='',
storage=None,
**kwargs):
self.db_alias = db_alias
self.storage = storage or default_storage
self.upload_to = upload_to
if callable(upload_to):
self.generate_filename = upload_to
super(DJFileField, self).__init__(**kwargs)
def __get__(self, instance, owner):
# Lots of information on whats going on here can be found
# on Django's FieldFile implementation, go over to GitHub to
# read it.
file = instance._data.get(self.name)
if isinstance(file, str_types) or file is None:
attr = self.proxy_class(instance, self, file)
instance._data[self.name] = attr
elif isinstance(file, File) and not isinstance(file, FieldFile):
file_copy = self.proxy_class(instance, self, file.name)
file_copy.file = file
file_copy._committed = False
instance._data[self.name] = file_copy
elif isinstance(file, FieldFile) and not hasattr(file, 'field'):
file.instance = instance
file.field = self
file.storage = self.storage
# That was fun, wasn't it?
return instance._data[self.name]
def __set__(self, instance, value):
instance._data[self.name] = value
# The 3 methods below get used by the FieldFile proxy_object
def get_directory_name(self):
return os.path.normpath(force_text(datetime.datetime.now().strftime(self.upload_to)))
def get_filename(self, filename):
return os.path.normpath(self.storage.get_valid_name(os.path.basename(filename)))
def generate_filename(self, instance, filename):
return os.path.join(self.get_directory_name(), self.get_filename(filename))
def to_mongo(self, value):
# Store the path in MongoDB
# I also used this bit to actually save the file to disk.
# The value I'm getting here is a FileFiled and it all looks
# pretty good at this stage even though I'm not 100% sure
# of what's going on.
import ipdb; ipdb.set_trace()
if not value._committed and value is not None:
value.save(value.name, value)
return value.path
return value.path
def to_python(self, value):
# Now this is the real problem, value is the path that got saved
# in mongo. No idea how to return a FileField obj from here.
# self.instance and instance throw errors.
I think it would be a good addition - maybe called LocalFileField to make it more framework agnostic and if you provided tests and docs it would make a great addition to https://github.com/MongoEngine/extras-mongoengine
The only reason I'm not sold on having it in core - is if you are running a replicaset the file would still only be stored on one node.