I have a models.FileField on my Admin-Page in a Model and would like to show an error to the user when he tries to upload an already existing file.
I already tried overriding get_available_name() on FileSystemStorage, but if I throw a ValidationError, it doesn't get displayed nicely.
Is there any way to do this (easily)?
Provide a custom form to your ModelAdmin:
class FileModel(models.Model):
name = models.CharField(max_length=100)
filefield = models.FileField()
class CustomAdminForm(forms.ModelForm):
# Custom validation: clean_<fieldname>()
def clean_filefield(self):
file = self.cleaned_data.get('filefield', None):
if file:
# Prepare the path where the file will be uploaded. Depends on your project.
# In example:
file_path = os.path.join( upload_directory, file.name )
# Check if the file exists
if os.path.isfile(file_path):
raise ValidationError("File already exists")
return super(CustomAdminForm, self).clean_filefield()
# Set the ModelAdmin to use the custom form
class FileModelAdmin(admin.ModelAdmin):
form = CustomAdminForm
Related
I find myself in an odd situation only when deployed (debug == false):
My model throws a path traversal attempt exception. I want to create a directory for every file uploaded and save the file within the directory (some.zip) used in example. In my dev environment I have no problems and everything works just fine.
models.py:
class Template(models.Model):
def get_folder(self, filename):
filename_PATH = Path(filename)
template_dir = filename_PATH.stem
return Path(settings.TEMPLATES_FOLDER).joinpath(template_dir, filename)
name = models.CharField("template", max_length=32, unique=True)
file = models.FileField("templatefile", upload_to=get_folder, null=True, max_length=260, storage=OverwriteStorage())
class OverwriteStorage(FileSystemStorage): #this is actually above
def get_available_name(self, name, max_length=None):
self.delete(name)
return name
forms.py:
class TemplateAdminForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = Template
fields = ["name", "file", ]
def clean(self):
cleaned_data = super().clean()
upFile = Path(str(cleaned_data["file"]))
if upFile.suffix == ".zip":
path = self.instance.get_folder(cleaned_data["name"])
logging.error(f"{path}")
unpack_zip(path) ## works! the directory is created/filled
else:
raise forms.ValidationError("unknown file type ...")
logging.error("DONE!") # I see this output
return cleaned_data
## signal to see when the error might be happening:
#receiver(post_save, sender = Template)
def testing(sender, **kwargs):
logging.error("we never get here")
settings.py:
TEMPLATES_FOLDER = PATH(MEDIA_ROOT).joinpath("TEMPLATES")
but:
ERROR:django.security.SuspiciousFileOperation:Detected path traversal attempt in '/opt/project/media_root/TEMPLATES/some/some' WARNING:django.request:Bad Request: /admin/appName/template/add/
Edit:
Because of this discussion it might be important, this is happening on django 3.2.8
I get the same error on Django 3.2.6 when opening a file with mode "wb" at an absolute path name, when I'm not using a temporary file which I have read is recommened in order to avoid this problem so I will link this answer in case it helps you deploy it and share my experience.
Here's where it's been advised: answer
One possible solution would be to move that directory under the django project root folder and address it with a relative path. I'd try to use this too in order to understand how you could achieve this:
import os
print("WORKING DIRECTORY: " + os.getcwd())
An article on this topic suggests to use the following code (when dealing with an image file in that case): link
from django.core.files.temp import NamedTemporaryFile
from django.core import files
image_temp_file = NamedTemporaryFile(delete=True)
in_memory_image = open('/path/to/file', 'rb')
# Write the in-memory file to the temporary file
# Read the streamed image in sections
for block in in_memory_image.read(1024 * 8):
# If no more file then stop
if not block:
break # Write image block to temporary file
image_temp_file.write(block)
file_name = 'temp.png' # Choose a unique name for the file
image_temp_file.flush()
temp_file = files.File(image_temp_file, name=file_name)
Lets go through the code:
Create a NamedTemporaryFile instead of TemporaryFile as Django’s ImageField requires file name.
Iterate over your in-memory file and write blocks of data to the NamedTemporaryFile object.
Flush the file to ensure the file is written to the storage.
Change the temporary file to a Django’s File object.
You can assign this file to Django models directly and save it.
>>> from blog.models import Blog
>>> b = Blog.objects.first()
>>> b.image = temp_file
>>> b.save()
I personally solved my SuspiciousFileOperation problems by addressing my directory with "BASE_DIR" from settings.py as the beginning of the path (nothing above that level in the filesystem), using a NamedTemporaryFile and by using the model FileField save() method appropriately like this:
# inside a model class save(self, *args, **kwargs) method
# file_name is the file name alone, no path to the file
self.myfilefield.save(file_name, temporary_file_object, save=False) # and then call the super().save(*args, **kwargs) inside the save() method of your model
I'm using boto3 to upload files to S3 and save their path in the FileField.
class SomeFile(models.Model):
file = models.FileField(upload_to='some_folder', max_length=400, blank=True, null=True)
For the above model the following code works to create a record.
ff = SomeFile(file='file path in S3')
ff.full_clean()
ff.save()
Now, when I use ModelSerializer to do the same.
class SomeFileSerializer(serializers.ModelSerializer):
class Meta:
model = SomeFile
fields = ('file')
I get this error after running the code below
rest_framework.exceptions.ValidationError: {'file': [ErrorDetail(string='The submitted data was not a file. Check the encoding type on the form.', code='invalid')]}
serializer = SomeFileSerializer(data={'file': 'file path to S3'})
serializer.is_valid(raise_exception=True)
I need help in setting up the serializer to accept file path without actually having the file.
I was really in the same situation, and it was hard to find the solution on the web.
We have two options to solve this problem.
1. Passing data directly to save method
Read action: use serializer's read only ImageField
Write action: pass kwargs to save method
serializers.py
from rest_framework import serializers
class SomeFileSerializer(serializers.ModelSerializer):
file = serializers.ImageField(read_only=True)
class Meta:
model = SomeFile
fields = ('file')
views.py
serializer = SomeFileSerializer(data={'file': 'file path to S3'})
serializer.is_valid(raise_exception=True)
# for put method
serializer.save(file=request.data.get('file'))
# for patch method (if partial=True in serializer)
if request.data.get('file'):
serializer.save(file=request.data.get('file'))
else:
serializer.save()
2. Using CharField instead of ImageField
Read action: override to_representation function to response absolute url
Write action: use CharField to avoid ImageField's validation and action
serializers.py
from rest_framework import serializers
class SomeFileSerializer(serializers.ModelSerializer):
file = serializers.CharField(max_length=400)
class Meta:
model = SomeFile
fields = ('file')
def to_representation(self, instance):
representation = super().to_representation(instance)
if instance.file:
# update filename to response absolute url
representation['file'] = instance.file_absolute_url
return representation
models.py
class SomeFile(models.Model):
file = models.FileField(upload_to='some_folder', max_length=400, blank=True, null=True)
#property
def file_absolute_url(self):
return self.file.url if self.file else None
Although I chose the 2nd solution because of drf_spectacular for documentation, the 1st solution would be easy to implement.
I'm trying to upload file using the file input in the model creation page in the Django's admin panel. The file isn't a part of an object, so it doesn't belong to an object itself. I just need to get it, process it and then delete it.
I've created a form:
class AddTaskAndTestsForm(forms.ModelForm):
tests_zip_field = forms.FileField(required=False)
def save(self, commit=True):
# I need to save and process the tests_zip_field file here
return super(AddTaskAndTestsForm, self).save(commit=commit)
class Meta:
model = Problem
And I added the form to the admin panel, so it's now displayed there.
I need to save the file, once the create form is submitted, but how do I do that?
UPDATE: here's how I use it.
admin.py:
class ProblemAdmin(admin.ModelAdmin):
form = AddTaskAndTestsForm
fieldsets = [
# ... some fieldsets here
('ZIP with tests', {
'fields': ['tests_zip_field']
})
]
# ... some inlines here
Try this:
class AddTaskAndTestsForm(forms.ModelForm):
tests_zip_field = forms.FileField(required=False)
def save(self, commit=True):
instance = super(AddTaskAndTestsForm, self).save(commit=False)
f = self['tests_zip_field'].value() # actual file object
# process the file in a way you need
if commit:
instance.save()
return instance
You can call tests_zip_field.open() ( behaves pretty much like the python open() ) and use it in your save() method like this :
tests_zip_file = self.tests_zip_field.open()
tests_zip_data = tests_zip_file.read()
## process tests_zip_data
tests_zip_file.close()
the file is saved in your MEDIA_ROOT/{{upload_to}} folder whenever the save() method finishes
I have a Django model with an ImageField() field. Now I'd like to rename the filename of the image (based on a unique CharField of the same model) before it gets saved to the filesystem. Additionally, if an image with the same filename already exists, the existing file should be renamed and the newly uploaded file should keep its filename.
I am not quite sure what's the correct or preferred way to do it. Should I override ModelAdmin.save_model(), do it in the Model.save() method or write an own custom file storage?
Can anyone give me some hits how I can accomplish this? Any tips or sample code are greatly appreciated.
You can combine two mechanisms here: passing an upload_to argument in the field's definition and a custom FileSystemStorage backend.
Here is a dummy model with an upload_to callable:
def upload_bar(instance, filename):
# Do something with the filename
return new_filename
class Foo(models.Model):
bar = models.ImageField(upload_to=upload_bar)
...
And a custom dummy FileSystemStorage backend:
from django.core.files.storage import FileSystemStorage
class OverwriteStorage(FileSystemStorage):
def _save(self, name, content):
if self.exists(name):
# Rename it
return super(OverwriteStorage, self)._save(name, content)
def get_available_name(self, name):
return name
However I would be very cautious in meddling with existing files (ie changing existing files' names). Note that Django does not remove the file from the filesystem even when the object is deleted.
Here's the solution to get unique filenames like 18f6ad9f-5cec-4420-abfd-278bed78ee4a.png
models.py
import os
import uuid
from django.conf import settings
from django.db import models
def make_upload_path(instance, filename):
file_root, file_ext = os.path.splitext(filename)
dir_name = '{module}/{model}'.format(module=instance._meta.app_label, model=instance._meta.module_name)
file_root = unicode(uuid.uuid4())
name = os.path.join(settings.MEDIA_ROOT, dir_name, file_root + file_ext.lower())
# Delete existing file to overwrite it later
if instance.pk:
while os.path.exists(name):
os.remove(name)
return os.path.join(dir_name, file_root + file_ext.lower())
class YourModel(models.Model):
title = models.CharField(max_length=100)
image = models.ImageField(blank=True, upload_to=make_upload_path)
you can access self.image.name from Model.save()
http://lightbird.net/dbe/photo.html
I am new to Django and would like to know what is the Django-way to add elements in a database not by entering each field from an html form (like it is done by default) but uploading a single file (for example a json file) that will be used to populate the database?
So let say the model has only three fields: title,description,quantity.
And I have a text file (myFile.txt) with "myTitle:myDesc" written in it.
What I want is just a FileField that will accept a text file so I can upload myFile.txt and the title and description will be read from this file.
And at the same time the quantity will be asked "normally" in a text input as it would be by default (only title and description are read from the file).
Of course, validation on the file will be done to accept/deny the uploaded file.
The problem I am facing is that if I add a FileField to the model, the file will be stored in the local storage.
I want the content of the uploaded file to be read, used to create an entry in the model, and then deleted.
Even the admin should not be able to manually add an element entering the title and description in a HTML form but only by uploading a file.
Can someone help me in a Django-way?
You can create two forms:
A form based on django.forms.Form which is used to get the file from request
A model form which is used to validate model fields and create a model object
Then you can call the second form from the first one, like this:
class MyModelForm(ModelForm):
class Meta:
model = MyModel
class FileUploadForm(forms.Form):
file = forms.FileField()
def clean_file(self):
data = self.cleaned_data["file"]
# read and parse the file, create a Python dictionary `data_dict` from it
form = MyModelForm(data_dict)
if form.is_valid():
# we don't want to put the object to the database on this step
self.instance = form.save(commit=False)
else:
# You can use more specific error message here
raise forms.ValidationError(u"The file contains invalid data.")
return data
def save(self):
# We are not overriding the `save` method here because `form.Form` does not have it.
# We just add it for convenience.
instance = getattr(self, "instance", None)
if instance:
instance.save()
return instance
def my_view(request):
form = FileUploadForm(request.POST, request.FILES)
if form.is_valid():
form.save()
else:
# display errors
You can use form wizard to achieve such tasks. The basic idea is to create two forms; one with the FileField and the other form with the title, description quantity fields.
The user views the form with FileField first. Once the user uploads the file and submits the request, you can render the other form with initial values read from the file (you can also delete the file at this step).
Regarding the admin functionality, you can read about how to integrate form wizard with admin here
I found another way of populating a model before saving it.
Instead of using pre_save, or using 2 different forms, if we are using the admin.ModelAdmin, we can simply redefine the save_model() method:
def save_model(self, request, obj, form, change):
obj.user = request.user
# populate the model
obj.save()
# post actions if needed
To achieve this you have to write some custom code. Each FileField has a connected File object. You can read the content of this file object like you would when dealing with files in Python.
There are of course different locations you could do that. You can overwrite the forms/models save method which contains the FileField. If you have model you could use pre_save/post_save signals as well.