Building a Video Sharing Website: Part 1 Setup & CRUD Operations

Video Tutorial

Code on Github

In this tutorial we are going to complete the first installment of the series Building a Video Sharing Website. Before we get started, a quick disclaimer: This is not meant to be a complete beginner series. We will go through the beginning videos relatively quickly. If you are a beginner you might still be able to follow along but it will definitely help to have a basic understanding of Django before starting. We are going to build a website similar to the service that a site like Youtube or Vimeo provide. This site will be built using Python 3 and Django. It will allow a user to upload videos to share to the site. There will also be pages available allowing the user to update the uploads as well as delete the video. We will eventually add more advanced features to the site in future installments but in this part we will just focus on set up and the basic functionalities.

In this video we are going to focus on those operations, the CRUD operations. CRUD (Create, Read, Update, Delete) will be the foundation of this project. A user should have the option to create a video upload, update it, read it (or view it), and delete it. We aren’t going to focus on user authentication yet, that will come in a future video, so we won’t worry about forcing a user to login first to upload a video. That will come later. Let’s get started with this project.

Setting Up a Django Project

Before we can do anythig we need to install Django and set up a Django project. Let’s install Django with pip:

pip install Django

Now lets create a Django project:

django-admin startproject video-sharing

This will create a folder with some files which will include a settings.py and a urls.py which we will use extensively. The settings.py will hold all of our project settings and the urls.py will hold the root url routes that we will create for our project.

Let’s create an app. An app is just a part of a Django project that will do one specific piece of the entire application. We will create a videos app to handle HTML forms for uploading, updating, and deleting videos as well as the Video model itself. We will create other apps as well to handle user authentication, user profiles, etc. Let’s get started with just the video app for now.

To do this, navigate to the directory that holds the manage.py file and run this command.

python3 manage.py createapp videos

Now that our videos app is created, we need to add it to the settings.py file so we can use it. Go ahead and open settings.py.

In this file you will see various different dictionaries, lists, and variables. We will go over them as we need to for now scroll down until you see INSTALLED_APPS, it should look something like this:

INSTALLED_APPS = [    'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.sites',
'django.contrib.staticfiles',
]

To add the videos app that we just created to it, all we need to do is add it to the list:

INSTALLED_APPS = [    'videos',    'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.sites',
'django.contrib.staticfiles',
]

Now we are ready to use our videos app. Our first goal will be to render a simple index page.

Setting Up a Basic Index Page

To render a page we need to follow a couple steps, create any models needed for the view, create a view, and create a URL pattern for the view. We aren’t going to worry about models for now. Let’s create a simple view, navigate to the videos directory and open the views.py file. Our first view will look like this:

from django.shortcuts import renderdef index(request):
return render(request, 'videos/index.html')

This is the most basic of a view that you can have. All it does is render a HTML template called index.html inside of the videos app. We haven’t created that yet so lets go ahead a do that. Create a directory in videos called templates and then within that directory create a directory called videos. This is just the default for Django templates, the templates won’t be able to be found with the default settings unless they are set up in this way. Create a file called index.html. Let’s add some placeholder HTML for now:

<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<title>Video Sharing</title>
</head>
<body>
<h1>Index</h1>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>

In this file we are doing a couple things. First, we are adding CSS and Javascript for Bootstrap 4. We will use this to quickly add CSS styles to our template. We are also adding a h1 heading that says Index so that we know what page we are on. Next let’s tell Django what URL pattern should open this template. Open up the file called urls.py in the video-sharing directory (not in the videos directory). We need to add to the list in this file:

from django.contrib import admin
from django.urls import path
from .video import views as video_views
urlpatterns = [
path('admin/', admin.site.urls),
path('', video_views.index, name='index'),
]

We first need to import the views.py file from the video directory. We then add a URL pattern below the admin (which we will talk about later). This pattern is just an empty string so it will map to the root URL which is where the index page should be. we then next put in the view we want and then give it a name to reference it by.

At this point our fist view is all done and it should render at the root. Go back to where the manage.py file is and run the following command:

python3 manage.py runserver

Go to localhost:8000 in a browser and you should see the index page we just created.

Adding the Create View

We need a way for users to upload videos to site. We are going to do this by using Django’s generic class based views. There is a CreateView that makes this process very easy. We don’t need to create a form, we just need to specify the fields in the form and then pass that form object to the template. But before we do that, we need to create the Video data structure in the models.py file. Let’s open that up and create it:

from django.db import models
from django.core.validators import FileExtensionValidator
from django.utils import timezone
class Video(models.Model):
title = models.CharField(max_length=100)
description = models.TextField()
videoFile = models.FileField(upload_to='uploads/videoFiles', validators=[FileExtensionValidator(allowed_extensions=['mp4'])])
thumbnail = models.FileField(default="uploads/thumbnails/thumbnail-default.jpg", upload_to='uploads/thumbnails', validators=[FileExtensionValidator(allowed_extensions=['png', 'jpg', 'jpeg'])])
date_posted = models.DateTimeField(default=timezone.now)

We are doing a few different things here. We are creating some basic a CharField, TextField, and FileFields to build the pieces that the user will need to upload with the files. We are using one of Django’s built in validators FileExtensionValidator to check the file extensions to make sure they only upload a video file for the videoFile variable and an image for the thumbnail variable. We are also using a DateTimeField to automatically fill in the current time when they upload the video. We are also creating a relationship between the User and the Video to store the user who is uploading the video. The User object is imported at the top because it is built into Django. Now with our first model created, we are ready to get started building the CreateView. But before we do that, we need to run some set up commands in the terminal. Naviagate to the directory holding the manage.py file and run these commands:

python3 manage.py makemigrations
python3 manage.py migrate
python3 manage.py createsuperuser

Now lets open up views.py to add our CreateView:

from django.shortcuts import render
from django.urls import reverse
from django.views.generic.edit import CreateView
from .models import Video
def index(request):
return render(request, 'videos/index.html')
class CreateVideo(CreateView):
model = Video
fields = ['title', 'description', 'thumbnail', 'videoFile']
template_name = 'videos/create_video.html'
def get_success_url(self):
return reverse('video-detail', kwargs={'pk': self.object.pk})

First, we create a class called CreateVideo. It will inherit from the CreateView class. We need to give it the model it should be creating an object for, which in this case is Video. We specify the fields that should be in the form and tell it where and what the name of the template is. We then add a method, get_success_url() which will give it a redirect url to go to once the form is successfully submitted. In this case we are going to send it to the video-detail url which we haven’t created yet. We also need to pass in the current objects primary key as an argument. This will make more sense after we add the DetailView.

Now that we have the view created we need to create a HTML template. But first we are going to make a base template so we don’t have to repeat ourselves anymore.

<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/media/css/style.css"> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<!-- Video.js -->
<link href="https://vjs.zencdn.net/7.7.6/video-js.css" rel="stylesheet" />
<!-- Sea Theme -->
<link href="https://unpkg.com/@videojs/themes@1/dist/sea/index.css" rel="stylesheet">
<!-- If you'd like to support IE8 (for Video.js versions prior to v7) -->
<script src="https://vjs.zencdn.net/ie8/1.1.2/videojs-ie8.min.js"></script>
<title>YouTube Clone</title>
</head>
<body>
{% include 'videos/navigation.html' %} {% if messages %}
<div class="container mt-3">
<div class="row justify-content">
<div class="col-md-8 col-sm-12 col-xs-12">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% block content %}
{% endblock content %}
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="https://unpkg.com/ionicons@5.0.0/dist/ionicons.js"></script>
<script src="https://vjs.zencdn.net/7.7.6/video.js"></script>
<!-- sharer.js -->
<script src="https://cdn.jsdelivr.net/npm/sharer.js@latest/sharer.min.js"></script>
</body>
</html>

Now with that created lets make our create view template. We are going to add this in the same directory that our index.html file is in and we will call it create_video.html.

{% extends 'videos/base.html' %}
{% load crispy_forms_tags %}
{% block content %}<div class="container mb-5">
<div class="row justify-content-center">
<div class="col-md-6 col-sm-12 col-xs-12">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<legend class="border-bottom mb-4">Upload Video</legend>
{{ form | crispy }}
<button class="btn btn-outline-info btn-block" type="submit">Submit</button>
</form>
</div>
</div>
</div>
{% endblock content %}

In this template all we are doing is putting in the form that the CreateView is automatically creating for us. You will also see that we put a filter on it called crispy. Crispy Forms is something we can add to help make the forms look better but we need to install it and set it up.

pip install django-crispy-forms

We also need to add a couple of things to our settings.py file:

INSTALLED_APPS = [    'videos',    'crispy_forms',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.sites',
'django.contrib.staticfiles',
]

We need to add crispy_forms to the INSTALLED_APPS, we also need to tell it to use Bootstrap 4, we will add this line to the bottom of settings.py:

#Crispy Forms Template Pack
CRISPY_TEMPLATE_PACK = 'bootstrap4'

The last part for building the create view is to add a URL route for it. Let’s create a file in videos called urls.py. Now first before we open that up we need to add it to our root URLs. Open up the urls.py file in the main app directory and add this line:

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
import videos.views as video_views
urlpatterns = [
path('admin/', admin.site.urls),
path('', video_views.index, name='index'),
path('videos/', include('videos.urls')),
]

Now all of our video URLs will start with videos/. Open up the file we just created and add this:

from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
path('create/', views.CreateVideo.as_view(), name='video-create'),
]

Now our create form will render at ‘videos/create’. For class based views we need to add .as_view() after the view name. This won’t work correctly without the DetailView so let’s add that in now.

Adding the Detail View

To add the detail view, first we need to add a view. We are going to use another class based view for that. We will add the following to videos/views.py:

from django.shortcuts import render
from django.urls import reverse
from django.views.generic.edit import CreateView
from .models import Video
def index(request):
return render(request, 'videos/index.html')
class CreateVideo(CreateView):
model = Video
fields = ['title', 'description', 'thumbnail', 'videoFile']
template_name = 'videos/create_video.html'
def get_success_url(self):
return reverse('video-detail', kwargs={'pk': self.object.pk})
class DetailVideo(DetailView):
model = Video
template_name = 'videos/detail_video.html'

The detail view is very simple, we inherit from the DetailView class and give it the model we are using and the name of the HTML template. The primary key is automatically passed into the view without us having to do anything. Next we need to make that template:

{% extends 'videos/base.html' %}{% block content %}
<div class="container">
<div class="row mt-5">
<div class="col-md-8 col-sm-12 col-xs-12">
<video-js class="vjs-theme-sea"
width="720"
height="405"
controls
data-setup='{"playbackRates": [0.5, 1, 1.5, 2], "fluid": true}'>
<source src="/media/{{ object.videoFile }}" type="video/mp4">
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
</p>
</video-js>
<h5 class="mt-4">{{ object.title }}</h5>
<div class="row">
<div class="col-md-6 text-muted">
<p>{{ object.date_posted | date:"M d, Y" }}</p>
</div>
<div class="col-md-6 text-muted text-right">
<!-- Button trigger modal -->
<ion-icon class="video-icons" name="share-social-outline" type="button" data-toggle="modal" data-target="#exampleModal">
Share
</ion-icon>

<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Share This Video!</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body text-center">
<ion-icon class="social-share-icons twitter-social-color" name="logo-twitter" data-sharer="twitter" data-title="Checkout {{ object.title }} by {{ object.uploader }}!" data-hashtags="" data-url="http://localhost:8000/videos/{{ object.pk }}">Share on Twitter</ion-icon>
<ion-icon class="social-share-icons facebook-social-color" name="logo-facebook" data-sharer="facebook" data-title="Checkout {{ object.title }} by {{ object.uploader }}!" data-hashtags="" data-url="http://localhost:8000/videos/{{ object.pk }}">Share on Facebook</ion-icon>
<ion-icon class="social-share-icons reddit-social-color" name="logo-reddit" data-sharer="reddit" data-title="Checkout {{ object.title }} by {{ object.uploader }}!" data-hashtags="" data-url="http://localhost:8000/videos/{{ object.pk }}">Share on Reddit</ion-icon>
<p class="mt-3">Link: http://localhost:8000/videos/{{ object.pk }}</p>
</div>
</div>
</div>
</div>
{% if object.uploader == user %}
<a class="video-icons icon-color" href="{% url 'video-update' object.pk %}"><ion-icon name="pencil-outline"></ion-icon></ion-icon></a>
<a class="video-icons icon-color" href="{% url 'video-delete' object.pk %}"><ion-icon name="close-outline"></ion-icon></a>
{% endif %}
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6 d-flex flex-row align-items-center">
<a href="{% url 'profile' object.uploader.pk %}"><img src="{{ object.uploader.profile.image.url }}" class="rounded-circle mr-4" height="65" width="65"></a>
<a class="video-uploader" href="{% url 'profile' object.uploader.pk %}"><h5>{{ object.uploader }}</h5></a>
</div>
</div>
<p class="ml-5 mt-3"> {{ object.description }}</p>
</div>
</div>
</div>


{% endblock content %}

There are a couple important pieces to this template. We use video.js to give us an improvement on the basic HTML5 video player. We are also using sharer.js to easily create sharable links for our videos. We put these share links inside of a modal that will pop up when the share icon is pressed. We also added some ion icons to improve the look of the links from basic text. The rest is just basic bootstrap with some Django templates thrown in. When we need to link to another page on the site we can use {% url ‘urlName’ %} to route to it based on the name given to the url pattern. We can also pass in primary keys or anything else into the url with this syntax: {% url ‘urlName’ object.pk %} It is important to note that object is the default name passed into the context so in this case. The video that is used for the detail view will automatically get passed into the template with the name of object.

Finally, let’s add the url pattern in videos/urls.py:

from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
path('create/', views.CreateVideo.as_view(), name='video-create'),
path('<int:pk>/', views.DetailVideo.as_view(), name='video-detail'),
]

Same as before, we will add .as_view() after any class based view. We use the syntax <int:pk> to add the primary key for the specific video into the url.

Now the last two views we need are an update view and a delete view.

Adding the Update View

Now let’s add the update view to the videos/views.py file:

from django.shortcuts import render
from django.urls import reverse
from django.views.generic.edit import CreateView
from .models import Video
def index(request):
return render(request, 'videos/index.html')
class CreateVideo(CreateView):
model = Video
fields = ['title', 'description', 'thumbnail', 'videoFile']
template_name = 'videos/create_video.html'
def get_success_url(self):
return reverse('video-detail', kwargs={'pk': self.object.pk})
class DetailVideo(DetailView):
model = Video
template_name = 'videos/detail_video.html'
class UpdateVideo(UpdateView):
model = Video
fields = ['title', 'description']
template_name = "videos/create_video.html"
def get_success_url(self):
return reverse('video-detail', kwargs={'pk': self.object.pk})

The update view is pretty similar to the create view. We even use the same template as before. We give it the model, the fields for the form, and the template_name and then the get_success_url gives it a redirect url after successful submission of the form. The only thing left to add for this view is the URL pattern. Let’s open videos/urls.py to add that:

from django.urls import path
from . import views
urlpatterns = [
path('create/', views.CreateVideo.as_view(), name='video-create'),
path('<int:pk>/', views.DetailVideo.as_view(), name='video-detail'),
path('<int:pk>/edit', views.UpdateVideo.as_view(), name='video-update'),
]

We are also adding in the primary key for the video using <int:pk>. That finishes our update view to edit videos that are already posted.

Adding the Delete View

The final view we are adding for the first part of this tutorial is the delete view.

from django.shortcuts import render
from django.urls import reverse
from django.views.generic.edit import CreateView
from .models import Video
def index(request):
return render(request, 'videos/index.html')
class CreateVideo(CreateView):
model = Video
fields = ['title', 'description', 'thumbnail', 'videoFile']
template_name = 'videos/create_video.html'
def get_success_url(self):
return reverse('video-detail', kwargs={'pk': self.object.pk})
class DetailVideo(DetailView):
model = Video
template_name = 'videos/detail_video.html'
class UpdateVideo(UpdateView):
model = Video
fields = ['title', 'description']
template_name = "videos/create_video.html"
def get_success_url(self):
return reverse('video-detail', kwargs={'pk': self.object.pk})
class DeleteVideo(DeleteView):
model = Video
template_name = 'videos/delete_video.html'
def get_success_url(self):
return reverse('index')

This is almost identical to the previous views with a different template. Django’s class based views makes these very easy to build. In this view we are redirecting back to the index page after deleting a video. This template will go to a confirmation page before deleting the video. Let’s build that template now. We’ll put in in the videos/templates/videos directory and call it delete_video.html to match what we put in the view.

{% extends 'videos/base.html' %}{% block content %}<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-sm-12 col-xs-12 text-center">
<form method="POST">
{% csrf_token %}
<legend class="border-bottom mb-4">Delete Video</legend>
<h5>Are You Sure?</h5>
<p>This will delete the video forever. This cannot be undone.</p>
<button class="btn btn-outline-danger btn-block" type="submit">Delete</button>
</form>
</div>
</div>
</div>
{% endblock content %}

All we have to do here is create a form that asks if they are sure that they want to delete the video. Once they submit the form, it will delete the video from the database.

Finally, we need to add the url pattern for this view.

from django.urls import path
from . import views
urlpatterns = [
path('create/', views.CreateVideo.as_view(), name='video-create'),
path('<int:pk>/', views.DetailVideo.as_view(), name='video-detail'),
path('<int:pk>/edit', views.UpdateVideo.as_view(), name='video-update'),
path('<int:pk>/delete', views.DeleteVideo.as_view(), name="video-delete"),
]

This concludes the first part of this series. In the future we will add more parts to this tutorial to the point where will have a fully functioning web application.