Building a Django web application with authentication and file uploading

Fedor Bystrov
12 min readAug 11, 2022

--

We will create an application called Giffy. Giffy is a simple web application that lets users upload their favorite gif animations and enjoy animations uploaded by others. Our application will have an authorization, authentication, and an admin interface.

tl;dr

Don’t want to walk with me through the guide step by step? — The full source code of the project can be found here.

Prerequisites

I assume you have python installed as well as basic knowledge of python programming language. Also, I will use pipenv to manage project dependencies.

Here are the links:

Animations for this project I took from the https://giphy.com.

Creating a new project

First, we need to auto-generate a backbone of our project — a collection of settings, including database configuration, Django-specific options, and application-specific settings.

Let’s start with creating a root directory that will contain all our code:

mkdir giffy-django && cd giffy-django

Next, we will initialize pipenv. pipenv will create a virtual environment and a Pipfile with project’s dependencies for us:

pipenv shell --python 3.10

Then, we need to install Django dependency:

pipenv install django

Finally, we can initialize the Django project:

django-admin startproject giffy .

Let’s check that everything works.

Execute python manage.py runserver and open http://127.0.0.1:8000/

If you did everything right, you should see the following page:

The giffy-django folder will looke like this:

├── Pipfile
├── Pipfile.lock
├── db.sqlite3
├── giffy
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py

I will not go into details about files django created. You can read about it in the official Django guide.

Create a new app

By convention, a typical Django application consists of one or more so-called apps. Let’s create one:

python manage.py startapp giffy_app

The directory tree should look like this:

├── Pipfile
├── Pipfile.lock
├── db.sqlite3
├── giffy
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── giffy_app
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── manage.py

You can read about Django conventions and apps in the official guide.

Creating an index view

Substitute everything you have in giffy_app/views.py with the code given below

giffy_app/views.py:

from django.http import HttpResponse


def index(request):
return HttpResponse("This is giffy")

Then, create urls.py inside giffy_app folder

giffy_app/urls.py:

from django.urls import path
from . import views

urlpatterns = [
path('', views.index, name='index'),
]

Next, we need to register giffy_app/urls.py in the giffy/urls.py:

giffy/urls.py:

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

urlpatterns = [
# Include all paths from giffy app
path("", include("giffy_app.urls")),

# Default django admin interface
path("admin/", admin.site.urls),
]

Finally, we need to add giffy_app to the list of the installed apps in giffy/settings.py

giffy/settings.py:

INSTALLED_APPS = [
...
# add "giffy_app" here at the end
"giffy_app",
]

Let’s test, run python manage.py runserver You should see white page with This is giffy text on it:

Creating an Image model

Our web application will serve only one type of file — images.

Delete everything from giffy_app/models.py and paste the following:

giffy_app/models.py:

from django.conf import settings
from django.db import models

class Image(models.Model):
# image title, not blank string with maximum of 60 characters
title = models.CharField(max_length=60, blank=False)

# the uploaded image. It is important to understand that
# django will not store the image in the database.
# The image field in the database table will contain the
# image name (with max_length = 36) whereas the actual image
# will be stored in the directory defined in giffy/settings.py.
image = models.ImageField(max_length=36)

# image upload date and time
uploaded_date = models.DateTimeField()

# link to the user that uploaded the image
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)

def __str__(self) -> str:
return f"Image<{self.id}>"

After that, we need to install Pillow to be able to use ImageField.

pipenv install Pillow

Once we have a model we need to tell Django to make a migration. Djano will use it to create the image table in the database.

python manage.py makemigrations giffy_app

And tell Django to apply the migration:

python manage.py migrate

Set up static files serving

For Django to be able to save and serve uploaded images, we need to add a few settings to the giffy/settings.py

giffy/settings.py:

# append to the end of the file
MEDIA_ROOT = BASE_DIR / "images"
MEDIA_URL = "/media/"

Also, we need to add static file handler togiffy/urls.py

giffy/urls.py:

from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
path("", include("giffy_app.urls")),
path("admin/", admin.site.urls),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Let’s play with the admin interface a little bit

First, we need to register Image in the admin settings. Delete everything from the giffy_app/admin.py and paste the following:

giffy_app/admin.py:

from django.contrib import admin

from .models import Image

admin.site.register(Image)

Next, we need to create an admin account. Run the following command and follow the instructions:

python manage.py createsuperuser

Ok, now we can run the server and log in using the admin account created above.

Execute the python manage.py runserver and open http://127.0.0.1:8000/admin/login

Once you logged in you should see the following:

Notice the GIFFY_APP section and Images under it. Click on + Add next to Images

You should see the Add image form:

Let’s test it and upload a few images. To save a new image, you must fill all the fields in the form.

In the end, you should see something similar depending on the number of images you uploaded:

Ok, it’s time to return to our index view. Press the Log Out in the top right corner and let’s continue.

Displaying uploaded gifs

First, create a templates/giffy_app folders inside the giffy_app folder. Then, create a base.html template.

giffy_app/templates/giffy_app/base.html:

<!DOCTYPE html>
<html>
<head>
<title>Giffy</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
</head>

<body>
<!--
It is a base template that contains html boilerplate.
Other templates will 'extend' it by substituting
the content block below
-->
{% block content %}
{% endblock content %}
</body>
</html>

The next step is to create index.html template.

giffy_app/templates/giffy_app/index.html:

{% extends "giffy_app/base.html" %}

{% block content %}
<main>
{% for image in images %}
{% include 'giffy_app/image.html' %}
{% endfor%}
<!--
The construction above means the following:
- for each image from the images list
- go get giffy_app/image.html template, render it
and past the result here
-->
</main>
{% endblock content %}

Finally, let’s create create the image.html

giffy_app/templates/giffy_app/image.html:

<article class="centered">
<!--
Remember the { for image in images } from the index.html?
image is basically the Image model from the giffy_app/models.py
-->
<h3>{{ image.title }}</h3>
<!--
Django generates image url for us. Since the image here is
just an instance of the Image model from the giffy_app/models.py
we can access the underlying file by calling image.image
-->
<img src="{{ image.image.url }}"/>
<p>Uploaded by {{ image.uploaded_by }}</p>
</article>

The directory tree should look like this:

├── Pipfile
├── Pipfile.lock
├── db.sqlite3
├── giffy
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── giffy_app
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── giffy_app
│ │ ├── base.html
│ │ ├── image.html
│ │ └── index.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── images
│ ├── ...
└── manage.py

Once all templates are sorted out, we need to adjust the index view.

giffy_app/views.py:

from django.views import View
from django.shortcuts import render
from .models import Image


class IndexView(View):
def get(self, request):
# Get all images from the database
# and sort them by uploaded_date
images = Image.objects.order_by("uploaded_date")
return render(
request,
# render giffy_app/index.html and return it in the response
"giffy_app/index.html",
# pass images we fetched earlier to the template
{
"images": images,
},
)

and update the giffy_app/urls.py

giffy_app/urls.py:

from django.urls import path
from .views import IndexView

urlpatterns = [
path("", IndexView.as_view(), name="index"),
]

Let’s check the result. Execute the python manage.py runserver and open http://127.0.0.1:8000/

You should see something like this:

Okay, almost perfect… We forgot one important thing — styles!

Create staic/giffy_app folders in the giffy_app folder and then create styles.css with the following content.

giffy_app/static/giffy_app/styles.css:

.centered {
text-align: center;
}

And the final touch, add styles.css to base.html template:

giffy_app/templates/giffy_app/base.html:

<!--
This tells django that we will use static file (in static folder)
-->
{% load static %}
<!DOCTYPE html>
<html>

<head>
<title>Giffy</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<!-- static 'giffy_app/styles.css' returns a url of styles.css -->
<link rel="stylesheet" type="text/css" href="{% static 'giffy_app/styles.css' %}">
</head>

<body>
{% block content %}
{% endblock content %}
</body>
</html>

That’s better!

Authentication and Authorization

Okay, we can finally see our beloved animations on the main page, but there is one problem we have not addressed yet — there is no way for a regular user to upload an animation!

Only authenticated users will be able to upload images to our website. Let’s start by implementing Login, Log out and Sign up functionality!

Login and logout

Django has convenient built-in views for both log-in and log-out, we just need to create a template and add log-in and log-out views to giffy_app/urls.py

giffy_app/templates/giffy_app/login.html:

{% extends "giffy_app/base.html" %}

{% block content %}
<div class="centered">
<h3>Login</h3>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="submit"/>
</form>
</div>
{% endblock content %}

giffy_app/urls.py:

from django.urls import path
# import default django LoginView and LogoutView
from django.contrib.auth.views import LoginView, LogoutView
from .views import IndexView

app_name = "giffy_app"

urlpatterns = [
path("", IndexView.as_view(), name="index"),

path(
"login/",
LoginView.as_view(
# name of the login template
template_name="giffy_app/login.html",
# user will be redirected to index page upon successful login
next_page="giffy_app:index",
redirect_authenticated_user=True,
),
name="login",
),

path(
"logout/",
# user will be redirected to index page upon logout
LogoutView.as_view(next_page="giffy_app:index"),
name="logout",
),
]

Sign up

Signing up is a bit more complicated since Django doesn’t provide views for that out of the box.

Let’s start by creating a singup.html

giffy_app/templates/giffy_app/signup.html:

{% extends "giffy_app/base.html" %}

{% block content %}
<div class="centered">
<h3>Sign up</h3>
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
</div>
{% endblock content %}

then, update styles.css

giffy_app/static/giffy_app/styles.css:

.centered {
text-align: center;
}

/* For simplicity, we will hide helptext generated by Django. */
.helptext {
display: none;
}

Next, let’s create SignUpView in giffy_app/views.py

giffy_app/views.py:

from django.contrib.auth import authenticate, login

# We will use django's default UserCreationForm
from django.contrib.auth.forms import UserCreationForm

from django.shortcuts import render, redirect
from django.views import View

from .models import Image


class IndexView(View):
def get(self, request):
images = Image.objects.order_by("uploaded_date")
return render(
request,
"giffy_app/index.html",
{
"images": images,
},
)


class SignUpView(View):
def get(self, request):
# Render giffy_app/signup.html with
# UserCreationForm upon page load
return render(
request,
"giffy_app/signup.html",
{
"form": UserCreationForm(),
},
)

def post(self, request):
# Validate form, create user, and login
# upon sign-up form submission
form = UserCreationForm(request.POST)

# If user credentials are valid
if form.is_valid():
# create a new user in the database
form.save()

# get username and password from form
username = form.data["username"]
password = form.data["password1"]

# authenticate user using credentials
# submited in the UserCreationForm
user = authenticate(
request,
username=username,
password=password,
)

if user:
# login user
login(request, user)

# redirect user to index page
return redirect("/")

# if form is not valid, re-render the template
# showing the validation error messages
return render(
request,
"giffy_app/signup.html",
{
"form": form,
},
)

Update the giffy_app/urls.py

giffy_app/urls.py:

from django.urls import path
from django.contrib.auth.views import LoginView, LogoutView
from .views import IndexView, SignUpView

app_name = "giffy_app"

urlpatterns = [
path("", IndexView.as_view(), name="index"),
path(
"login/",
LoginView.as_view(
template_name="giffy_app/login.html",
next_page="giffy_app:index",
redirect_authenticated_user=True,
),
name="login",
),
path(
"logout/",
LogoutView.as_view(next_page="giffy_app:index"),
name="logout",
),
# Added signup view here
path("signup/", SignUpView.as_view(), name="signup"),
]

Adding a header to the index page

Let’s add header to the top of our website. The header will contain links to Log in, Log out, Sign up, and Upload Image pages.

First, create header.html template.

giffy_app/templates/giffy_app/header.html:

<header>
<nav>
<a href="/">Home</a>

<a href="" class="upload-btn">Upload</a>

{% if user.is_authenticated %}
<!-- show log out link if user is already logged in -->
<a href="{% url 'giffy_app:logout' %}">Logout</a>

{% else %}

<!-- show log in and sing up links otherwise -->
<a href="{% url 'giffy_app:login' %}">Login</a>
<a href="{% url 'giffy_app:signup' %}">Signup</a>

{% endif %}
</nav>
</header>

Then, update styles.css

giffy_app/static/giffy_app/styles.css:

.centered {
text-align: center;
}

.helptext {
display: none;
}

/* styles for upload image button */
.upload-btn {
color: var(--accent) !important;
border-color: var(--accent) !important;
}

Finally, add header to the base.html

giffy_app/templates/giffy_app/base.html:

{% load static %}
<!DOCTYPE html>
<html>

<head>
<title>Giffy</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<link rel="stylesheet" type="text/css" href="{% static 'giffy_app/styles.css' %}">
</head>

<body>
<!-- Added header.html -->
{% include 'giffy_app/header.html' %}

{% block content %}
{% endblock content %}
</body>
</html>

It’s time to test the result. Execute the python manage.py runserver and open http://127.0.0.1:8000/

Looks like login, logout, and signup work as expected!

Upload image view

The last thing left to do is to create an upload image functionality. Let’s start by creating an upload.html template.

giffy_app/templates/giffy_app/upload.html:

{% extends "giffy_app/base.html" %}

{% block content %}
<div class="centered">
<h3>Upload</h3>
<!-- It's important to add enctype="multipart/form-data" -->
<!-- param, otherwise django will fail to upload the image -->
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="submit"/>
</form>
</div>
{% endblock content %}

This time around we have to create a form ourselves. First, let’s create forms.py in the giffy_app folder.

giffy_app/forms.py:

from django import forms

from .models import Image


# We use Django's built-in ModelForm class
class UploadImageForm(forms.ModelForm):
class Meta:
model = Image
# only image and title fields from the Image
# model will be available to user
fields = ["image", "title"]
# Let's play with it a little to
# get a taste of Django widgets
widgets = {
"title": forms.Textarea(
attrs={
"cols": 60,
"rows": 3,
}
),
}

Next, let’s create a UploadImageView

giffy_app/views.py:

from django.views import View
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm

# LoginRequiredMixin will check that user
# is authenticated before rendering the template.
from django.contrib.auth.mixins import LoginRequiredMixin

from datetime import datetime
from .models import Image
from .forms import UploadImageForm


class IndexView(View):
def get(self, request):
images = Image.objects.order_by("uploaded_date")
return render(
request,
"giffy_app/index.html",
{
"images": images,
},
)


class SignUpView(View):
def get(self, request):
return render(
request,
"giffy_app/signup.html",
{
"form": UserCreationForm(),
},
)

def post(self, request):
form = UserCreationForm(request.POST)

if form.is_valid():
form.save()
username = form.data["username"]
password = form.data["password1"]
user = authenticate(
request,
username=username,
password=password,
)
if user:
login(request, user)
return redirect("/")

return render(
request,
"giffy_app/signup.html",
{
"form": form,
},
)


class UploadImageView(LoginRequiredMixin, View):
# Not authenticated users will be redirected
# to /login page if they try to access this view
login_url = "/login/"

def get(self, request):
return render(
request,
"giffy_app/upload.html",
{
"form": UploadImageForm(),
},
)

def post(self, request):
user = request.user
if not user.is_authenticated:
# Double check that user is authenticated
raise Exception("User is not authenticated")

# File, uploaded by user, will be available at request.FILES
form = UploadImageForm(request.POST, request.FILES)

if form.is_valid():
# Creating a new image model instance
img = Image(
# get title from form.data
title=form.data["title"],
# get image from form.files
image=form.files["image"],
uploaded_date=datetime.now(),
uploaded_by=user,
)

# save image to database
img.save()

# redirect to index page upon successful upload
return redirect("/")

# re-render form, show validation errors
return render(
request,
"giffy_app/upload.html",
{
"form": form,
},
)

and add our new view to urls.py

giffy_app/urls.py:

from django.urls import path
from django.contrib.auth.views import LoginView, LogoutView

from .views import IndexView, SignUpView, UploadImageView

app_name = "giffy_app"

urlpatterns = [
path("", IndexView.as_view(), name="index"),
path(
"login/",
LoginView.as_view(
template_name="giffy_app/login.html",
next_page="giffy_app:index",
redirect_authenticated_user=True,
),
name="login",
),
path(
"logout/",
LogoutView.as_view(next_page="giffy_app:index"),
name="logout",
),
path("signup/", SignUpView.as_view(), name="signup"),

# Added upload view here, don't forget to
# import UploadImageView from .views
path("upload/", UploadImageView.as_view(), name="upload"),
]

Finally, update the header.

giffy_app/templates/giffy_app/header.html:

<header>
<nav>
<a href="/">Home</a>

<!-- added url 'giffy_app:upload' inside href attribute -->
<a href="{% url 'giffy_app:upload' %}" class="upload-btn">
Upload
</a>

{% if user.is_authenticated %}
<a href="{% url 'giffy_app:logout' %}">Logout</a>

{% else %}

<a href="{% url 'giffy_app:login' %}">Login</a>
<a href="{% url 'giffy_app:signup' %}">Signup</a>

{% endif %}
</nav>
</header>

Let’s test upload functionality!

Conclusion

In this brief guide, we have seen firsthand how easy and powerful the Django framework is. In just 10 minutes, we created a demo application with image uploading, authorization, authentication, and an admin interface!

In the next guide, I will show how to deploy giffy to the Google Cloud. Stay tuned!

Have any questions? — Ask in the comments!
Enjoyed the story? — Subscribe, connect on Linkedin, or follow me on Twitter!

--

--