Django dynamic FileField upload_to - django

I'm trying to make dynamic upload path to FileField model. So when user uploads a file, Django stores it to my computer /media/(username)/(path_to_a_file)/(filename).
E.g. /media/Michael/Homeworks/Math/Week_1/questions.pdf or /media/Ernie/Fishing/Atlantic_ocean/Good_fishing_spots.txt
VIEWS
#login_required
def add_file(request, **kwargs):
if request.method == 'POST':
form = AddFile(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.parent = Directory.objects.get(directory_path=str(kwargs['directory_path']))
post.file_path = str(kwargs['directory_path'])
post.file_content = request.FILES['file_content'] <-- need to pass dynamic file_path here
post.save()
return redirect('/home/' + str(post.author))
MODELS
class File(models.Model):
parent = models.ForeignKey(Directory, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
file_name = models.CharField(max_length=100)
file_path = models.CharField(max_length=900)
file_content = models.FileField(upload_to='e.g. /username/PATH/PATH/..../')
FORMS
class AddFile(forms.ModelForm):
class Meta:
model = File
fields = ['file_name', 'file_content']
What I have found was this, but after trial and error I have not found the way to do it. So the "upload/..." would be post.file_path, which is dynamic.
def get_upload_to(instance, filename):
return 'upload/%d/%s' % (instance.profile, filename)
class Upload(models.Model):
file = models.FileField(upload_to=get_upload_to)
profile = models.ForeignKey(Profile, blank=True, null=True)

You can use some thing like this(i used it in my project):
import os
def get_upload_path(instance, filename):
return os.path.join(
"user_%d" % instance.owner.id, "car_%s" % instance.slug, filename)
Now:
photo = models.ImageField(upload_to=get_upload_path)

Since the file_path is an attribute on the File model, can you not build the full path something like this:
import os
def create_path(instance, filename):
return os.path.join(
instance.author.username,
instance.file_path,
filename
)
And then reference it from your File model:
class File(models.Model):
...
file_content = models.FileField(upload_to=create_path)
Link to docs

The other answers work flawlessly; however, I want to point out the line in the source code that allows such functionality. You can view the function, generate_filename, here, in Django's source code.
The lines that make the magic happen:
if callable(self.upload_to):
filename = self.upload_to(instance, filename)
When you pass a callable to the upload_to parameter, Django will call the callable to generate the path. Note that Django expects your callable to handle two arguments:
instance
the model that contains the FileField/ImageField
filename
the name of the uploaded file, including the extension (.png, .pdf, ...)
Also note that Python does not force your callable's arguments to be exactly 'instance' and 'filename' because Django passes them as positional parameters. For example, I prefer to rename them:
def get_file_path(obj, fname):
return os.path.join(
'products',
obj.slug,
fname,
)
And then use it like so:
image = models.ImageField(upload_to=get_file_path)

Related

Django rename uploaded file: append specific string at the end

I'm restricting the upload button to allow only csv files.
I need help please to append _hello at the end of each file uploaded by the user, but before the extension. (e.g. user_file_name.csv becomes automatically user_file_name_hello.csv)
Optional: I'd like the original file to be first renamed automatically, then saved to my uploads directory.
models.py
from django.db import models
# validation method to check if file is csv
from django.core.exceptions import ValidationError
def validate_file_extension(value):
if not value.name.endswith('.csv'):
raise ValidationError(u'Only CSV files allowed.')
# Create your models here.
class user_file(models.Model):
user_file_csv = models.FileField(upload_to='documents/user_files/', validators=[validate_file_extension])
forms.py
from django import forms
from .models import user_file
from django.forms import FileInput
class user_file_form(forms.ModelForm):
class Meta:
model = user_file
widgets = {'user_file_csv': FileInput(attrs={'accept': 'text/csv'})}
fields = ('user_file_csv',)
Thank you!
Maybe you need something like this:
class FileUploadUtil:
#staticmethod
def my_files_path(instance, filename):
name, file_extention = os.path.splitext(filename)
name = 'prefix-{}-{}-sufix.{}'.format(name, instance.id, file_extention)
return "my_files/{}".format(name)
class MyModel(models.Model):
# Other fields
# ...
my_file = models.FileField(max_length=300, upload_to=FileUploadUtil.my_files_path)
Optional: I'd like the original file to be first renamed automatically, then saved to my uploads directory.
You can override save() method. Check here
Django document
Maybe You need decorator.
from pathlib import Path
def rename_helper(path: str, append_text: str):
stem, suffix = Path(path).stem, Path(path).suffix
return f"{stem}{append_text}{suffix}"
def rename_previous_image(func):
""" return wrapper object """
def wrapper(*args, **kwargs):
self = args[0]
model = type(self)
previous_obj = model.objects.filter(pk=self.pk)
if previous_obj.exists():
old_name_with_path = Path(str(previous_obj[0].user_file_csv))
Path.rename(old_name_with_path , rename_helper(path=old_name_with_path , append_text="_hello"))
return func(*args, **kwargs)
return wrapper
And, You can decorate your model save() method.
class MyModel(models.Model):
# Other fields
# ...
my_file = models.FileField(max_length=300, upload_to=FileUploadUtil.my_files_path)
#rename_previous_image
def save(self, **kwargs):
super(user_file, self).save(**kwargs) # You must add This row.
besides,
recommend rename your user_file class
like UserFile
Check This PEP 8
Have a good day.

Changing save location of file for a model inside a view

I have the following model.
class Image(models.Model):
customer = models.ForeignKey(Customer, related_name='images')
image = models.ImageField(upload_to='/pictures/', verbose_name='Image')
Each time user add a new Image I want it to go under pictures/customer.id/custom_filename
When I was using local filesystem, that was easy. I used a function that handled file chunks upload with a new name and returned the new path. But now I want to use S3 to store my files. So I am using django-storages. I did the following tests:
testmodel
class TestModel(models.Model):
name = models.CharField(max_length=10)
logo = models.ImageField(upload_to='pictures/')
when I do this in my view
def index(request):
form = TestModelForm(request.POST or None, request.FILES or None)
if request.method == 'POST':
if form.is_valid():
print 'posting form'
model = form.save(commit=False)
print model.name
print model.logo.url
model.save()
and the image is uploaded as it should under mybucket.s3.amazon.com/pictures/logoname.jpg
But if I change the file name by some way like
def index(request):
form = TestModelForm(request.POST or None, request.FILES or None)
if request.method == 'POST':
if form.is_valid():
print 'posting form'
model = form.save(commit=False)
print model.name
filename = str(request.FILES['logo']).split('.')[0]
extension = str(request.FILES['logo']).split('.')[1]
path = '%d%d%d_%s_%d.%s' %(datetime.now().year, datetime.now().month, datetime.now().day, filename, models_no+1, extension)
model.logo = path
print model.logo.url
model.save()
i get a new url which is mybucket.s3.amazon.com/newlogoname.jpg (correct since I didn't user pictures in the new path) but the file is not uploaded. Must I do it manually using boto? I guess it's the only way since folders for each customer (initial example) might not exist and need to be created. What is the correct way to upload images at custom locations/directories for each model?
I tried to call default storage directly to save the image in the location I wanted but didn't work
default_storage.save('pictures/one.jpg', ContentFile(request.FILES['logo']))
The key is created (the folder and the file) but the image is empty. I think using cloud storage in a flexible way is very difficult :(
You can pass a callable as the upload_to named argument, in your model definition (so you actually don't have to do this on the view level). To make it clear, try doing something like this:
class Image(models.Model):
customer = models.ForeignKey(Customer, related_name='images')
image = models.ImageField(upload_to=self.generate_upload_path, verbose_name='Image')
def generate_upload_path(self, filename):
return os.path.join('/pictures/'+str(self.customer.id), filename)

How can I upload two images in a form to 2 different directories in django?

model:
class Model1(models.Model):
...
pic1 = models.ImageField()
pic2 = models.ImageField()
How can I upload two images in a form to 2 different directories in django? For example, pic1 in MEDIA_ROOT/pic1/ and pic2 in MEDIA_ROOT/pic2/?
I have read the django document, but I can't find the answer with upload_to argument of FileField.
updated: Sorry to mention, when uploadding the images to 2 different directories, how can I rename them respectively? pic1 will be saved to MEDIA_ROOT/pic1/year-month-day-original_name.jpg and pic2 to MEDIA_ROOT/pic2/year-month-day-original_name.jpg
models.py
def get_pic1_path(instance, filename):
return new_path(path="pic1", filename)
def get_pic2_path(instance, filename):
return new_path(path="pic2", filename)
def new_path(path, filename):
today = date.now()
today_path = today.strftime("%Y-%m-%d")
image_path = "{0}-{1}".format(today_path, filename)
return os.path.join(path, image_path)
class Model1(models.Model):
...
pic1 = models.CustomImageField(upload_to=get_pic1_path)
pic2 = models.ImageField(upload_to=get_pic2_path)
views.py
...
if request.method == 'POST':
form = Model1Form(request.POST, request.FILES)
if form.is_valid():
image = Model1.objects.create()
pic1 = request.FILES['pic1']
pic2 = request.FILES['pic2']
image.pic1.save(pic1.name, image_file)
image.pic2.save(pic2.name, image_file)
...
I think I must have misunderstood your question. Surely this does exactly what you're describing -
class Model1(models.Model):
...
pic1 = models.ImageField(upload_to="pic1")
pic2 = models.ImageField(upload_to="pic2")
The file in the pic1 field will be uploaded to MEDIA_ROOT/pic1/ and the file in the pic2 field will be uploaded to MEDIA_ROOT/pic2/.
UPDATE
Have a look at the docs on the upload_to parameter - you can pass it a function to create the path dynamically, as described in Catherines answer.
I know it's not quite what you're planning, but if you wanted something super simple to organise your files, then it's worth noting that you can also use strftime notation in the upload_to parameter.
pic1 = models.ImageField(upload_to='pic1/%Y/%m/%d')
This would give you a path like - /path/to/media_root/pic1/2013/03/11/filename.jpg

How to use django-filebrowser rename a file before uploading it to a folder

like django's upload_to
def upload_to(instance, filename):
filename = time.strftime('%Y%m%d%H%M%S')
ym = time.strftime('%Y%m')
return 'uploads/%s/%s.jpg' % (ym,filename)
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
photo = models.ImageField(u"Image (Upload)",upload_to=upload_to)
file saved 'uploads/%s/%s.jpg'
but change for
photo = FileBrowseField("Image", max_length=200, directory="uploads/", extensions=[".jpg"], blank=True, null=True)
How to rename a file before uploading it to a folder
like django's upload_to
In filebrowser/sites.py you can create a hook for this when uploading / handling uploads:
def _upload_file(self, request):
"""
Upload file to the server.
"""
if request.method == "POST":
folder = request.GET.get('folder', '')
if len(request.FILES) == 0:
return HttpResponseBadRequest('Invalid request! No files included.')
if len(request.FILES) > 1:
return HttpResponseBadRequest('Invalid request! Multiple files included.')
filedata = list(request.FILES.values())[0]
fb_uploadurl_re = re.compile(r'^.*(%s)' % reverse("filebrowser:fb_upload", current_app=self.name))
folder = fb_uploadurl_re.sub('', folder)
path = os.path.join(self.directory, folder)
# we convert the filename before uploading in order
# to check for existing files/folders
file_name = convert_filename(filedata.name)
filedata.name = file_name
file_path = os.path.join(path, file_name
....
You can modify file_path here to whatever you like, or modify the file name.
For those of you that just want to ensure that files are not overwritten, you can set the FILEBROWSER_OVERWRITE_EXISTING flag in your settings.py as such:
FILEBROWSER_OVERWRITE_EXISTING = False
This will ensure that when you edit your files you give them a unique name, and it also ensures new uploads get their filename converted to something unique using filebrowsers convert_filename method defined in filebrowser/utils.py
More on filebrowser settings here. Hope this helps :)
For all these many years of this question and similar questions on other sites, I have not found a ready-made solution, but only hints of it. It's time to post the solution:
signals.py:
from filebrowser.signals import filebrowser_pre_upload
import os
import time
def pre_upload_callback(sender, **kwargs):
fullname = kwargs['file'].name
filename, extension = os.path.splitext(fullname)
new_filename = time.strftime('%Y%m%d%H%M%S')
kwargs['file'].name = new_filename + extension
filebrowser_pre_upload.connect(pre_upload_callback)
I hope I helped someone

Import csv data into database in Django Admin

I've tried to import a csv file into a database by tweaking the modelform inside the admin doing this:
models.py:
class Data(models.Model):
place = models.ForeignKey(Places)
time = models.DateTimeField()
data_1 = models.DecimalField(max_digits=3, decimal_places=1)
data_2 = models.DecimalField(max_digits=3, decimal_places=1)
data_3 = models.DecimalField(max_digits=4, decimal_places=1)
Forms.py:
import csv
class DataImport(ModelForm):
file_to_import = forms.FileField()
class Meta:
model = Data
fields = ("file_to_import", "place")
def save(self, commit=False, *args, **kwargs):
form_input = DataImport()
self.place = self.cleaned_data['place']
file_csv = request.FILES['file_to_import']
datafile = open(file_csv, 'rb')
records = csv.reader(datafile)
for line in records:
self.time = line[1]
self.data_1 = line[2]
self.data_2 = line[3]
self.data_3 = line[4]
form_input.save()
datafile.close()
Admin.py:
class DataAdmin(admin.ModelAdmin):
list_display = ("place", "time")
form = DataImport
admin.site.register(Data, DataAdmin)
But i'm stuck trying to import the file i put in "file_to_import" field. Getting AttributeError in forms.py : 'function' object has no attribute 'FILES'.
What i'm doing wrong?
After a long search i found an answer: Create a view inside the admin using a standard form
Form:
class DataInput(forms.Form):
file = forms.FileField()
place = forms.ModelChoiceField(queryset=Place.objects.all())
def save(self):
records = csv.reader(self.cleaned_data["file"])
for line in records:
input_data = Data()
input_data.place = self.cleaned_data["place"]
input_data.time = datetime.strptime(line[1], "%m/%d/%y %H:%M:%S")
input_data.data_1 = line[2]
input_data.data_2 = line[3]
input_data.data_3 = line[4]
input_data.save()
The view:
#staff_member_required
def import(request):
if request.method == "POST":
form = DataInput(request.POST, request.FILES)
if form.is_valid():
form.save()
success = True
context = {"form": form, "success": success}
return render_to_response("imported.html", context,
context_instance=RequestContext(request))
else:
form = DataInput()
context = {"form": form}
return render_to_response("imported.html", context,
context_instance=RequestContext(request))
The rest is part of this post:
http://web.archive.org/web/20100605043304/http://www.beardygeek.com/2010/03/adding-views-to-the-django-admin/
Take a look at django-admin-import, it does more or less exactly what you want -- you can upload a XLS (not a CSV, but that should not matter) and lets you assign columns to model fields. Default values are also supported.
https://pypi.org/project/django-admin-import/
Additionally, it does not take away the possibility to modify individual records by hand because you don't have to replace the default model form used in the administration.
In the save() method, you don't have any access to the request object - you can see that it's not passed in. Normally you would expect to have a NameError there, but I suspect that you've got a function elsewhere in the file called request().
At the point of saving, all the relevant data should be in cleaned_data: so you should be able to do
file_csv = self.cleaned_data['file_to_import']
At that point you'll have another problem, which is when you get to open - you can't do that, as file_to_import is not a file on the server filesystem, it's an in-memory file that has been streamed from the client. You should be able to pass file_csv directly to csv.reader.