Django - how to create a file and save it to a model's FileField?
Asked Answered
V

4

159

Here's my model. What I want to do is generate a new file and overwrite the existing one whenever a model instance is saved:

class Kitten(models.Model):
    claw_size = ...
    license_file = models.FileField(blank=True, upload_to='license')

    def save(self, *args, **kwargs):
        #Generate a new license file overwriting any previous version
        #and update file path
        self.license_file = ???
        super(Request,self).save(*args, **kwargs)

I see lots of documentation about how to upload a file. But how do I generate a file, assign it to a model field and have Django store it in the right place?

Vested answered 22/9, 2011 at 12:49 Comment(0)
S
217

You want to have a look at FileField and FieldFile in the Django docs, and especially FieldFile.save().

Basically, a field declared as a FileField, when accessed, gives you an instance of class FieldFile, which gives you several methods to interact with the underlying file. So, what you need to do is:

self.license_file.save(new_name, new_contents)

where new_name is the filename you wish assigned and new_contents is the content of the file. Note that new_contents must be an instance of either django.core.files.File or django.core.files.base.ContentFile (see given links to manual for the details).

The two choices boil down to:

from django.core.files.base import ContentFile, File

# Using File
with open('/path/to/file') as f:
    self.license_file.save(new_name, File(f))

# Using ContentFile
self.license_file.save(new_name, ContentFile('A string with the file content'))
Sixth answered 22/9, 2011 at 13:7 Comment(9)
Ok, I think that will work but I'm getting into some kind of recursive loop calling that in the save method. It just keeps creating files forever.Vested
For the recursive issue I have to call self.license_file.save with the arg save=False.Vested
This (ContentFile) works perfectly with the file string returned by django-wkhtmltopdf's convert_to_pdf command. Thank you!!Tourney
In addition to this, I got an error if I don't specify the file mode while opening the file. So, f = open('/path/to/file', 'r') For ZIP kind of file, f = open('/path/to/file.zip', 'rb')Voter
In my case, the above was not saving the file into the folder. Turns out the issue is that I'm using docker-compose to run my django app along with a celery worker. The django app volume for MEDIA_ROOT was not shared with the same volume in the celery worker. Sharing the named volume fixed it (ref).Boiler
where does File come from?Elysia
@Elysia from django.core.files import FileBinoculars
@Elysia , do we need to close the file object explicitly?Sinus
Note that you don't want to pass in a folder path for the filename - that's the job of the upload_to() property of the FileField in the model. IOTW you have on the FileField upload_to="licenses/uploads/%Y/%m/%d/", and you do self.license_file.save(new_name, File(f)) and then self.license_file.name you'll get `'licenses/uploads/2021/08/16/myfile.txt'Benempt
B
33

Accepted answer is certainly a good solution, but here is the way I went about generating a CSV and serving it from a view.

Thought it was worth while putting this here as it took me a little bit of fiddling to get all the desirable behaviour (overwrite existing file, storing to the right spot, not creating duplicate files etc).

Django 1.4.1

Python 2.7.3

#Model
class MonthEnd(models.Model):
    report = models.FileField(db_index=True, upload_to='not_used')

import csv
from os.path import join

#build and store the file
def write_csv():
    path = join(settings.MEDIA_ROOT, 'files', 'month_end', 'report.csv')
    f = open(path, "w+b")

    #wipe the existing content
    f.truncate()

    csv_writer = csv.writer(f)
    csv_writer.writerow(('col1'))

    for num in range(3):
        csv_writer.writerow((num, ))

    month_end_file = MonthEnd()
    month_end_file.report.name = path
    month_end_file.save()

from my_app.models import MonthEnd

#serve it up as a download
def get_report(request):
    month_end = MonthEnd.objects.get(file_criteria=criteria)

    response = HttpResponse(month_end.report, content_type='text/plain')
    response['Content-Disposition'] = 'attachment; filename=report.csv'

    return response
Billhead answered 14/2, 2013 at 22:18 Comment(0)
R
12

It's good practice to use a context manager or call close() in case of exceptions during the file saving process. Could happen if your storage backend is down, etc.

Any overwrite behavior should be configured in your storage backend. For example S3Boto3Storage has a setting AWS_S3_FILE_OVERWRITE. If you're using FileSystemStorage you can write a custom mixin.

You might also want to call the model's save method instead of the FileField's save method if you want any custom side-effects to happen, like last-updated timestamps. If that's the case, you can also set the name attribute of the file to the name of the file - which is relative to MEDIA_ROOT. It defaults to the full path of the file which can cause problems if you don't set it - see File.__init__() and File.name.

Here's an example where self is the model instance where my_file is the FileField / ImageFile, calling save() on the whole model instance instead of just FileField:

import os
from django.core.files import File

with open(filepath, 'rb') as fi:
    self.my_file = File(fi, name=os.path.basename(fi.name))
    self.save()
Rathbun answered 6/9, 2019 at 17:14 Comment(2)
I didn't have any issue with this approach. Worthy to note also is that this obeys the FileField's upload_to property.Sorb
That should be the preferred answer.Primero
P
0

As an alternative for doing this in save(), I overrode form_valid()'s CreateView to achieve this (i.e. creating a file and saving it to a FileField).

In the example below I'm generating a PDF with Weasyprint and am saving it to the model's request_pdf field upon form submission.

def form_valid(self, form):

    html_template = get_template('request_pdf.html')
    pdf_dict = {}
    pdf_dict['something'] = form.instance.something
    rendered_html = html_template.render(pdf_dict).encode(encoding="UTF-8")

    base_url = self.request.build_absolute_uri("/")
    pdf_file_bytes = HTML(string=rendered_html, base_url=base_url).write_pdf()
    form.instance.request_pdf = ContentFile(pdf_file_bytes)

    # We need to specify the filename, else the file won't be saved
    form.instance.request_pdf.name = "my_filename"

    return super(MyCreateView, self).form_valid(form)
Pennsylvanian answered 23/4, 2023 at 7:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.