Django File (and Image) Uploads Tutorial
Tags: django, python, tutorial, beginners
Published: 2020-02-05 19:41:51

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](https://pipenv.readthedocs.io/en/latest/) to install both Django and [pillow](https://python-pillow.org/) 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) $
```

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
```

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`.

```python
# 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
]
```

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
```

## 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.

```python
# 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
```

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

```python
# insta_project/settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
```

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
```

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

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

from .models import Post

admin.site.register(Post)
```

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
```

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
```

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
```

If you go to [http://127.0.0.1:8000/admin](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](https://dev-to-uploads.s3.amazonaws.com/i/zyxn6ayo7szofgeuuk43.png)

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](https://learndjango.com/static/images/tutorials/file_image_uploads/djangopony.png) yourself if you like.

![Django Pony Post](https://dev-to-uploads.s3.amazonaws.com/i/167shq67yk2q9ot291i5.png)

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

![Image Posts](https://dev-to-uploads.s3.amazonaws.com/i/xqznyfd8iybyra1ivfmk.png)

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.

```python
# 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)
```

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

```
(insta) $ touch posts/urls.py
```

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

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

from .views import HomePageView

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
]
```

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`.

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


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

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
```

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

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

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

```html
<!-- 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>
```

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](http://127.0.0.1:8000). Refresh the page if needed.

![Homepage](https://dev-to-uploads.s3.amazonaws.com/i/zyxn6ayo7szofgeuuk43.png)

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](https://docs.djangoproject.com/en/3.0/ref/class-based-views/generic-editing/#django.views.generic.edit.CreateView). We'll also import [reverse_lazy](https://docs.djangoproject.com/en/3.0/ref/urlresolvers/#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.

```python
# 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')
```

Next up that form. First create it.

```
(insta) $ touch posts/forms.py
```

We can extend Django's built-in [ModelForm](https://docs.djangoproject.com/en/3.0/topics/forms/modelforms/#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`.

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

class PostForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = ['title', 'cover']
```

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

```python
# 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
]
```

Then create the new template.

```
(insta) $ touch templates/post.html
```

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.

```html
<!-- 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>
```

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

![Create Post](https://dev-to-uploads.s3.amazonaws.com/i/zgied3jpd3fw86j5bwug.png)

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](http://whitenoise.evans.io) on the server for your static files, however WhiteNoise explicitly [does not support media files](http://whitenoise.evans.io/en/stable/django.html#serving-media-files). The common practice is to use [django-storages](https://django-storages.readthedocs.io/en/latest/) 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](https://github.com/jazzband/sorl-thumbnail).