Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 46

Handling File Uploads in Django Models

Recommended resources

Hello! Welcome back to building your task manager.

In our last lesson, you implemented a clean and effective pagination system, making your application much more user-friendly for handling large numbers of tasks. You also learned how to make pagination work seamlessly with search and filtering.

Today, we're adding another significant feature: the ability for users to attach files to their tasks. This is a common requirement in many web applications.

Today's goal is to add a FileField or ImageField to a model to handle file uploads. We'll focus on the backend setup: modifying your Task model, configuring Django to handle media files, and testing the uploads through the Django admin panel.

By the end of this lesson, your database and project will be ready to store file attachments for each task.

1. Understanding Media Files and Model Fields

In Django, user-uploaded content, like images or documents, are called "media files." They are handled differently from "static files" (like CSS and your own site images), which are part of your project's code.

To store a reference to an uploaded file in your database, Django provides two main field types:

  • FileField: A general-purpose field for any type of file (PDFs, documents, zip files, etc.).
  • ImageField: A special type of FileField that validates if the uploaded file is an image. It also provides extra attributes like image height and width.

To use ImageField (and it's good practice even when just using FileField), Django relies on a Python library called Pillow. Let's install it first. Make sure your virtual environment is activated and run:

pip install pillow

Now we can modify our model.


2. Modifying the Task Model

Let's add a field to your Task model to store an optional attachment. We'll use a FileField as it's more versatile for a task manager.

  1. Open your tasks/models.py file (or whatever you named your app).
  2. Add a new field to your Task model.
# In your_app/models.py

from django.db import models
from django.contrib.auth.models import User

class Task(models.Model):
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, null=True, blank=True)
    title = models.CharField(max_length=200)
    description = models.TextField(null=True, blank=True)
    complete = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    # Add the new field below
    attachment = models.FileField(upload_to='attachments/%Y/%m/%d/', null=True, blank=True)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['complete']

Let's break down the new field: attachment = models.FileField(...)

  • upload_to='attachments/%Y/%m/%d/': This is the most important argument. It tells Django where to save the uploaded files. The path is relative to your media root (which we'll define next). Using format strings like %Y/%m/%d is a great way to organize files into date-based subdirectories, preventing one giant folder.
  • null=True, blank=True: We make the field optional. A user doesn't have to upload a file for every task.

The following tutorial provides a concise written guide to this step.

Django File/Image Uploads Tutorial

Read the 'Models' section of this LearnDjango tutorial. It clearly shows how to add an ImageField (which is very similar to the FileField we just added) and explains the upload_to parameter.

Focus on the Post model example. Note how it uses models.ImageField with upload_to='images/'. This directly corresponds to what we did with FileField.


3. Configuring Settings for Media Files

Now, we need to tell Django two things: where to store media files on the file system (MEDIA_ROOT) and what URL to use to serve them (MEDIA_URL).

Open your project's settings.py file (e.g., project_name/settings.py) and add the following two lines at the bottom, near your STATIC_URL setting.

# In your_project/settings.py

# ... other settings ...

STATIC_URL = 'static/'

# Add these two lines
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
  • MEDIA_URL = '/media/': This is the URL prefix that will be used to access the files. For example, a file will be available at http://127.0.0.1:8000/media/attachments/some-file.pdf.
  • MEDIA_ROOT = BASE_DIR / 'media': This tells Django to create a media folder in the root directory of your project to store all uploaded files.
Django Project Configuration for Media File Uploads
This image shows the exact configuration for `MEDIA_URL` and `MEDIA_ROOT` in your `settings.py` file.
Django Project Media Folder Structure
Your project structure will now include this `media` directory where uploaded files will be stored.

This video from Dave Gray offers a clear walkthrough of this configuration step.

Python Django Images - How to Upload & Display Images

Watch this short clip to see the settings configuration in action. Hearing the explanation can help solidify the concepts.

Watch from 01:08 to 02:37. The video walks through adding MEDIA_URL and MEDIA_ROOT to the settings.py file, just as we did.


4. Configuring URLs for Development

By default, Django's development server does not serve user-uploaded media files for security reasons. We need to explicitly tell it to do so for our development environment.

Open your project's main urls.py file (e.g., project_name/urls.py) and modify it to serve files from MEDIA_ROOT when in debug mode.

# In your_project/urls.py

from django.contrib import admin
from django.urls import path, include

# Add these two imports
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('tasks.urls')), # Or whatever your app is named
]

# Add this line at the end
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

This snippet checks if your project is in DEBUG mode (which it is during development) and, if so, adds a new URL pattern that knows how to serve the files you upload.

Test your understanding!

Why is it important that this URL pattern is only added when settings.DEBUG is True?

Show answer

In a production (live) environment, you should never use Django's development server to serve media or static files. It's inefficient and not built for security or high traffic. Production setups use a dedicated web server like Nginx or a cloud storage service (like Amazon S3) to handle these files directly. The if settings.DEBUG: check ensures this inefficient method is only used during local development.


5. Applying Migrations and Testing with the Admin

With our model and settings updated, it's time to update the database.

  1. Create migrations:

    python manage.py makemigrations
    

    You should see output indicating that the attachment field was added to the Task model.

  2. Apply migrations:

    python manage.py migrate
    

    This applies the change to your actual database schema.

Now, how can we test this without building a whole new form? The Django admin panel is perfect for this!

  1. Make sure your Task model is registered in your app's admin.py file. If not, add it:

    # In your_app/admin.py
    from django.contrib import admin
    from .models import Task
    
    admin.site.register(Task)
    
  2. Run your server (python manage.py runserver) and go to the admin page (/admin).

  3. Navigate to your Tasks section and either create a new task or edit an existing one. You will now see a new "Attachment" file input field!

  4. Use the "Choose File" button to select a file from your computer and save the task.

If everything is configured correctly, Django will save the file to your project's media/attachments/ directory (inside a new date-based folder) and store the path in the database.

The following video demonstrates this exact process of migrating and testing with the admin.

Python Django Images - How to Upload & Display Images

Watch this section to see the migration and admin testing process from start to finish.

Watch from 06:11 to 09:21. The video shows running makemigrations and migrate, then logging into the admin panel to upload an image to a model instance. This is exactly what you just did to test your FileField.


6. Going Further: Cleaning Up Deleted Files

There's one loose end: if you delete a Task object through your app or the admin, the database record is removed, but the associated file remains on your server's disk, orphaned. Over time, this can waste a lot of space.

We can fix this by overriding the delete() method on our Task model.

# In your_app/models.py
# Add this inside your Task model class

    def delete(self, *args, **kwargs):
        # You have to prepare what to do with the file here,
        # because after super().delete() it will be gone
        if self.attachment:
            self.attachment.delete()
        super().delete(*args, **kwargs)  # Call the "real" delete() method

With this addition, whenever you delete a Task instance, Django will first call our custom delete method. We check if an attachment exists and tell it to delete the file from the storage system. Then, super().delete() proceeds with deleting the database record.

This short video explains the concept clearly.

Django Media Files - Handling User Uploads in Django Forms & Models

This clip from BugBytes demonstrates how to override the model's delete method to remove associated files from the filesystem.

Watch from 26:53 to 29:19. The video shows how to add the delete method to the model and explains why super().delete() is called at the end. It also mentions an important caveat about queryset deletes, which is good to be aware of.


Conclusion

Fantastic! You have successfully configured your Django project to handle file uploads. You've modified the Task model with a FileField, correctly set up MEDIA_ROOT and MEDIA_URL, and enabled file serving in development. Most importantly, you were able to test the entire backend functionality using just the Django admin.

Key Takeaways:

  • User-uploaded files are "media," distinct from "static" files.
  • FileField and ImageField are used in models to store references to uploaded files.
  • MEDIA_ROOT is the filesystem path where files are saved.
  • MEDIA_URL is the URL prefix used to access those files.
  • The development server needs a special URL pattern to serve media files.
  • Overriding a model's delete() method is a clean way to remove associated files when an object is deleted.

Preview of the Next Lesson:

The backend is ready, but your users can't upload files yet. In the next lesson, we will update your forms and views to process and save user-uploaded files directly from the main application interface. You'll learn how to modify your CreateView and UpdateView to handle forms with enctype="multipart/form-data" and display the attachments.