How to transform your Static Website into a Django Web Application - a very comprehensive guide
Building a website is a thrilling experience. The ups and downs of facing technical problems, debugging and creating a versatile digital experience make the activity worthwhile.
Upon learning to code, the first few steps one takes in web development often involve HTML
, CSS
and JavaScript
. These are the key ingredients in creating a static website.
"A static website is one with content that rarely changes, presented to the web with the primary tools the internet understands for presentation."
Here is a link to a starter branch on the GitHub repository for this tutorial. The branch described holds the assets we will make use of as we incorporate our static website into a web application using the Django web framework.
First steps
To get started using Django, you will need to install certain packages. I recommend doing this within a python virtual environment.
Python can be downloaded here.
Using a modern version of Python, a virtual environment can be created with the following command - replacing <env-name>
with a suitable name, possibly that of the project.
python3 -m venv ~/Tools/virtualz/<env-name>
It is generally advised to use this toolset when working on python projects. Virtual environments help to separate dependencies between projects and on the other hand the underlying computer system.
pip install Django django-environ
When starting a new Django project, it helps to first create a directory (folder) where you will work out of first. After doing this, you will have to use the following command:
Django-admin startproject <project_name> .
The command above does not require <
or >
when specifying the project name. If your project name requires spacing of any sort, utilize the underscore symbol (_
), as this will give the intended effect without producing errors. The dot at the end is quite important as well, as this lets django-admin
know that you intend to initialize project at your current location on the computer system.
Still, in the same location on your system, copy all of your static files - HTML
, CSS
, JS
(and images, if present) - into a directory called frontend
. Next, create another directory called templates
and move all your HTML files here.
To make the most use of Django as a full-stack web framework, one has to understand how to implement the management of data and resources in its unique patterns.
Working with Django apps
Now would be a good time to create a Django app for our project. This will help us with setting up independent URLs and views, as well as static files - scoped to a certain development concern. The command to create a Django app is as follows:
django-admin startapp <app_name> .
The naming convention is similar to that of creating a project name. For the purpose of the tutorial, I called the app fun_fact
, as we will be building a simple system to collect information from users on this topic.
In the newly created app, create a directory named static
. Inside this directory, created another folder named exactly the same as the app. Upon doing so, copy all static assets (CSS
, JS
, Image files) into a directory within the last created folder - the one within static, with the app's name.
At this point you can delete the now empty frontend
directory.
Also important to note, is the fact that the newly created app needs to be registered in the INSTALLED_APPS
variable - holding a list. The way to do this is shown below:
INSTALLED_APPS = [
# django-native apps
'<app-name>.apps.<AppName>Config', # insert upon new app creation
]
Setting up static file management
The WhiteNoise
package is very helpful when working with static files. If your web application will use images, I also suggest installing the Pillow
package.
This can be done with the line below.
pip install whitenoise Pillow
Configuring WhiteNoise can be done as follows:
# Update the middleware list as follows, placing WhiteNoise directly after security.
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
...
]
# Compressed, but not cached.
# This is preferred for ease of usage in simple deployments.
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
The first block shows where to place the relevant middleware, and the second highlights how to enable it to work with static files.
To let Django know where to store static files, we need to register a location in our settings.py
file for this. The variable will have to be called STATIC_ROOT
and its value is essentially a one-liner.
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # static files directory
We've come a long way. Now we are ready to use an essential Django command:
python manage.py collectstatic
The line above instructs Django to collect and compress all the files stored within the value of the STATIC_URL
- in our case, the static directory. These files are then copied to the STATIC_ROOT
; precisely, to the location that we specified in its value.
With these changes, we will later see that it is possible to dynamically call such files from within templates.
Working with Django templating
Remember the templates
directory where we copied all our HTML files to earlier. Let work on setting it up for use now.
Firstly, we need to register the directory for use in our settings.py
file.
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
...
},
]
At the end of changing the value of the 'DIRS'
key, our TEMPLATES variable should look similar to the block above. In this case, and all others, the ...
refers to sections of code that will remain unchanged.
In the directory holding the project name (including the settings file), we will find and slightly modify the urls.py
file.
Originally it should look like this:
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
We will include an import from django.urls
for the include
function. Also, we will import the app we previously created (in our case fun_fact
). Our final action will involve working with these two imports. The aim of this to specify a base URL
, one that will allow us to create even further of the same, in the app imported. This is relayed below in code.
from django.contrib import admin
from django.urls import path, include
import fun_fact
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('fun_fact.urls')),
]
In our fun_fact
app, we will now create a urls.py
file. This will help us produce more URLs as we proceed.
Now we need to create the Views that will feed our Templates, and produce output for our URLs.
At the start, our newly created file will look as follows:
from django.urls import path
from . import views
app_name = 'fun'
urlpatterns = [
path('', views.Submission.as_view(), name='submission'),
]
The most basic view that can be created in Django, will typically take the form of the code block below.
from django.shortcuts import render
def homepage(request):
return render(request, 'index.html')
Note that the file index.html
is not completely necessary, and can be replaced with any other valid HTML file in the templates
directory, named in whatsoever way. Also too, it is important to observe that the file location within the single - or double - quotes, must be relative to its placement within the registered templates directory.
The key elements general to all templates are as follows:
- The
base
file, which other relevant HTML files will follow - Using the
extend
keyword which permits the following highlighted above - Using the
block
keyword to scope unique content, from parent pages moving inwards - Loading
static
assets and implementing their use within templates - Implementing Django's unique form of link declaration with the
URL
keyword
With the implementation of static and URL for the reasons highlighted above, it is important to observe how this is done. The current syntax takes the form shown in the code block below.
# for static files
{% static '<app-name>/<folder-name>/<file-name>.<extension>' %}
# for links
{% url '<app-name>:<path-name>' %}
With static files, it is important to be sure of how exactly each file is located within the directory tree. When in doubt, look within the staticfiles
directory, after running python manage.py collectstatic
.
External links do not need to follow the convention that Django imposes with those of an internal nature. The values of whatever web resources located outside the web application can typically be called in the usual HTML format.
Below is sample code located in the repository shown depicting a typical templates
directory setup. Moving through the GitHub resource will show in more depth how dynamic templating can be implemented in Django. As we progress through this tutorial, many of these same concepts will be equally addressed.
Models and administration
To hold - or more directly save - information submitted to our new Django web application, we will need to make use of models. Thankfully, this is a concept native to the web framework.
To create a model requires naming and proper definitions of the various fields within the same. As a developer, you also have the choice of how your models will be represented in the admin dashboard native to Django. More precisely, one can even define how model instances will be displayed through the setting of flexible parameters.
Our simple web application will have the following model definition:
from django.db import models
class FunFactSubmission(models.Model):
username = models.CharField(max_length=50, blank=False)
fun_fact = models.TextField(blank=False)
def __str__(self):
return "{} - {}".format(self.id, self.username)
Above, we stipulate just two fields. The first is the username
, which helps us to know who submitted what fun_fact
, which in itself is the second field.
After creating a model. It helps to register it in the admin.py
file present in each initialized Django app. This fields affects how our model data is presented in the administrative dashboard present within every Django project.
Our admin file will take the form of the code block below.
from django.contrib import admin
from .models import FunFactSubmission
class FunFactSubmissionAdmin(admin.ModelAdmin):
fields = ['username', 'fun_fact']
admin.site.register(FunFactSubmission, FunFactSubmissionAdmin)
Note how on the third line, we import the model we previously created. Our ModelAdmin
involves a declaration of what fields we would like to display in the administrative dashboard. The final line of the file is a straightforward registration of both our created model and ModelAdmin.
We have come to a significant junction in the creation of our Django project. Now we can create our superuser - an action of grave necessity as it is absolutely required for administration.
The following line shows how to do this.
python manage.py createsuperuser
You will be prompted to offer values for the username
, email
and password
fields respectively; in that order. While offering a value for email is largely your choice as a developer, the other two fields require proper thought and fulfilment.
Seeing as we are yet to migrate
our web application - migration refers to an action that stipulates changes in the database that works with our web application. Django comes with a simple db.sqlite3
file that serves for development purposes but is not appropriately suitable for production environments.
The commands below will fulfil our migration obligations.
python manage.py makemigrations
python manage.py migrate
Ordinarily only the second line would be necessary. The first is required because of our newly created model
. The order in which these commands are run is relevant as well - for this reason.
With our models migrated and the superuser
created, we can now finally log into our administrative dashboard. By default, the url for this is the main resource location (eg. http://127.0.0.1:8000/
), followed by admin/
. Do note however that this is not ideal in a production system. I would recommend saving the value of this URL in an environment variable when deploying for such a purpose.
Using forms for efficient data collection
Now create a forms.py
file in your Django app. This particular file does not come upon initialization. The particular type of form we will be creating is a ModelForm
. It will be directly connected to the model we previously created.
Here is a layout of the file currently being discussed.
from django import forms
from .models import FunFactSubmission
class SubmissionForm(forms.ModelForm):
class Meta:
model = FunFactSubmission
fields = ['username', 'fun_fact']
widgets = {
'username': forms.TextInput(
attrs={
'class': 'form-control'
}
),
'fun_fact': forms.Textarea(
attrs={
'class': 'form-control',
'rows': '5'
}
)
}
The first two lines of code involve the importing of the forms
attribute native to Django, followed by our custom model. As mentioned, this is a ModelForm. This requires stipulation in the form definition and follows with syntax typically unnecessary when creating a pure form.
The Meta
class hold information which generally describes how our form is expected to behave. The model as discussed is that of our creation. The fields
variable holds the parts of the model we intend to work with - particularly useful when creating forms from varying parts of a singular model.
Widgets let Django know how to render our form in HTML. What this value holds is a dictionary - with keys connected to fields earlier declared. Each key in the widgets
dictionary holds a value that explicitly instructs Django how to render previously highlighted fields.
Notice that each widget has an attrs
parameter. While not ordinarily required, I have found that this permits one to more precisely determine how the form fields are rendered by Django. Think of attrs
as the elements in an HTML tag that one would typically like to include.
Those familiar with Bootstrap forms will recognize 'class': 'form-control'
to some extent. This is the Django way of declaring class="form-control"
as one would usually do in an HTML input tag, following the library's pattern.
How to build Class-Based Views and create Dynamic Templates
For speedy development, I have found that CBVs
or Class-Based Views work really well when working with Django. Another benefit they provide is clean and easy to read code, both for one's self as a developer as well as for others who will go through your creation in the future.
This tutorial made use of a generic views which are built in to Django - namely, CreateView
, ListView
and DetailView
. The import code for these is as follows:
from django.views.generic import CreateView, ListView, DetailView
Moving both sequentially, and in the order that each is implemented in the project, we will take a look at their use in views.py
and other inter-related files.
With CreateView we are essential rendering a form based off its value in a relevant HTML file. In the views.py
file, it would look like the code block below.
class Submission(CreateView):
template_name = 'pages/submit.html'
model = FunFactSubmission
form_class = SubmissionForm
def get_success_url(self):
return reverse_lazy('fun:results')
Note that we have to stipulate several values in the class definition. First, we highlight the template we would like to use. Going through the GitHub repository mentioned above, this can be found most quickly.
The model also needs to be referred to. In connection to this, the form_class
variable holds the value of the form this view needs to operate. The imports for these two elements is shown below.
from .models import FunFactSubmission
from .forms import SubmissionForm
With regard to get_success_url
, this essentially lets the view know what page to redirect to on the valid submission of the form. With reverse_lazy
, Django knows to make the direct only after properly working upon the values submitted in the present page. Below, the line to import reverse_lazy is depicted. As with when working on URLs, the link used follows Django's unique declaration format (<app-name>:<path-name>
).
from django.urls import reverse_lazy
On the template side of things, we will need to dynamically offer Django the varying elements of the form specified. An apt representation of looping over a form in a template is shown below.
<form action="" method="post">
{% csrf_token %}
{% for field in form %}
<div>
<label>{{ field.label_tag }}</label>
<span style="color:red">
{{ field.errors }}
</span>
{{ field }}
</div>
{% endfor %}
<input type="submit" value="Submit" />
</form>
While the version of this in our submit template varies slightly due to bootstrap customization parameters, the fundamental principle stays the same. Here we loop over the form
value offered to the template context, extracting each element through the keyword field
.
Furthermore, via field we get access to other attributes yet still present via the loop. We can stipulate an extraction of the field label tag, errors and the field itself, all within the for loop block.
For ListView
, we take a slightly different approach. Kindly note that this view holds the redirect location present within CreateView
. This dance across views will be essential to keep in mind.
class Results(ListView):
template_name = 'pages/results.html'
model = FunFactSubmission
ordering = ['-id']
context_object_name = 'submissions'
I'll skip past the template_name
and model
variables as their purpose has already been explained.
With regard to ordering
, this affects how the model's values will be rendered in the template. We can see from the value of the ordering
variable that the field we intend to make use of is the id
(also known as the primary key). The use of the -
before the field name, means that we intend to utilize the same in descending order.
We use context_object_name
to provide us with a value to help with this rendering.
To help with context (pun intended), here is the HTML for this particular view.
{% extends 'base.html' %}
{% load static %}
{% block content %}
<h2 class="underline text-info">Results</h2>
<br />
<table class="table table-striped table-dark">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col">Fun Fact</th>
</tr>
</thead>
<tbody>
{% for value in submissions %}
<tr>
<th scope="row">{{ value.id }}</th>
<td>{{ value.username }}</td>
<td class="text-info">
<a class="btn btn-sm btn-light" href="{% url 'fun:result-focus' value.pk %}">
<i class="text-info lni lni-angle-double-right"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<br /><br /><br />
{% endblock %}
As in the case of the form earlier, we loop over values provided to the template. This time, with the help of the context_object_name
parameter. We extract value
from this - which could have been named in any other way - using the elements it holds to help with our template rendering.
Of key importance in this file, is the use of an internal URL. Apart from stipulating the usual '<app-name>:<path-name>'
syntax, something extra can be observed here. This is the use of a keyword argument or kwargs as it is known in Django.
Here is the URL, once more:
{% url 'fun:result-focus' value.pk %}
To better explain this, let us take a look at the URL path for the view this leads to.
path('<int:pk>/result/', views.ResultFocus.as_view(), name='result-focus'),
The first parameter that this path holds, holds somethings other than pure text. The <int:pk>
placeholder lets Django know that this part of the URL can be held by any integer that is a valid primary key in the registered model for this view.
Looking at the view, we see a healthy block of code largely foreign to all that has been discussed so far.
class ResultFocus(DetailView):
template_name = 'pages/result-focus.html'
model = FunFactSubmission
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['result'] = FunFactSubmission.objects.get(pk=self.kwargs['pk'])
return context
The function get_context_data
lets Django know what specific result in the FunFactSubmission
we are looking for. The context
variable first holds as a value a deep binding of both the higher-level class and whatever kwargs (keyword arguments) provided to the view. Next, a key is created in this context object and set to the value of result
, as seen above.
What results
holds is the outcome of a search within the FunFactSubmission
model of the singular value in the database, where the pk
(primary key) is exactly the same as that in the URL kwargs
. Remember the dance across views mentioned earlier ...
This value - now explained - is singular because each row in the database will possess a unique primary key. As such, this is an effective strategy to use when attempting to hold down values via context
to a potential template.
This brings us to the end of our elaborate transformation. Through the course of this tutorial, we have successfully modified a static website into a fully functional Django web application. Thanks for making it this far.
Similar blog posts
If you'd like to know how to configure file storage in a Django web application, kindly give this article a read.
If you'd like to know how to deploy a Django web application to Heroku, kindly give this article a read.
No Comments Yet