I'm having a problem setting a dynamic path to my imageField.
This is my models.py
class Imagen(models.Model):
def get_upload_path(self, filename):
return os.path.join(
"img/experiencias/experiencia_%d" % self.id_experiencia.id, 'ficha' + '_' + filename)
nombre = models.CharField(max_length=50)
id_experiencia = models.ForeignKey(TipoExperiencia)
imagen = models.ImageField(upload_to= get_upload_path)
caption = models.CharField(max_length=150,blank=True)
alt = models.CharField(max_length=100)
This is a solution that I've found here
This actually works fine when updating objects, but when I try to insert new elements, the inserts fail because in that moment self does not exists.
I've tried another solution here whose proposal is overriding the ImageField method to customize upload_to.
The problem is that I use South and it's quite difficult to manage custom fields
I use Django 1.5. I would like to know if exists any easy way to manage dynamic file path in django
Thanks
Alternatively you can override the save method to move the file to the correct path.
class Model(models.Model):
def save(self, *args, **kwargs):
instance = super(Model, self).save(*args, **kwargs)
# your logic here to change the file location
return instance
I think you can get away here with Unipath.
Unipath usage
Related
I want to create a model Changelog and make it editable from Admin page. Here is how it is defined in models.py:
class Changelog(models.Model):
id = models.AutoField(primary_key=True, auto_created=True)
title = models.TextField()
description = models.TextField()
link = models.TextField(null=True, blank=True)
picture = models.BinaryField(null=True, blank=True)
title and description are required, link and picture are optional. I wanted to keep this model as simple as possible, so I chose BinaryField over FileField. In this case I wouldn't need to worry about separate folder I need to backup, because DB will be self-contained (I don't need to store filename or any other attributes, just image content).
I quickly realized, that Django Admin doesn't have a widget for BinaryField, so I tried to use widget for FileField. Here is what I did to accomplish that (admin.py):
class ChangelogForm(forms.ModelForm):
picture = forms.FileField(required=False)
def save(self, commit=True):
if self.cleaned_data.get('picture') is not None:
data = self.cleaned_data['picture'].file.read()
self.instance.picture = data
return self.instance
def save_m2m(self):
# FIXME: this function is required by ModelAdmin, otherwise save process will fail
pass
class Meta:
model = Changelog
fields = ['title', 'description', 'link', 'picture']
class ChangelogAdmin(admin.ModelAdmin):
form = ChangelogForm
admin.site.register(Changelog, ChangelogAdmin)
As you can see it is a bit hacky. You also can create you own form field be subclassing forms.FileField, but code would be pretty much the same. It is working fine for me, but now I'm thinking is there are better/standard way to accomplish the same task?
A better and more standard way would be to create a Widget for this type of field.
class BinaryFileInput(forms.ClearableFileInput):
def is_initial(self, value):
"""
Return whether value is considered to be initial value.
"""
return bool(value)
def format_value(self, value):
"""Format the size of the value in the db.
We can't render it's name or url, but we'd like to give some information
as to wether this file is not empty/corrupt.
"""
if self.is_initial(value):
return f'{len(value)} bytes'
def value_from_datadict(self, data, files, name):
"""Return the file contents so they can be put in the db."""
upload = super().value_from_datadict(data, files, name)
if upload:
return upload.read()
So instead of subclassing the whole form you would just use the widget where it's needed, e.g. in the following way:
class MyModelAdmin(admin.ModelAdmin):
formfield_overrides = {
models.BinaryField: {'widget': BinaryFileInput()},
}
As you already noticed the code is much the same but this is the right place to put one field to be handled in a specific manner. Effectively you want to change one field's appearance and the way of handling when used in a form, while you don't need to change the whole form.
Update
Since writing that response Django has introduced an editable field on the models and in order to get this working you need to set the model field to editable=True which is false for BinaryField by default.
An alternative solution is to create a file storage backend that actually saves the binary data, along with file name and type, in a BinaryField in the database. This allows you to stay within the paradigm of FileField, and have the metadata if you need it.
This would be a lot more work if you had to do it yourself, but it is already done in the form of db_file_storage.
I actually followed #Ania's answer with some workaround since upload.read() was not saving image in the right encoding in Postrgres and image could not be rendered in an HTML template.
Furthermore, re-saving the object will clear the binary field due to None value in the uploading field (Change) [this is something that Django handles only for ImageField and FileField]
Finally, the clear checkbox was not properly working (data were deleted just because of the previous point, i.e. None in Change).
Here how I changed value_from_datadict() method to solve:
forms.py
class BinaryFileInput(forms.ClearableFileInput):
# omitted
def value_from_datadict(self, data, files, name):
"""Return the file contents so they can be put in the db."""
#print(data)
if 'image-clear' in data:
return None
else:
upload = super().value_from_datadict(data, files, name)
if upload:
binary_file_data = upload.read()
image_data = base64.b64encode(binary_file_data).decode('utf-8')
return image_data
else:
if YourObject.objects.filter(pk=data['pk']).exists():
return YourObject.objects.get(pk=data['pk']).get_image
else:
return None
Then I defined the field as a BinaryField() in models and retrieved the image data for the frontend with a #property:
models.py
image = models.BinaryField(verbose_name='Image', blank = True, null = True, editable=True) # editable in admin
#property
def get_image(self):
'''
store the image in Postgres as encoded string
then display that image in template using
<img src="data:image/<IMAGE_TYPE>;base64,<BASE64_ENCODED_IMAGE>">
'''
image_data = base64.b64encode(self.image).decode('utf-8')
return image_data
And finally is rendered in the template with:
yourtemplate.html
<img src="data:image/jpg;base64,{{object.get_image}}" alt="photo">
For modern Django, I found the following approach works best for me:
class BinaryField(forms.FileField):
def to_python(self, data):
data = super().to_python(data)
if data:
data = base64.b64encode(data.read()).decode('ascii')
return data
class BinaryFileInputAdmin(admin.ModelAdmin):
formfield_overrides = {
models.BinaryField: {'form_class': BinaryField},
}
The individual model field still needs editable=True, of course.
I'm trying to create a folder and upload an image to in it. My model is similar to this:
class XYZ(models.Model):
def code(self):
syms = ['#','#','$','%','&','*','+','-']
return ''.join(x+random.choice(syms) for x in [self.name[-2:],self.name[2:]])
def make_folder(self):
os.mkdir(os.getcwd()+'/XYZ/'+folder)
def save(self):
self.make_folder()
super(XYZ,self).save(*args,**kwargs)
name = models.CharField(max_length=20)
folder = property(code)
image = models.ImageField(upload_to='HELP_HERE')
I've tried using folder, self.folder, property(code) but it says it isn't defined, how do I access this attribute? Is this the correct approach?
I've also set in the configs.py my MEDIA_URL and MEDIA_ROOT
I think a more simple and understandable way to do this would be to avoid using the inner class function and doing something like this:
# Reference this function outside of your model class.
def xyz_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/xyz_<id>/<filename>
return 'xyz_{0}/{1}'.format(instance.id, filename)
Then, on your models, reference the function in your upload_to field like so:
class XYZ(models.Model):
image = models.ImageField(upload_to=xyz_directory_path)
With your media config set up, this should work. There's a really nice and simple explanation about file handling here.
I need to get ImageField name in upload_path function.
I tried use partial in ImageField definition:
class MyModel(models.Model):
image = models.ImageField(
upload_to=partial(image_upload_path, 'image')
)
Now I can get that string by first argument of function:
def image_upload_path(field, instance, filename):
....
All works fine, but now Django generate migration file, each time I use makemigrations, with same operations list in it:
operations = [
migrations.AlterField(
model_name='genericimage',
name='image',
field=core_apps.generic_image.fields.SorlImageField(upload_to=functools.partial(core_apps.generic_image.path.image_upload_path, *('image',), **{}),),
),
]
Maybe there is another way to access Field name in upload_path function or somehow I can fix my solution?
It seems like you don't need to provide a partial in this case, but just a callable with two parameters like in this example in the Django documentation.
Django will invoke the callable you provide in the upload_to argument with 2 parameters (instance and filename).
instance:
An instance of the model where the FileField is defined. More specifically, this is the particular instance where the current file is being attached.
This means you can access the name field of the instance like instance.name in the callable you write:
class MyModel(models.Model):
name = models.CharField(max_length=255)
image = models.ImageField(upload_to=image_upload_path)
def image_upload_path(instance, filename):
# Access the value of the `name` field
# of the MyModel instance passed in and save it to a variable:
name = instance.name
# Code that returns a Unix-style path (with forward slashes) goes here
I decide to build my own field:
class SorlImageField(ImageField):
def __init__(self, verbose_name=None, name=None, width_field=None,
height_field=None, lookup_name=None, **kwargs):
self.lookup_name = lookup_name
kwargs['upload_to'] = partial(image_upload_path, lookup_name)
super(SorlImageField, self).__init__(verbose_name, name,
width_field, height_field, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(SorlImageField, self).deconstruct()
del kwargs['upload_to']
# del upload_to will solve migration issue
return name, path, args, kwargs
def check(self, **kwargs):
errors = super(SorlImageField, self).check(**kwargs)
if self.lookup_name != self.name:
error = [
checks.Error(
'SorlImageField lookup_name must be equal to '
'field name, now it is: "{}"'.format(self.lookup_name),
hint='Add lookup_name in SorlImageField',
obj=self,
id='fields.E210',
)]
errors.extend(error)
return errors
Problem with migration was solved in deconstruct method, by deleting upload_to argument. Also I add additional argument into __init__ which point to field name, check function check for correct lookup_name value. If it not, it will raise an error when migrations starts.
class MyModel(models.Model):
image = SorlImageField(
lookup_name='image'
)
I am trying to create a form where users will be allowed to upload any image file + SWF files. Django's ImageField does not support SWF so I need to override it.
What I want to do is to check if the file is SWF, if True, return it. If it's not SWF, call the original method which will take care of the file validation.
However, I am not sure how to implement that. Here is an example of what I am trying to achieve, but it does not work:
from hexagonit import swfheader
class SwfImageField(forms.ImageField):
def to_python(self, data):
try:
swfheader.parse(data)
return data
except:
return super(SwfImageField, self).to_python(data)
What is actually does is allowing only SWF files at the moment.
An alternative and possibly easiest solution is to use a standard FileField with a custom validator:
def my_validate(value):
ext = os.path.splitext(value.name)[1] # [0] returns path filename
valid = ['.jpg', '.swf']
if ext not in valid:
raise ValidationError("Unsupported file extension.")
class MyForm(forms.Form):
file = forms.FileField(validators=[my_validate])
may be it will be useful:
from django.core.validators import FileExtensionValidator
class MyModel(models.Model):
......
my_field = models.FileField('Name', validators=[FileExtensionValidator(['svg', 'jpg', 'png', '.....'])])
I have the following code in one of my models
class PostImage(models.Model):
post = models.ForeignKey(Post, related_name="images")
# #### figure out a way to have image folders per user...
image = models.ImageField(upload_to='images')
image_infowindow = models.ImageField(upload_to='images')
image_thumb = models.ImageField(upload_to='images')
image_web = models.ImageField(upload_to='images')
description = models.CharField(max_length=100)
order = models.IntegerField(null=True)
IMAGE_SIZES = {
'image_infowindow':(70,70),
'image_thumb':(100,100),
'image_web':(640,480),
}
def delete(self, *args, **kwargs):
# delete files..
self.image.delete(save=False)
self.image_thumb.delete(save=False)
self.image_web.delete(save=False)
self.image_infowindow.delete(save=False)
super(PostImage, self).delete(*args, **kwargs)
I am trying to delete the files when the delete() method is called on PostImage. However, the files are not being removed.
As you can see, I am overriding the delete() method, and deleting each ImageField. For some reason however, the files are not being removed.
You can delete a model instance with multiple methods.
One method is by calling delete():
PostImage.objects.get(...).delete()
In this case the delete() is called, hence the files will be removed. However you can also delete objects by using querysets:
PostImage.objects.filter(...).delete()
The difference is that using the latter method, Django will delete the objects in bulk by using SQL DELETE command, hence the delete() method for each object is not called. So probably you are deleting objects using querysets, and therefore files are not being removed.
You can solve this by using post_delete Django signal as follows:
#receiver(post_delete, sender=PostImage)
def post_delete_user(sender, instance, *args, **kwargs):
instance.image.delete(save=False)
instance.image_thumb.delete(save=False)
instance.image_web.delete(save=False)
instance.image_infowindow.delete(save=False)
Please note that if you use this method, you don't have to overwrite the delete() method anymore.
More about this here and here