How to transform your Static Website into a Django Web Application - a very comprehensive guide

How to transform your Static Website into a Django Web Application - a very comprehensive guide

Ninte Dangana's photo
Ninte Dangana

Published on Nov 28, 2020

27 min read

Subscribe to my newsletter and never miss my upcoming articles

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.

 
Share this