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
Related
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()
If upload a file image.png from a web browser, a new file named image.png will appear in the upload directory on the server.
If I then upload another file named image.png (same name), a new file named image_aj642zm.png will appear in the upload directory on the server.
Then, if I upload another file named image.png (again the same name), a new file named image_z6z2BaQ.png will appear in the upload directory on the server.
What method does Django use to rename the uploaded file if a file with that name already exists in the upload directory?
(i.e. where does the extra _aj642zm and _z6z2BaQ come from?)
The usual set-up:
In models.py:
from django.db import models
class Image(models.Model):
image = models.ImageField(upload_to='uploads/')
In forms.py:
from django import forms
from .models import Image
class ImageForm(forms.ModelForm):
class Meta:
model = Image
fields = ['image']
In views.py:
from django.shortcuts import render, redirect
from .forms import ImageForm
def upload_image(request):
if request.method == 'POST':
form = ImageForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('index')
else:
form = ImageForm()
return render(request, 'upload_file.html', {'form': form})
Django default Storage class method called get_available_name
# If the filename already exists, add an underscore and a random 7
# character alphanumeric string (before the file extension, if one
# exists) to the filename until the generated filename doesn't exist.
# Truncate original name if required, so the new filename does not
# exceed the max_length.
Django by default saves object by its name but if object with that name already exists adds up underscore and 7 random chars as quoted in code comment
Also as addition to this Django Storage class method get_valid_name parses up file name before and replaces all spaces with underscores and removes all chars that are not unicode, alpha, dash, underscore or dot
re.sub(r'(?u)[^-\w.]', '', s)
I'm trying to add a search box for users on the webpage to see his profile, and if the user doesn't exist, then I have the option to create it.
In flask, I used a solution that used jquery for the autocomplete, and when no one was found, it would simply put "Create_user" as the text submitted in the form, and then redirect to the url for user creation. I was not able to port this to django(javascript is not my forté and I'm starting django.)
So I tried django-autocomplete-light, but while the autocomplete worked, I found no way to replicate the behavior that would redirect me to the user creation page in the case no one was found. (the create exemple in the docs only allow to create a simple entry, while I need to create a user based on a model)
Any leads on how to accomplish this with django?
That's what i was looking few days ago, i found this
Example Admin code for autocomplete
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django import forms
from selectable.forms import AutoCompleteSelectField, AutoCompleteSelectMultipleWidget
from .models import Fruit, Farm
from .lookups import FruitLookup, OwnerLookup
class FarmAdminForm(forms.ModelForm):
owner = AutoCompleteSelectField(lookup_class=OwnerLookup, allow_new=True)
class Meta(object):
model = Farm
widgets = {
'fruit': AutoCompleteSelectMultipleWidget(lookup_class=FruitLookup),
}
exclude = ('owner', )
def __init__(self, *args, **kwargs):
super(FarmAdminForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.pk and self.instance.owner:
self.initial['owner'] = self.instance.owner.pk
def save(self, *args, **kwargs):
owner = self.cleaned_data['owner']
if owner and not owner.pk:
owner = User.objects.create_user(username=owner.username, email='')
self.instance.owner = owner
return super(FarmAdminForm, self).save(*args, **kwargs)
class FarmAdmin(admin.ModelAdmin):
form = FarmAdminForm
admin.site.register(Farm, FarmAdmin)
Source code
https://github.com/mlavin/django-selectable
and
Documentation
http://django-selectable.readthedocs.org/en/latest/
Hope this will help you too
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 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.