I have an ImageField in my model which I want to store the image url dynamically depending on a user session var.
Imaginary like this:
logo = models.ImageField(null=True, upload_to = 'empresas/'+codEmp+'/logo/')
And the var codEmp is a session variable:
request.session['codEmp']
Therefore if the codEmp of a user is for example 'McDonalds' it should save the following path: 'empresas/McDonalds/logo/imaginary_picture.jpg'.
I tried by init but I can't finish it and I'm not sure if it'll work.
class Empresa(models.Model):
def __init__(self, filter_on, *args, **kwargs):
super(Empresa, self).__init__(*args, **kwargs)
codEmp = filter_on
logo = models.ImageField(null=True, upload_to = 'empresas/'+codEmp+'/logo/')
I tried to do this in the ModelForm but it seems that forms.ImageField doesn't have an upload_to attribute.
The upload_to parameter may be a callable expected to accept 2 parameters: instance and filename. See here.
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 am working on some Django project (first time) and after lot of searching i have no clue how to proper update object attribute after creation. I have sucha models.py
from django.db import models
import os
# Create your models here.
class Place(models.Model):
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Add templates folder for dynamic usage, os independent
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates/places")
name = models.CharField(max_length=100, unique=True)
display_name = models.CharField(max_length=100)
floor = models.DecimalField(max_digits=3, decimal_places=0)
template = models.FilePathField(path=TEMPLATE_DIR, match=".*html")
url = models.URLField(unique=True)
# Display name of object in object list view
def __str__(self):
return self.name
Question is how to update url attribute after object creation of url to this particular object, it should be something like (base url + object_id i know object_id is created after object creation) but after searching documentation i dont have ideas how to do this properly.
I tried get_absolute_path but with no success.
Maybe some kind post_save method override ?
One option would be to override the model save method and detect if the model instance is being added or updated and update the url field accordingly:
class Place(models.Model):
# ...
def save(self, *args, **kwargs):
is_adding = self._state.adding
super(Place, self).save(self, *args, **kwargs)
if is_adding:
url = os.path.join(self.TEMPLATE_DIR, str(self.pk))
super(Place, self).save(self, *args, **kwargs)
However, you needn't actually store the url as you can derive the value from other model fields when required. Hence, you can delete the url field and create a url method instead:
class Place(models.Model):
# ...
#property
def url(self):
return os.path.join(self.TEMPLATE_DIR, str(self.pk))
Here, the #property decoration here allows you to access place.url as if it was a model field.
This might be a good time to introduce something like Django celery into you're project and run this task async after creation.
You could have a task in a file like this:
# I'm making assumptions on project architecture..
from app.places import Place
from app.places.utils import PLACE_BASE_URL
#shared_task
def task_update_place_url(place_id):
"""
Simple method to update Place url after creation.
"""
place = Place.objects.get(pk=place_id)
place.url = PLACE_BASE_URL + place_id
place.save()
Then inside of your view when you create your Place you can do this:
import json
from django.http.response import HttpResponse
from app.places.models import Place
from app.places.tasks import task_update_place_url
def create_place(request):
"""
Creates a new Place and update url in task
"""
data = json.loads(request)
place = Place.objects.create(**data)
task_update_place_url.delay(place.id)
return HttpResponse("Finished")
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'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
I have following models for category
class Category(models.Model):
name = models.CharField(max_length=30)
is_active=models.BooleanField()
photo=models.ImageField(upload_to='category')
def __unicode__(self):
name = str(self.name)
return name
def image(self):
return self.photo or 'DEFAULT_PIC.jpg'
class Meta:
permissions = (
('category','Category'),
('view_category', 'View category'),
)
My form class is as follows
class categoryForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={'class':'box'}),max_length=32,label='Category',required=True)
is_active = forms.BooleanField(required=False)
id = forms.CharField(widget=forms.HiddenInput,required=False)
photo = forms.FileField(
required=False,
label='Select an Image',
help_text='max. 4 megabytes'
)
It is working perfect for insert and update both, now i am trying to load the image which has already been upload in the form (say i have already added one category with image , now i want to upload another image, so i am trying to show the preview of previously uploaded image)
I passed the data in the view and tried few hack in forms
formdata = categoryForm({'name':p.name,'is_active':p.is_active,'id':p.id,'photo':p.image()})
In form i made modification as below
def __init__(self, data=None, **kwargs):
# pass
super(categoryForm, self).__init__(data, **kwargs)
if self.data['photo']!='':
self.fields['uploaded_photo'] =forms.CharField(widget=forms.TextInput,required=False,label=mark_safe('<img src="/media/%s" height="100">'%(self.data['photo'])))
#self.fields['uploaded_photo'].widget.attrs['value']=self.data['photo']
Now it is perfectly showing previously uploaded image on label .
After that i tried to upload another image but it shows following error
TypeError at /update/category/
init() takes at most 2 arguments (3 given)
Script to handle image upload is as follows
formdata = categoryForm(request.POST,request.FILES)
if formdata.is_valid():
cd = formdata.cleaned_data
p1=Category()
p1.id=cd['id']
p1.name=cd['name']
p1.is_active=cd['is_active']
p1.photo=cd['photo']
p1.save()
Please let me know what is happening here
Try making life easy by using the ImageField
class ImageField(**kwargs)ΒΆ
Default widget: ClearableFileInput
Empty value: None
Normalizes to: An UploadedFile object that wraps the file content and file name into a single object.
Validates that file data has been bound to the form, and that the file is of an image format understood by PIL.
Error message keys: required, invalid, missing, empty, invalid_image
Using an ImageField requires that the Python Imaging Library is installed.
When you use an ImageField on a form, you must also remember to bind the file data to the form.
Check out the complete documentation here