I have a django model that takes an audio file:
class Thing(models.Model):
audio_file = AudioFileField( upload_to=audio_dir, blank=True, null=True )
photo_file = models.ImageField( upload_to=img_dir, blank=True, null=True )
...
where the AudioFileField is a subclass of FileField that performs some validation:
class AudioFileField(models.FileField):
def validate(self, value, model_instance):
try:
if not (value.file.content_type == "audio/x-wav" or
value.file.content_type == "audio/amr" or
value.file.content_type == "video/3gpp"):
raise ValidationError(u'%s is not an audio file' % value)
except IOError:
logger.warning("no audio file given")
and the audio_dir callback sets the path and renames the file:
def audio_dir(instance, filename):
return os.path.join("audio", "recording_%s%s" % (
datetime.datetime.now().isoformat().replace(":", "-"),
os.path.splitext(filename)[1].lower() ))
In Django REST framework the ImageField works fine, but the subclassed AudioFileField doesn't. This is because the subclass serializers.FileField doesn't accept the keyword argument upload_to.
How can I expose the same functionality through the API? The audio_dir callback is particularly important to me.
I search how to customize filefield and I don't know if it's fix your problem. If not I will search again for it and just tell me the error.
class Thing(models.Model):
audio_file = AudioFileField(
upload_to=audio_dir,
blank=True, null=True,
content_types=['audio/x-wav', 'audio/amr', 'video/3gpp']
)
...............
class AudioFileField(models.FileField):
def __init__(self, *args, **kwargs):
self.content_types = kwargs.pop("content_types")
super(AudioFileField, self).__init__(*args, **kwargs)
def clean(self, *args, **kwargs):
data = super(AudioFileField, self).clean(*args, **kwargs)
audio_file = data.audio_file
try:
content_type = audio_file.content_type
if content_type in self.content_types:
raise ValidationError(u'{0} is not an audio file'.format(content_type))
else:
raise forms.ValidationError(_('Audio file type not supported.'))
except AttributeError:
pass
return data
Related
I'm trying to force certain FilerImageFields to be convert to another format on saving.
I've created this model
class SearchImage(FilerImageField):
def convert_to_webp(self):
print("in convert")
extension = ['jpeg', 'png', 'jpg', 'img', 'gif']
if any(ext in self.file.name.lower() for ext in extension):
#try:
img = Image.open(self.file)
correggi_nome = self.file.name.split('.')[0]
img.save(correggi_nome + '.webp','webp')
logger.debug('img.save save another copy of the file not repalced the original!')
#except Exception as ex:
# logger.error(ex)
def save(self, *args, **kwargs):
self.convert_to_webp()
print("Save search image")
super().save()
class Organisation(models.Model):
....
search_image = SearchImage(
related_name='+',
verbose_name=_('Search thumbnail image'),
help_text=_('Search thumbnail image'),
on_delete=models.SET_NULL,
blank=True,
null=True
)
....
def save(*args, **kwargs):
print('saving')
print("just before search thumbnail save")
self.search_image.save()
print("just after search thumbnail save")
super.save()
my SeachImage.save() is never called. When I save an image (or organisation) I see the print statements within the Organisation.save() but not the SearchImage.save(). How to I get this to work?
Okay so I went down a bit of a rabbit hole but it looks like you want to overwrite the Image object's save method, not the FilerImageField, which is a ForeignKey object:
class MyImageModel(Image):
def convert_to_webp(self):
print('in convert')
extension = ['jpeg', 'png', 'jpg', 'img', 'gif']
if any(ext in self.file.name.lower() for ext in extension):
print('Doing the conversion')
def save(self, *args, **kwargs):
self.convert_to_webp()
print('Save search image')
super().save(*args, **kwargs)
class SearchImage(FilerImageField):
default_model_class = MyImageModel
class Organisation(models.Model):
search_image = FilerImageField(
verbose_name='Search thumbnail image',
help_text='Search thumbnail image',
on_delete=models.SET_NULL,
blank=True,
null=True
)
def save(self, *args, **kwargs):
self.search_image.save()
super().save(*args, **kwargs)
I have a Files table with information about uploaded files in a remote directory. This is the model for that table:
class Files(models.Model):
id = models.AutoField(primary_key=True)
subjectid = models.ForeignKey('Subjects', models.DO_NOTHING, db_column='subjectid')
filetypeid = models.ForeignKey(FileTypes, models.DO_NOTHING, db_column='filetypeid')
filedescid = models.ForeignKey(FileDescription, models.DO_NOTHING, db_column='filedescid')
filepath = models.CharField(max_length=45, blank=True, null=True)
filename = models.FileField(upload_to='attachments/', blank=True, null=True)
ispublic = models.IntegerField(choices=YESNO)
extra_info = models.CharField(max_length=255, blank=True, null=True)
def __str__(self):
return self.filename.name or ''
class Meta:
managed = False
db_table = 'files'
verbose_name_plural = 'files'
I've created my own URL widget to replace the Django FileField url shown as 'Currently:' in the change_form template. The link points to a view that downloads the file. So far, so good it works but the problem is that when I try to add a new file I can select the new file with the Browse file button but when I click on Save the field filename field is empty and no file is uploaded.
class MyAdminURLFieldWidget(URLInput):
template_name = 'admin/widgets/url.html'
def __init__(self, attrs=None):
#final_attrs = {'class': 'vURLField'}
final_attrs = {'type': 'file'}
if attrs is not None:
final_attrs.update(attrs)
super(MyAdminURLFieldWidget, self).__init__(attrs=final_attrs)
def get_context(self, name, value, attrs):
context = super(MyAdminURLFieldWidget, self).get_context(name, value, attrs)
context['current_label'] = _('Currently:')
context['change_label'] = _('Change:')
context['widget']['href'] = smart_urlquote('/DownloadView/' + str(value.instance.id) + '/attachment/') if value else ''
return context
class FilesAdmin(admin.ModelAdmin):
list_display = ('id', '_animalid', '_filename', '_filedesc', 'ispublic', 'extra_info')
search_fields = ('subjectid__animalid','filename')
list_per_page = 50
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'filename':
request = kwargs.pop("request", None)
kwargs['widget'] = MyAdminURLFieldWidget
return db_field.formfield(**kwargs)
else:
return super(FilesAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def _animalid(self, obj):
return obj.subjectid.animalid
def _filename(self, obj):
return obj.filename.name
def _filedesc(self, obj):
return obj.filedescid.description
Can anybody tell what I'm missing here?
Hi lost Django community,
I will answer my own question since it seems that nobody was able to realize the answer. It happens that as newbie I'm following examples I've found here and there but there is little about sub-classing the FileField associated widget. So, after diving deep into the Django code I've found the answer. In my case the problem was I derived my MyAdminURLFieldWidget from URLInput instead of the correct subclass ClearableFileInput.
You are welcome.
I have a Django project in which I have a view subclassed from the Django CreateView class. This view is used to upload a file to the server, and uses an UploadedFile model which I have created. The UploadedFile also needs to be associated with a project.
The project id is passed in as part of the URL: (r'^projects/(?P<proj_key>\d+)/$', UploadedFileCreateView.as_view(), {}, 'upload-new')
The problem is that I am not sure where the appropriate place is to associate this key with my model. Is there a method of CreateView or one of its ancestors that I should override that creates the model, or can this be done anywhere in my code in one of the methods I already override (this feels hacky though).
Furthermore, the project attribute of my UploadedFile is defined as a ForeignKey of type Project. How do I get the Project to associate with it?
Here is my model definition:
class Project(models.Model):
"""This is a project that is owned by a user and contains many UploadedFiles."""
name = models.CharField(max_length=200)
class UploadedFile(models.Model):
"""This represents a file that has been uploaded to the server."""
STATE_UPLOADED = 0
STATE_ANNOTATED = 1
STATE_PROCESSING = 2
STATE_PROCESSED = 4
STATES = (
(STATE_UPLOADED, "Uploaded"),
(STATE_ANNOTATED, "Annotated"),
(STATE_PROCESSING, "Processing"),
(STATE_PROCESSED, "Processed"),
)
status = models.SmallIntegerField(choices=STATES,
default=0, blank=True, null=True)
file = models.FileField(upload_to=settings.XML_ROOT)
project = models.ForeignKey(Project)
def __unicode__(self):
return self.file.name
def name(self):
return os.path.basename(self.file.name)
def save(self, *args, **kwargs):
if not self.status:
self.status = self.STATE_UPLOADED
super(UploadedFile, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
os.remove(self.file.path)
self.file.delete(False)
super(UploadedFile, self).delete(*args, **kwargs)
Here is my view definition:
class UploadedFileCreateView(CreateView):
model = UploadedFile
def form_valid(self, form):
logger.critical("Inside form_valid")
self.object = form.save()
f = self.request.FILES.get('file')
data = [{'name': f.name,
'url': settings.MEDIA_URL + "files/" + f.name.replace(" ", "_"),
'project': self.object.project.get().pk,
'delete_url': reverse('fileupload:upload-delete',
args=[self.object.id]),
'delete_type': "DELETE"}]
response = JSONResponse(data, {}, response_mimetype(self.request))
response['Content-Disposition'] = 'inline; filename=files.json'
return super(UploadedFileCreateView, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(UploadedFileCreateView, self).get_context_data(**kwargs)
return context
You could do it right where you are calling form.save(). Just pass commit=False so that it won't save it to the db until you add the project id. For example:
self.object = form.save(commit=False)
self.object.project_id = self.kwargs['proj_key']
self.object.save()
Just make sure your form excludes the project field.
EDIT: to exclude the field, add an excludes variable to the form meta class:
class UploadedFileForm(forms.ModelForm):
class Meta:
model = UploadedFile
excludes = ('project',)
I have a model where the location of pdf directory I'm pointing to with my FilePathField is based on the "client" and "job_number" fields.
class CCEntry(models.Model):
client = models.CharField(default="C_Comm", max_length=64)
job_number = models.CharField(max_length=30, unique=False, blank=False, null=False)
filename = models.CharField(max_length=64, unique=False, blank=True, null=True)
pdf = models.FilePathField(path="site_media/jobs/%s %s", match=".*\.pdf$", recursive=True
#property
def pdf(self):
return "site_media/jobs/%s %s" % (self.client, self.job_number)
def __unicode__ (self):
return u'%s %s' % (self.client, self.filename)
class Admin:
pass
I've tried to pass the client and job_number data to the pdf field dynamically by using a #property method on the model class, but either my approach or my syntax is fualty because the entire pdf field disappears in the admin. Any pointers on what I'm doing wrong?
Based on your subsequent post on similar functionality in the FileField (see last link below)
And my inability to get any of the above to work, I'm gonna hazard a guess that it's not yet possible for the FilePathField field type.
I know that passing a callable works for most fields' 'default' parameters...
https://docs.djangoproject.com/en/dev/ref/models/fields/#default
... as it appears to work for the upload_to param of FieldField
(eg https://stackoverflow.com/questions/10643889/dynamic-upload-field-arguments/ ) andImageField` (eg Django - passing extra arguments into upload_to callable function )
Anyone interested in extending FilePathField to include this feature?
Anyone interested in extending FilePathField to include this feature?
I'd love to see this extension!
Just for the record, this is the solution that worked for me (django 1.3):
# models.py
class Analysis(models.Model):
run = models.ForeignKey(SampleRun)
# Directory name depends on the foreign key
# (directory was created outside Django and gets filled by a script)
bam_file = models.FilePathField(max_length=500, blank=True, null=True)
# admin.py
class CustomAnalysisModelForm(forms.ModelForm):
class Meta:
model = Analysis
def __init__(self, *args, **kwargs):
super(CustomAnalysisModelForm, self).__init__(*args, **kwargs)
# This is an update
if self.instance.id:
# set dynamic path
mypath = settings.DATA_PATH + self.instance.run.sample.name
self.fields['bam_file'] = forms.FilePathField(path=mypath, match=".*bam$", recursive=True)
class AnalysisAdmin(admin.ModelAdmin):
form = CustomAnalysisModelForm
Hope this helps somebody out there.
try to set the path value as callable function
def get_path(instance, filename):
return "site_media/jobs/%s_%s/%s" % (instance.client, instance.job_number, filename)
class CCEntry(models.Model):
....
pdf = models.FilePathField(path=get_path, match=".*\.pdf$", recursive=True)
but I'm not sure if this works, I didn't test it.
Added an implementation of this based on Django v1.9 FilePathField implementation:
from django.db.models import FilePathField
class DynamicFilePathField(FilePathField):
def __init__(self, verbose_name=None, name=None, path='', match=None,
recursive=False, allow_files=True, allow_folders=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive
if callable(self.path):
self.pathfunc, self.path = self.path, self.path()
self.allow_files, self.allow_folders = allow_files, allow_folders
kwargs['max_length'] = kwargs.get('max_length', 100)
super(FilePathField, self).__init__(verbose_name, name, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(FilePathField, self).deconstruct()
if hasattr(self, "pathfunc"):
kwargs['path'] = self.pathfunc
return name, path, args, kwargs
And example use:
import os
from django.db import models
def get_data_path():
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'data'))
class GenomeAssembly(models.Model):
name = models.CharField(
unique=True,
max_length=32)
chromosome_size_file = DynamicFilePathField(
unique=True,
max_length=128,
path=get_data_path,
recursive=False)
I'm trying to generate dynamic file paths in django. I want to make a file system like this:
-- user_12
--- photo_1
--- photo_2
--- user_ 13
---- photo_1
I found a related question : Django Custom image upload field with dynamic path
Here, they say we can change the upload_to path and leads to https://docs.djangoproject.com/en/stable/topics/files/ doc. In the documentation, there is an example :
from django.db import models
from django.core.files.storage import FileSystemStorage
fs = FileSystemStorage(location='/media/photos')
class Car(models.Model):
...
photo = models.ImageField(storage=fs)
But, still this is not dynamic, I want to give Car id to the image name, and I cant assign the id before Car definition completed. So how can I create a path with car ID ??
You can use a callable in the upload_to argument rather than using custom storage. See the docs, and note the warning there that the primary key may not yet be set when the function is called. This can happen because the upload may be handled before the object is saved to the database, so using ID might not be possible. You might want to consider using another field on the model such as slug. E.g:
import os
def get_upload_path(instance, filename):
return os.path.join(
"user_%d" % instance.owner.id, "car_%s" % instance.slug, filename)
then:
photo = models.ImageField(upload_to=get_upload_path)
You can use lambda function as below, take note that if instance is new then it won't have the instance id, so use something else:
logo = models.ImageField(upload_to=lambda instance, filename: 'directory/images/{0}/{1}'.format(instance.owner.id, filename))
https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.FileField.upload_to
def upload_path_handler(instance, filename):
return "user_{id}/{file}".format(id=instance.user.id, file=filename)
class Car(models.Model):
...
photo = models.ImageField(upload_to=upload_path_handler, storage=fs)
There is a warning in the docs, but it shouldn't affect you since we're after the User ID and not the Car ID.
In most cases, this object will not
have been saved to the database yet,
so if it uses the default AutoField,
it might not yet have a value for its
primary key field.
My solution is not elegant, but it works:
In the model, use a the standard function that will need the id/pk
def directory_path(instance, filename):
return 'files/instance_id_{0}/{1}'.format(instance.pk, filename)
in views.py save the form like this:
f=form.save(commit=False)
ftemp1=f.filefield
f.filefield=None
f.save()
#And now that we have crated the record we can add it
f.filefield=ftemp1
f.save()
It worked for me.
Note: My filefield in models and allowed for Null values. Null=True
Well very late to the party but this one works for me.
def content_file_name(instance, filename):
upload_dir = os.path.join('uploads',instance.albumname)
if not os.path.exists(upload_dir):
os.makedirs(upload_dir)
return os.path.join(upload_dir, filename)
Model like this only
class Album(models.Model):
albumname = models.CharField(max_length=100)
audiofile = models.FileField(upload_to=content_file_name)
There are two solutions on DjangoSnippets
Two-stage save: https://djangosnippets.org/snippets/1129/
Prefetch the ID (PostgreSQL only): https://djangosnippets.org/snippets/2731/
This guy has a way to do dynamic path. The idea is to set your favourite storage and customise "upload_to()" parameter with a function.
Hope this helps.
I found out a different solution, which is dirty, but it works. You should create a new dummy model, which is self synchronized with the original one. I'm not proud of this, but didn't find another solution. In my case I want to upload files, and store each in a directory named after the model id (because I'll generate there more files).
the model.py
class dummyexperiment(models.Model):
def __unicode__(self):
return str(self.id)
class experiment(models.Model):
def get_exfile_path(instance, filename):
if instance.id == None:
iid = instance.dummye.id
else:
iid = instance.id
return os.path.join('experiments', str(iid), filename)
exfile = models.FileField(upload_to=get_exfile_path)
def save(self, *args, **kwargs):
if self.id == None:
self.dummye = dummyexperiment()
self.dummye.save()
super(experiment, self).save(*args, **kwargs)
I'm very new in python and in django, but it seems like ok for me.
another solution:
def get_theme_path(instance, filename):
id = instance.id
if id == None:
id = max(map(lambda a:a.id,Theme.objects.all())) + 1
return os.path.join('experiments', str(id), filename)
As the primary key (id) may not be available if the model instance was not saved to the database yet, I wrote my FileField subclasses which move the file on model save, and a storage subclass which removes the old files.
Storage:
class OverwriteFileSystemStorage(FileSystemStorage):
def _save(self, name, content):
self.delete(name)
return super()._save(name, content)
def get_available_name(self, name):
return name
def delete(self, name):
super().delete(name)
last_dir = os.path.dirname(self.path(name))
while True:
try:
os.rmdir(last_dir)
except OSError as e:
if e.errno in {errno.ENOTEMPTY, errno.ENOENT}:
break
raise e
last_dir = os.path.dirname(last_dir)
FileField:
def tweak_field_save(cls, field):
field_defined_in_this_class = field.name in cls.__dict__ and field.name not in cls.__bases__[0].__dict__
if field_defined_in_this_class:
orig_save = cls.save
if orig_save and callable(orig_save):
assert isinstance(field.storage, OverwriteFileSystemStorage), "Using other storage than '{0}' may cause unexpected behavior.".format(OverwriteFileSystemStorage.__name__)
def save(self, *args, **kwargs):
if self.pk is None:
orig_save(self, *args, **kwargs)
field_file = getattr(self, field.name)
if field_file:
old_path = field_file.path
new_filename = field.generate_filename(self, os.path.basename(old_path))
new_path = field.storage.path(new_filename)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
os.rename(old_path, new_path)
setattr(self, field.name, new_filename)
# for next save
if len(args) > 0:
args = tuple(v if k >= 2 else False for k, v in enumerate(args))
kwargs['force_insert'] = False
kwargs['force_update'] = False
orig_save(self, *args, **kwargs)
cls.save = save
def tweak_field_class(orig_cls):
orig_init = orig_cls.__init__
def __init__(self, *args, **kwargs):
if 'storage' not in kwargs:
kwargs['storage'] = OverwriteFileSystemStorage()
if orig_init and callable(orig_init):
orig_init(self, *args, **kwargs)
orig_cls.__init__ = __init__
orig_contribute_to_class = orig_cls.contribute_to_class
def contribute_to_class(self, cls, name):
if orig_contribute_to_class and callable(orig_contribute_to_class):
orig_contribute_to_class(self, cls, name)
tweak_field_save(cls, self)
orig_cls.contribute_to_class = contribute_to_class
return orig_cls
def tweak_file_class(orig_cls):
"""
Overriding FieldFile.save method to remove the old associated file.
I'm doing the same thing in OverwriteFileSystemStorage, but it works just when the names match.
I probably want to preserve both methods if anyone calls Storage.save.
"""
orig_save = orig_cls.save
def new_save(self, name, content, save=True):
self.delete(save=False)
if orig_save and callable(orig_save):
orig_save(self, name, content, save=save)
new_save.__name__ = 'save'
orig_cls.save = new_save
return orig_cls
#tweak_file_class
class OverwriteFieldFile(models.FileField.attr_class):
pass
#tweak_file_class
class OverwriteImageFieldFile(models.ImageField.attr_class):
pass
#tweak_field_class
class RenamedFileField(models.FileField):
attr_class = OverwriteFieldFile
#tweak_field_class
class RenamedImageField(models.ImageField):
attr_class = OverwriteImageFieldFile
and my upload_to callables look like this:
def user_image_path(instance, filename):
name, ext = 'image', os.path.splitext(filename)[1]
if instance.pk is not None:
return os.path.join('users', os.path.join(str(instance.pk), name + ext))
return os.path.join('users', '{0}_{1}{2}'.format(uuid1(), name, ext))
MEDIA_ROOT/
/company_Company1/company.png
/shop_Shop1/shop.png
/bikes/bike.png
def photo_path_company(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/
return 'company_{0}/{1}'.format(instance.name, filename)
class Company(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_company)
def photo_path_shop(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/
parent_path = instance.company._meta.get_field('photo').upload_to(instance.company, '')
return parent_path + 'shop_{0}/{1}'.format(instance.name, filename)
class Shop(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_shop)
def photo_path_bike(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/bikes/
parent_path = instance.shop._meta.get_field('photo').upload_to(instance.shop, '')
return parent_path + 'bikes/{0}'.format(filename)
class Bike(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_bike)
You can override model's save method:
def save_image(instance, filename):
instance_id = f'{instance.id:03d}' # 001
return f'{instance_id}-{filename.lower()}' # 001-foo.jpg
class Resource(models.Model):
photo = models.ImageField(upload_to=save_image)
def save(self, *args, **kwargs):
if self.id is None:
photo = self.photo
self.photo = None
super().save(*args, **kwargs)
self.photo = photo
if 'force_insert' in kwargs:
kwargs.pop('force_insert')
super().save(*args, **kwargs)
The method will be
def user_directory_path(field_name):
def upload_path(instance, filename):
year = datetime.now().year
name, ext = instance.user, os.path.splitext(filename)[1]
return f'photos/{year}/{instance._meta.model_name}s/{instance.user}/{field_name}_{name}{ext}'
return upload_path
And in your models you can have as many ImageField as you like. example
photo = models.ImageField(upload_to=user_directory_path('photo'), null=True, blank=True,)
passport_photo = models.ImageField(upload_to=user_directory_path('passport_photo'), null=True, blank=True,)