The following documentation can be used to add the ability to upload files to a model. In addition, the files can be downloaded from the admin interface. The steps are working/tested on Django 4.2.1.
For this example the following assumptions are made:
projectmyappExampleDjango must be configured with a directory to upload the files to. In the projects settings.py (eg. project/project/settings.py) define the MEDIA_ROOT.
I am using the django-environ module, so my configuration looks like:
from pathlib import Path
import environ
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Read environment variables
# By default, the development environment is used
env = environ.FileAwareEnv()
environ.FileAwareEnv.read_env(
env_file=os.path.join(BASE_DIR, os.environ.get("ENV_FILE", "development.env"))
)
# Define the path that file uploads will be stored in
MEDIA_ROOT = env.str(
"MEDIA_ROOT",
default=os.path.join(BASE_DIR, "media/")
)
# Other Django configuration below
A couple of changes need to be made to the model:
Example object is deletedIn this case the project.myapp.Example model is stored in project/myapp/models/example.py; the changes should be made there.
In the model file, create a function that will define where the uploaded file is stored. I recommend specifying the application and model name in the path.
def generate_file_path(instance, filename: str) -> str:
"""Generate the file path where the uploaded file will be saved to
Args:
instance (Example): The Example object instance
filename (str): The name of the file that will be stored
Returns:
str: The storage location for the file
"""
return f"uploaded-files/myapp/example/{filename}"
You may add your own logic for the file path, as an example if you want to upload the file to a directory field specified in the model you would change the return to:
return f"uploaded-files/myapp/example/{instance.directory}/{filename}"
A FileField needs to be added to the model definition; the file field will contain the path that is generated by generate_file_path():
file = models.FileField(
upload_to=generate_file_path,
)
While optional, I also create a text field which contains the original file name that was uploaded:
filename = models.CharField(
help_text="The original name of the uploaded file",
max_length=255,
)
Again this is optional. Since I want the file on disk to be removed once the object is removed, a signal needs to be added like this:
import os
from django.dispatch import receiver
# Example model definition here
# ...
@receiver(models.signals.post_delete, sender=Example)
def delete_uploaded_file(sender, instance, **kwargs):
"""Remove the uploaded file on removal of an object
Args:
sender: The sender
instance (Example): The Example object instance that is being removed
"""
# Only continue if a file to remove is defined
if not instance.file:
return
# Remove the file if it exists
if os.path.isfile(instance.file.path):
os.remove(instance.file.path)
A view needs to be created which will allow the uploaded file to be downloaded. The view should also ensure that the user is allowed to download the file; in this case I am only checking that the user has access to the Django admin area (is staff member). You may add your own validation logic.
In the views.py file (eg. project/myapp/views.py) create the FileView:
from urllib.parse import quote
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.views.generic import DetailView
from django.views.generic.base import ContextMixin
from cdr.models import File
class BaseContextMixin(ContextMixin):
template_context = {}
def get_context_data(self, *args, **kwargs):
data = super(BaseContextMixin, self).get_context_data(*args, **kwargs)
if callable(self.template_context):
data.update(self.template_context())
else:
data.update(self.template_context)
return data
class FileView(BaseContextMixin, DetailView):
queryset = File.objects.all()
slug_field = "id"
def get(self, request, *args, **kwargs):
if request.user.is_staff:
instance = self.get_object()
response = HttpResponse(
instance.file,
content_type="application/force-download",
)
response["Content-Disposition"] = "attachment; filename={}".format(
quote(instance.name)
)
return response
else:
raise PermissionDenied()
A URL pattern for FileView needs to added to urls.py otherwise Django has no idea what to do with the requests for the file. In this case I edit project/project/urls.py and add the following:
from myapp.views import FileView
# Include the file download view
urlpatterns.append(
path("uploaded-files/myapp/example/<int:pk>/", FileView.as_view(), name="example_file")
)
Finally to provide a link to the uploaded file in the Django admin area, edit the admin file (eg. project/myapp/admin/example.py).
First create a widget (define this BEFORE the admin class):
from django.urls import reverse
from django.contrib.admin import widgets
class DownloadFileWidget(widgets.AdminFileWidget):
id = None
template_name = 'widgets/download_file_input.html'
def __init__(self, id, attrs=None):
self.id = id
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
print(self, name, value, attrs, self.id)
context['download_url'] = reverse('example_file', kwargs={'pk': self.id})
return context
Add the following to the admin class for the model:
from django.contrib import admin
from django.utils.html import format_html
class ExampleAdmin(admin.ModelAdmin):
# Your fields and other admin configuration
my_id_for_formfield = None
def get_form(self, request, obj=None, **kwargs):
if obj:
self.my_id_for_formfield = obj.id
return super(ExampleAdmin, self).get_form(request, obj=obj, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
if self.my_id_for_formfield:
if db_field.name == 'file':
kwargs['widget'] = DownloadFileWidget(id=self.my_id_for_formfield)
return super(ExampleAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def _get_download_url(self, instance):
return format_html('<a href="{}">{}</a>', reverse('example_file', kwargs={'pk': instance.id}), instance.name)
_get_download_url.short_description = 'Download File'
Finally, in the fields list you can then add _get_download_url which will return the file name and a link to download the file.