Hi, I'm Harlin and welcome to my blog. I write about Python, Alfresco and other cheesy comestibles.

Python - Simplest Django Tutorial Ever

I wanted to write up a very simple Django tutorial that will show you the workflow behind creating a very simple CRUD app. The workflow is repeatable of course. You can use it to build more functional apps.

The steps below will show you how to build a simple ToDo app. We'll also use Bootstrap to make the html presentation look pretty decent.

The idea here is for you to learn Django at its simplest so that you can take what you learn and then build on top of it. For example, I show how to get data from the database using Django's built-in ORM. I show how to get a list of objects and then how to also just get one. This should peak your interest enough to where you can go searching for a way to sort the results or perhaps to filter them.

So, let's get started.

Here is the simplest Django tutorial you can ever hope for. The idea is to show you step by step how you can create a very simple ToDo list app.

Use Linux or Mac (You can use Windows but ...)

If you don't have a Mac, a Linux (Ubuntu, Fedora or CentOS) box, workstation or laptop, then you can consider installing VirtualBox. The best workstation Linux to use in my mind is using Ubuntu on a VirtualBox guest. You can use Windows if you must, but I don't include any instructions on how to get that going.

If you must use Windows, pay attention to !Windows Users! sections in this post.

If you're on Windows and want to go the VirtualBox route, you can download VirtualBox and install it.

Install Ubuntu 16.04 on VirtualBox. You can download Ubuntu 16.04 here.

Instructions for install Ubuntu 16.04 on Windows VirtualBox.

Are you able to login to your Ubuntu 16.04 guest on VirtualBox? or Mac OS X? or your own Ubuntu 16.04 or CentOS 7 machine? Yes? Then, move on to Step 1.

Install necessary packages for Ubuntu:

Let's assume you are able to get Ubuntu installed or you have a RedHat variant ready to go:

$ sudo apt update
$ sudo apt install build-essential
$ sudo apt install libbz2-dev libssl-dev libreadline-dev libsqlite3-dev zlib1g-dev

or CentOS 7 / RHEL / Fedora:

$ sudo yum groupinstall 'Development Tools'
$ sudo yum install bzip2-libs bzip2-devel openssl-libs openssl-devel readline readline-devel sqlite-devel zlib zlib-devel

!Windows Users! You can skip this step.
!Mac Users! You can skip this step as well.

Install pyenv

!Windows Users! Skip this step as pyenv is not supported for Windows. However, you may want to have a look at venv to use as a virtual environment.

Everyone else ...

Open a terminal and run:

$ curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash

If on Ubuntu, add the following to your ~/.bashrc file and if on Redhat or CentOS, add to your ~/.bash_profile:

export PATH="/home/[your username]/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

You will need to source either .bashr or .bash_profile:

$ . ~/.bashrc

or

$ . ~/.bash_profile

You can also log out and log back in.

Test the pyenv install by running:

$ pyenv --version
pyenv 1.1.5

Install latest Python version

$ pyenv install 3.6.3 
Downloading Python-3.6.3.tar.xz...
-> https://www.python.org/ftp/python/3.6.3/Python-3.6.3.tar.xz
Installing Python-3.6.3...
Installed Python-3.6.3 to /root/.pyenv/versions/3.6.3

!Windows User! You will have to download Python and install it if you haven't already. You can get the version we're using here.

Set up the project folder and environment

$ cd ~
$ mkdir projects
$ cd projects
$ mkdir django-todo
$ cd django-todo
$ pyenv global 3.6.3

!Windows Users! Just use the GUI to create your folder structure. Skip the pyenv parts.

To make sure we have the right Python interpreter, issue "python" and make sure you see something like the output below:

$ python
Python 3.6.3 (default, Oct 17 2017, 23:42:57) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Setting up the rest of the project:

$ pyenv virtualenv django-todo
$ pyenv local django-todo
(django-todo)$ pip install django

!WindowsUsers! You will need to use venv to get your virtual environments working but you can go through this exercise without it.

Django should now be installed. Don't worry about setting up a database server because for now we will use sqlite3 as our database. By default, your Django project will create a db.sqlite file and write entries to it. This is not for production use however but our goal with this tutorial is to do a very simple set up so that you can start using Django very quickly.

Issue pip freeze and we'll see the packages and versions now installed:

$ pip freeze
Django==1.11.6
pytz==2017.2

Create our django todo project

$ django-admin startproject todo

Our directory structure (from the projects directory) should now look like:

.
└── django-todo
    └── todo
        ├── manage.py
        └── todo
            ├── __init__.py
            ├── settings.py
            ├── urls.py
            └── wsgi.py

Run migrations and create admin user

$ cd todo
$ ./manage.py makemigrations
No changes detected
$ ./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 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 sessions.0001_initial... OK
$ ./manage.py createsuperuser
Username (leave blank to use 'root'): admin
Email address: admin@localhost
Password: 
Password (again): 
Superuser created successfully.

Start Django and Access the Admin Console

If you're doing this from your own laptop or workstation:

$ ./manage.py runserver

Performing system checks...

System check identified no issues (0 silenced).
October 18, 2017 - 04:01:45
Django version 1.11.6, using settings 'todo.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

You should now be able to access your Django site by pointing your browser to http://localhost:8000.

If you're running from a virtual box, run this instead:

$ ./manage.py runserver 0.0.0.0:8000

Performing system checks...

System check identified no issues (0 silenced).
October 18, 2017 - 04:02:19
Django version 1.11.6, using settings 'todo.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

Make a note of your virtual box ip address and point your browser instead to http://:8000

You should then see a page that delivers the following message:

It Worked!

If you see this error message (it means you are likely using a virtualbox guest):

DisallowedHost at /
Invalid HTTP_HOST header: '192.168.15.17:8000'. You may need to add '192.168.15.17' to ALLOWED_HOSTS.

You will need to open todo/settings.py and change:

ALLOWED_HOSTS = []

to

ALLOWED_HOSTS = ['192.168.15.17',] # or to your virtualbox ip address 

To access the admin page of your Django app, point your browser to http://localhost:8000/admin.

Django Admin

You'll login with the admin user you created from ./manage.py createsuperuser. Have a look around. For now, there's not much to see, though you should be able to find Users and Groups under Authentication and Authorization.

Ok. Come on out and back to the terminal. Go ahead and enter control-C to end the development server.

Create the Tasks App

Inside the top todo directory, let's go ahead and create the tasks app. We use manage.py to do this:

$ ./manage.py startapp tasks

This will create a folder called tasks with the following content:

tasks
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

Now, let's open todo/settings.py and register our new app with our project:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tasks', # <- add tasks, the name of our app, here
]

Create the Task Model

Let's go ahead and create our Task model. Open tasks/models.py and add the following code:

"""Models module for the tasks app."""
from django.db import models


class Task(models.Model):

    title = models.CharField('Title', max_length=100)
    description = models.TextField('Description')
    created = models.DateTimeField(auto_now_add=True)
    due = models.DateTimeField()
    end = models.DateTimeField()
    importance = models.CharField(
        'Importance', max_length=50, choices=(
            ('Important/Urgent', 'Important/Urgent'),
            ('Important/Not urgent', 'Important/Not urgent'),
            ('Not important/Urgent', 'Not important/Urgent'),
            ('Not important/Not urgent', 'Not important/Not urgent'),
        )
    )

    def __str__(self):
        return self.title

Our tasks app's most important object is going to be a Task model. Here we've defined the following fields for our task:

  • Title (the title of our task)
  • Description (a description of the task)
  • Created (when the task was entered -- note that this will not be editable by users)
  • Due (when the task is due)
  • End (when the task is expected to be finished)
  • Importance (level of importance of the task -- Stretch goal: ever hear of Stephen Covey? Look up his theories on importance/urgency for tasks)
  • We've also added a helper function for str. This will give us a string representation of our model when it's listed in the admin console.

Next, let's add this model in our tasks/admin.py file:

from django.contrib import admin
from tasks.models import Task


admin.site.register(Task)

We have to add our Task model and "register" it so that it shows in our admin console. But first, we need to register our model with the database -- that is, create a table with columns that match each of the fields above and their datatypes.

$ ./manage.py makemigrations
Migrations for 'tasks':
  tasks/migrations/0001_initial.py
    - Create model Task

$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, tasks
Running migrations:
  Applying tasks.0001_initial... OK

Now, let's start up the Django dev server with:

$ ./manage.py runserver

Point your browser to http://localhost:8000/admin

Django Admin Tasks Model

We should now see a new app called Tasks with Tasks as our model. Click on Add for tasks and let's create a couple of tasks. Create these two:

Title: Go to the store
Description: Buy some milk and bread
Due: Set tomorrow's date and time to 12:00pm
End: Set tomorrow's date and time to 12:30pm
Importance: Set to Important/Urgent. After all, a big snowstorm is coming. We don't want to be without milk and bread :-)

Title: Go see the chiropractor
Description: Need a back and neck alignment
Due: Set tomorrow's date and time to 4:00pm
End: Set tomorrow's date and time to 5:00pm
Importance: Set to Important/Not urgent. We could skip our appointment but we really need to get the back aligned :-)

Now that we've created and saved those two tasks, when we go to the Tasks page in admin, we should see two tasks with the titles showing in a list.

We could just use Django's admin console but it's very limited in what we can do with it. So, we're going to make our own page.

Go ahead and stop the development server by issuing control-c at the command line.

We'll need a directory to store our html templates so that a page for our tasks can be rendered. Create a subdirectory called 'templates' under tasks directory.

Build Template

$ mkdir -p tasks/templates

And now our tasks app structure should look like:

tasks
├── __init__.py
├── __pycache__
├── admin.py
├── apps.py
├── migrations
├── models.py
├── templates
├── tests.py
└── views.py

Now, we're going to create our first view. Open tasks/views.py and add the following:

"""Views module for the tasks app."""
from django.shortcuts import render


def index(request):
    return render(
        request,
        'tasks_index.html',
        {}
    )

Next, create an html file called tasks_index.html inside tasks/templates using the following code:

<!DOCTYPE html>
<html>
<head>

  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

  <title>ToDo App</title>
</head>
<body>
  <div class="jumbotron text-center">
    <h1>ToDo's</h1>
  </div>

  <div class="container">
    <div class="row">
      <div class="col-sm-4">
      </div>
      <div class="col-sm-4">
        <h3>Tasks List</h3>
      </div>
      <div class="col-sm-4">
      </div>
    </div>
  </div>

</body>
</html>

Add URL Route

Now, we need to add a route to our tasks app index page. Open todo/urls.py and make sure you have the following:

from django.conf.urls import url
from django.contrib import admin
from tasks.views import index


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(
      regex=r'^$',
      view=index,
      name='tasks_index',
    ),
]

Start the development server:

$ ./manage.py runserver

and go to http://localhost:8000.

Task List Empty

Not completely inspiring is it? No worries, we're going to add the tasks that we created in the admin console earlier.

Open tasks/views.py and this time we'll add the lists of tasks we've created and then have them rendered on the template page:

"""Views module for the tasks app."""
from django.shortcuts import render
from tasks.models import Task


def index(request):

    task_list = Task.objects.all()

    return render(
        request,
        'tasks_index.html',
        {
            'task_list' : task_list,
        }
    )

Save views.py and open tasks_index.html and make sure you have this:

<!DOCTYPE html>
<html>
<head>

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <title>ToDo App</title>
</head>
<body>
    <div class="jumbotron text-center">
      <h1>ToDo's</h1>
    </div>

    <div class="container">
        <div class="row">
            <div class="col-sm-4">
            </div>

            <div class="col-sm-4">
                <h3>Tasks List</h3>

                <table>
                    <tr>
                        <th>Task</th>
                        <th>Due?</th>
                    </tr>
                    {% for task in task_list %}

                        <tr>
                            <td>
                                {{ task.title }}
                            </td>
                            <td>
                                {{ task.due }}
                            </td>
                        </tr>

                    {% endfor %}
                </table>
            </div>
            <div class="col-sm-4">
            </div>
        </div>
    </div>

</body>
</html>

Now, have a look at it at http://localhost:8000. Looks a little better now, right?

Task List

Add Some CSS (but not much)

Well, we probably could use some CSS to spread the table out a bit. Stop the development server. In the main todo project directory, add a static/css directory:

$ cd todo
$ mkdir -p static/css

Our directory structure should now look like:

├── db.sqlite3
├── manage.py
├── static
│   └── css
├── tasks
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── templates
│   │   └── tasks_index.html
│   ├── tests.py
│   └── views.py
└── todo
    ├── __init__.py
    ├── __pycache__
    ├── settings.py
    ├── urls.py
    └── wsgi.py

We need to let Django know that our static directory will serve only static files. Add the following to the end of todo/settings.py:


STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

Next, let's create a css file at static/csss/base.css and add the following:


td {
  padding: 10px 5px;
}

and add a link tag to reference the css file in tasks_index.html:

...
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/base.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
...

Now, start up the development server and go to http://localhost:8000. Looks a little better now. The table is a bit more spread out.

So now, our page will display all of our tasks but we need a form so that we can create them as needed. In most other web frameworks, you would have to create an html form from scratch but with Django, there's no need for that. We can have one created for us based on the Task model itself.

Create a Form for Task Model

Create a file called tasks/forms.py and add the following to it:

"""Forms module for tasks app."""
from django.forms import ModelForm
from .models import Task


class TaskForm(ModelForm):
    class Meta:
        model = Task
        fields = [
            'title', 'description', 'due', 'end', 'importance',
        ]

Now, we need to go back to tasks/views.py and create a form there to put in our template. Our index view should now look like:

"""Views module for the tasks app."""
from django.shortcuts import render
from tasks.models import Task
from tasks.forms import TaskForm


def index(request):

    task_list = Task.objects.all()
    task_form = TaskForm()

    return render(
        request,
        'tasks_index.html',
        {
            'task_form': task_form,
            'task_list' : task_list,
        }
    )

and in our tasks_index.html, we'll add this:

...
</table>

  <h3>Add New Task</h3>
  <table>
      {{ task_form.as_table }}
      <tr>
          <td></td>
          <td>
              <input type="submit" value="Save" />
          </td>
      </tr>
  </table>

</div>
...

You should now see the form at http://localhost:8000 but you'll notice that clicking on the Save button doesn't do anything.

Task List - Form

So, we'll need to add some form processing functionality. First let's add a form tag in our tasks_index.html page:

...
<h3>Add New Task</h3>
<form action="" method="POST">
    <table>
        {{ task_form.as_table }}
        <tr>
            <td></td>
            <td>
                <input type="submit" value="Save" />
            </td>
        </tr>
    </table>
</form>
...

Open the tasks/views.py and make sure you have the following code for the index function:

"""Views module for the tasks app."""
from django.http import HttpResponseRedirect
from django.shortcuts import render
from tasks.models import Task
from tasks.forms import TaskForm


def index(request):

    task_list = Task.objects.all()

    if request.method == 'POST':
      task_form = TaskForm(request.POST)
      if task_form.is_valid():
        task_form.save()
        task_form = TaskForm()

    else:
      task_form = TaskForm()

    return render(
        request,
        'tasks_index.html',
        {
            'task_form': task_form,
            'task_list' : task_list,
        }
    )

Add Some Security

Now, if you try save a new Task, you should see this error message in your browser:

Forbidden (403)
CSRF verification failed. Request aborted.
Help
Reason given for failure:
    CSRF token missing or incorrect.

You will need a csrf_token tag right after your form tag in the html. I left this part out on purpose so you can see how Django handles the Cross Site Request Forgery vulnerability. In case you're not aware of what that is, a CSRF is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated. CSRF attacks specifically target state-changing requests, not theft of data, since the attacker has no way to see the response to the forged request. If you're curious to know more about it, I would recommend having a read here further about it.

So the area near our form tag should look something like this to handle it:

...
<h3>Add New Task</h3>
  <form action="" method="POST">
      {% csrf_token %}
      <table>
...                    

Now, try to save your new task to our ToDo app. When filling out the Due and End field, your format should like this:

YEAR-MO-DA HR:MN:SE

or

2017-10-20 13:00:00

When you save your new task it should show up in your list.

There's only one thing left to add that will make our CRUD app complete. We need a quick way to delete our tasks. Open the tasks_index.html file and make sure your task_list loop looks like this:

{% for task in task_list %}

    <tr>
        <td>
            {{ task.title }}
        </td>
        <td>
            {{ task.due }}
        </td>
        <td>
            <a href="/task/{{ task.id }}/">
                Delete
            </a>
        </td>
    </tr>

{% endfor %}

Open the tasks/views.py and let's add a new delete function:

...
from django.http import HttpResponseRedirect
...

def delete(request, task_id):

    task = Task.objects.get(pk=task_id)
    task.delete()

    return HttpResponseRedirect('/')

Add URL Routing

And open todo/urls.py and add this route:

from django.conf.urls import url
from django.contrib import admin
from tasks.views import index, delete


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(
      regex=r'^$',
      view=index,
      name='tasks_index',
    ),
    url(
      regex=r'^task/(?P<task_id>\d+)/$',
      view=delete,
      name='tasks_delete',
    )
]

Now, go to http://localhost:8000/ and you should see a "Delete" action link. If you click on Delete for any of the tasks, you should see them disappear.

Task List - Form

Ok, we've finished and you have a working single page app. Well, sort of. There are a number of things we would probably want to refactor or handle more in a way that reflects best practice but the goal of this post was just to show you how easy it is to use Django to build a quick app. Using what you've learned here, you can extend some of the functionality to include other apps or other things to enhance this project. It should also give you a better frame of reference when you look for help in places like StackOverflow or IRC or wherever you like to get help.

Any Comments, Always Welcome!