Django File (and Image) Uploads Tutorial

Will Vincent - Feb 5 '20 - - Dev Community

This tutorial shows how to implement file and then image uploading with Django. We'll build a basic Instagram clone.

Setup

If you're on a Mac the Desktop is a convenient place to put our code. The location doesn't matter; it just needs to be easily available.

On the command line, navigate there and create a directory insta for our files. We will use Pipenv to install both Django and pillow which is a Python image process library Django relies on for image files. For non-image file uploads, pillow is not needed. Finally activate our new virtual environment with the shell command.

$ cd ~/Desktop
$ mkdir insta && cd insta
$ pipenv install django==3.0.3 pillow==7.0.0
$ pipenv shell
(insta) $
Enter fullscreen mode Exit fullscreen mode

You should see (insta) going forward to indicate we're in an active virtual environment. You can type exit at any time to leave it and pipenv shell to re-enter.

Project and App

Now create our new Django project called insta_project and a new app called posts.

(insta) $ django-admin startproject insta_project .
(insta) $ python manage.py startapp posts
Enter fullscreen mode Exit fullscreen mode

Since we've added a new app we need to tell Django about it at the bottom of the INSTALLED_APPS configuration in settings.py.

# insta_project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'posts.apps.PostsConfig', # new
]
Enter fullscreen mode Exit fullscreen mode

Now run python manage.py migrate to setup the new database for our project.

(insta) $ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
Enter fullscreen mode Exit fullscreen mode

Models

Starting with the database model is a good choice. In our case our model Post will only have two fields: title and cover. We'll also include a __str__ method below so that the title appears in our Django admin later on.

# posts/models.py
from django.db import models


class Post(models.Model):
    title = models.TextField()
    cover = models.ImageField(upload_to='images/')

    def __str__(self):
        return self.title
Enter fullscreen mode Exit fullscreen mode

The location of the uploaded image will be in MEDIA_ROOT/images. In Django, the MEDIA_ROOT setting is where we define the location of all user uploaded items. We'll set that now.

If we wanted to use a regular file here the only difference could be to change ImageField to FileField.

MEDIA_ROOT

Open up insta_project/settings.py in your text editor. We will add two new configurations. By default MEDIA_URL and MEDIA_ROOT are empty and not displayed so we need to configure them:

  • MEDIA_ROOT is the absolute filesystem path to the directory for user-uploaded files
  • MEDIA_URL is the URL we can use in our templates for the files
# insta_project/settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
Enter fullscreen mode Exit fullscreen mode

We could pick a name other than media here but this is the Django convention. We'll also make an images folder within it to use shortly.

(insta) $ mkdir media
(insta) $ mkdir media/images
Enter fullscreen mode Exit fullscreen mode

Admin

Now update the posts/admin.py file so we can see our Post app in the Django admin.

# posts/admin.py
from django.contrib import admin

from .models import Post

admin.site.register(Post)
Enter fullscreen mode Exit fullscreen mode

And we're all set! Generate a new migrations file.

(insta) $ python manage.py makemigrations
Migrations for 'posts':
  posts/migrations/0001_initial.py
    - Create model Post
Enter fullscreen mode Exit fullscreen mode

Then run migrate to update the database.

(insta) $ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, posts, session
s
Running migrations:
  Applying posts.0001_initial... OK
Enter fullscreen mode Exit fullscreen mode

Now we can create a superuser account to access the admin and then execute runserver to spin up the local web server for the first time.

(insta) $ python manage.py createsuperuser
(insta) $ python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

If you go to http://127.0.0.1:8000/admin you'll be able to log in to the Django admin site. It should redirect you to this page:

Admin Homepage

Click on the "+ Add" link next to Posts. You can add whatever you like but for this tutorial I'm making a post on the Django Pony mascot. You can download it here yourself if you like.

Django Pony Post

Upon "Save" you will be redirected to the Posts page where we can see all our posts.

Image Posts

If you look within the local media folder in your project you'll see under images there is now the djangopony.png image file. See! I told you that was what MEDIA_URL would do.

Ok, so at this point we're done with the basics. But let's take it a step further and display our posts which means urls.py, views.py, and template files.

URLs

The confusing thing about Django is that you often need 4 different but interconnected files for one webpage: models.py, urls.py, views.py, and a template html file. I find it easiest to reason about this by going in order from models -> urls -> views -> template files. Our model is already done so that means diving into URL routes.

We'll need two urls.py file updates. First at the project-level insta_project/urls.py files we need to add imports for settings, include, and static. Then define a route for the posts app. Note we also need to add the MEDIA_URL if settings are in DEBUG mode, otherwise we won't be able to view uploaded images locally.

# insta_project/urls.py
from django.contrib import admin
from django.conf import settings # new
from django.urls import path, include # new
from django.conf.urls.static import static # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('posts.urls')), # new
]

if settings.DEBUG: # new
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Enter fullscreen mode Exit fullscreen mode

Next we'll need to sort out the URL routes within the posts app. First create that file.

(insta) $ touch posts/urls.py
Enter fullscreen mode Exit fullscreen mode

Then we'll put all posts on the homepage so again use the empty string '' as our route path.

# posts/urls.py
from django.urls import path

from .views import HomePageView

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
]
Enter fullscreen mode Exit fullscreen mode

This references a view called HomePageView which we'll create next.

Views

We can use the generic class-based ListView here, import our Post model, and then create a HomePageView that uses the model and a template called home.html.

# posts/views.py
from django.views.generic import ListView
from .models import Post


class HomePageView(ListView):
    model = Post
    template_name = 'home.html'
Enter fullscreen mode Exit fullscreen mode

Boom! Moving on the last step is that template file called home.html.

Templates

We have two choices for our template's location. We could put it within the posts app at posts/templates/posts/home.html but I find that structure redundant. Plus it's harder to reason about templates when they are all buried within their respective apps. So typically I will instead create a project-level templates directory.

$ mkdir templates
$ touch templates/home.html
Enter fullscreen mode Exit fullscreen mode

We tell Django to also look here for any templates by updating the TEMPLATES configuration within insta_project/settings.py.

# insta_project/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # new
        ...
    },
]
Enter fullscreen mode Exit fullscreen mode

Our home.html template file will display the title and image for all posts. Just like Instagram would :)

<!-- templates/home.html -->
<h1>Django Image Uploading</h1>
<ul>
  {% for post in object_list %}
    <h2>{{ post.title }}</h2>
    <img src="{{ post.cover.url}}" alt="{{ post.title }}">
  {% endfor %}
</ul>
Enter fullscreen mode Exit fullscreen mode

Ok, that's it! Make sure the server is running with the python manage.py runserver command and navigate to our homepage at http://127.0.0.1:8000. Refresh the page if needed.

Homepage

And voila! If you add additional posts with a title and image via the admin they will appear on the homepage.

Form

Now we can add a form so regular users, who wouldn't have access to the admin, can also add posts. That means creating a new page with a form.

Let's start with the views.py file. We'll name our new view CreatePostView which will extend the built-in Django CreateView. We'll also import reverse_lazy to handle the redirect back to our homepage after the form has been submitted.

Within the view we specify the model, a form_class which we'll create next, the template_name, and finally a success_url which is what we want to happen after submission.

# posts/views.py
from django.views.generic import ListView, CreateView # new
from django.urls import reverse_lazy # new

from .forms import PostForm # new
from .models import Post

class HomePageView(ListView):
    model = Post
    template_name = 'home.html'

class CreatePostView(CreateView): # new
    model = Post
    form_class = PostForm
    template_name = 'post.html'
    success_url = reverse_lazy('home')
Enter fullscreen mode Exit fullscreen mode

Next up that form. First create it.

(insta) $ touch posts/forms.py
Enter fullscreen mode Exit fullscreen mode

We can extend Django's built-in ModelForm. All we need for our basic form is to specify the correct model Post and the fields we want displayed which are title and cover.

# posts/forms.py
from django import forms
from .models import Post

class PostForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = ['title', 'cover']
Enter fullscreen mode Exit fullscreen mode

We'll make a dedicated page for this form at the path of post/.

# posts/urls.py
from django.urls import path

from .views import HomePageView, CreatePostView # new

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
    path('post/', CreatePostView.as_view(), name='add_post') # new
]
Enter fullscreen mode Exit fullscreen mode

Then create the new template.

(insta) $ touch templates/post.html
Enter fullscreen mode Exit fullscreen mode

And fill it with a headline and form. It's important to always add csrf_token for protection. We're specifiying form.as_p which means Django will output each field as a paragraph tag.

<!-- templates/post.html -->
<h1>Create Post Page</h1>
<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Submit New Post</button>
</form>
Enter fullscreen mode Exit fullscreen mode

That's it! Make sure your server is running and go to the page at http://127.0.0.1:8000/post/.

Create Post

After you submit a new post you'll be redirected back to the homepage and will see all the posts.

Next Steps

Hosting this site in production would require a few additional steps. Notably, it's likely that you would use WhiteNoise on the server for your static files, however WhiteNoise explicitly does not support media files. The common practice is to use django-storages for this purpose and connect to something like S3.

What else? You probably want to put restrictions around the image size which can be done initially in the models.py file or with CSS. Perhaps you want to add edit and delete options as well for the Post. And you'll likely want a thumbnail version of the images too which can be done with sorl-thumbnail.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .