A Basic Django 2.0 Tutorial (Part 1)

Introduction

We're going to create a simple, locally-deployed Django 2.0 web site here. Django is a backend web application framework for Python and is deployed using WSGI - the Python Web Services Gateway Interface. The Django code is essentially the controller in an MVC-style web application in which the models and views are supplied by the developer.

The setup can seem a little complicated at first, so we'll focus on the basic scaffolding here

Links:
http://www.djangoproject.com

We'll be using Python 3 on Ubuntu.

Installation

Django is available on pypi as the django package and can be installed using pip. Let's create a virtual environment and install the package there.

$ python3 -m venv django

$ source django/bin/activate

(django) $ pip list
DEPRECATION: The default format will switch to columns in the future. You can use --format=(legacy|columns) (or define a format=(legacy|columns) in your pip.conf under the [list] section) to disable this warning.
pip (9.0.1)
setuptools (38.5.1)

(django) $ pip install django
Collecting Django
  Downloading Django-2.0.2-py3-none-any.whl (7.1MB)
  ...
Collecting pytz (from Django)
  Downloading pytz-2018.3-py2.py3-none-any.whl (509kB)
  ...
Installing collected packages: pytz, Django
Successfully installed Django-2.0.2 pytz-2018.3

(django) $ pip list
DEPRECATION: The default format will switch to columns in the future. You can use --format=(legacy|columns) (or define a format=(legacy|columns) in your pip.conf under the [list] section) to disable this warning.
Django (2.0.2)
pip (9.0.1)
pytz (2018.3)
setuptools (38.5.1)

The directory structure of our virtual environment (including a few important directories and files that will be mentioned later) looks like this:

django/
    bin/
        django-admin
        ...
    lib/
        python3.6/
            site-packages/
                django/
                    conf/
                        global_settings.py
                    contrib/
                        admin/
                            ...
                    views/
                        debug.py
                        templates/
                            default_urlconf.html
                    ...
    ...

Getting Started - Creating a Project

A Django project is a collection of directories and files that serves as a container for one or more Django applications. A project is typically created using the django-admin program and its startproject subcommand. This program is located in the bin directory of the virtual environment and it provides a -h|--help option.

(django) $ django-admin -h

Type 'django-admin help <subcommand>' for help on a specific subcommand.

Available subcommands:

[django]
    check
    compilemessages
...

The functionality of django-admin can also be accessed by using python3 -m django.

Creating a project creates a base directory, and a project directory of the same name within the base directory. The name of the base directory isn't used by Django but the the name of the project directory is (historical note: prior to 1.6 the directory structure was flat and there was only a project directory).

Let's create a project myprj (we haven't changed our working directory - it remains the directory above our virtual environment directory django - but we could do this anywhere).

(django) $ django-admin startproject myprj

That's it. The directory structure looks like this:

myprj/
    manage.py
    myprj/
        __init__.py
        settings.py
        urls.py
        wsgi.py

settings.py is the project settings module. Settings in this module override those in the global settings module site-packages/django/conf/global_settings.py. There are a couple of settings in settings.py that should at a minimum be changed after project creation: the TIME_ZONE = 'UTC' setting should be modified to specify your timezone (e.g. 'Europe/Amsterdam'), and the ALLOWED_HOSTS = [] setting should be modified to contain localhost (e.g. ALLOWED_HOSTS = ['localhost']).

In fact, the default DEBUG = True setting, in addition to causing exception tracebacks to be displayed in the browser, disables enforcement of the ALLOWED_HOSTS setting. Modifying ALLOWED_HOSTS here is thus preventative - if DEBUG is set to False later and ALLOWED_HOSTS is empty then any browser access to a Django application gives HTTP 400 Bad Request errors.

Starting the HTTP Development Server

With only a project created, you can test a Django installation by starting the Django HTTP development server. This HTTP server is intended for testing during development and binds to 127.0.0.1:8000 by default. It watches for relevant file changes and restarts automatically. The server is started using the manage.py program and its runserver subcommand. This program is located in the base directory and it also provides a -h|--help option.

(django) $ python3 manage.py -h

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser
...

Let's start the server (our working directory is the base directory). You can disregard the message concerning "unapplied migrations" - it has to do with database configuration and we'll cover that next.

(django) $ python3 manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).

You have 14 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

February 23, 2018 - 06:53:12
Django version 2.0.2, using settings 'myprj.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

If you now browse to <http://localhost:8000/> you should see an it-works page, as well as output on the server console.

(django) $ python3 manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).

You have 14 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

February 23, 2018 - 07:13:00
Django version 2.0.2, using settings 'myprj.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Not Found: /favicon.ico
[23/Feb/2018 07:19:38] "GET /favicon.ico HTTP/1.1" 404 1971
Not Found: /favicon.ico
[23/Feb/2018 07:19:38] "GET /favicon.ico HTTP/1.1" 404 1971
[23/Feb/2018 07:19:41] "GET / HTTP/1.1" 200 16348
[23/Feb/2018 07:19:41] "GET /static/admin/css/fonts.css HTTP/1.1" 200 423
[23/Feb/2018 07:19:41] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 200 80304
[23/Feb/2018 07:19:41] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 200 81348
[23/Feb/2018 07:19:41] "GET /static/admin/fonts/Roboto-Bold-webfont.woff HTTP/1.1" 200 82564

A couple of things to note:

  • the server's document root is the directory from which the server is started e.g. myprj/
  • the server may be stopped using Ctrl-C
  • localhost:8000 only displays an it-worked page when DEBUG = True is in settings.py and the only urlpattern in myprj/myprj/urls.py is admin/ (more later)

manage.py is in fact equivalent to django-admin and thus to python3 -m django, but it can do more because it both inserts the base directory explicitly by name into sys.path (rather than just leaving '' i.e. the working directory in sys.path) and sets the value of the DJANGO_SETTINGS_MODULE environment variable to the project settings module settings.py. Thus manage.py is different for each project. This can be verified by starting a Python shell via manage.py shell.

(django) $ python3 manage.py shell
Python 3.6.1 (default, Apr 24 2017, 11:44:31) 
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 
>>> import sys
>>> sys.path
['/home/keith/dev/python/django/myprj', ...]
>>> 
>>> import os
>>> os.getenv("DJANGO_SETTINGS_MODULE")
'myprj.settings'
>>> 
>>> quit()
(django) $ 

Using a Database

A Django project and its applications typically make use of a database. Even if you have no need of a database on an application level, a database is necessary in order to use certain Django infrastructure-oriented features such as user authentication and sessions (although these features can be disabled too if desired). In either case, one way to get started is to use a SQLite database. SQLite is a lightweight, open source database library and shell that provides an RDBMS-like interface. It's part of the Python standard library (the sqlite3 module; no driver needs to be installed separately).

Database settings must be specified using the DATABASES setting in settings.py, but if you use SQLite then there is nothing to configure, because the default value is:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

Another convenience is that Django creates the SQLite database automatically when it first tries to access it. In fact, this happened when we started the HTTP development server. The database is the db.sqlite3 file in the base directory.

myprj/
    db.sqlite3
    manage.py
    myprj/
        __init__.py
        settings.py
        urls.py
        wsgi.py

Django also supports MySQL, PostgreSQL, etc. through a Python DB API 2.0-compliant database connector such as MySQLdb, Mysql Connector/Python, Psycopg, etc. that must be installed separately. In this case, the database must be created outside of Django (more later). We'll stay with SQLite for now.

Database objects are usually created in Django using the manage.py program and its migrate subcommand (although they may be created manually).

A migration when using an RDBMS is essentially a set of one or more SQL statements that changes the database schema in some way. manage.py migrate applies migrations. Creating a project creates a set of initial migrations related to administration and authentication that should be applied immediately. We can see these with manage.py showmigrations.

(django) $ python3 manage.py showmigrations
admin
 [ ] 0001_initial
 [ ] 0002_logentry_remove_auto_add
auth
 [ ] 0001_initial
 [ ] 0002_alter_permission_name_max_length
 [ ] 0003_alter_user_email_max_length
 [ ] 0004_alter_user_username_opts
 [ ] 0005_alter_user_last_login_null
 [ ] 0006_require_contenttypes_0002
 [ ] 0007_alter_validators_add_error_messages
 [ ] 0008_alter_user_username_max_length
 [ ] 0009_alter_user_last_name_max_length
contenttypes
 [ ] 0001_initial
 [ ] 0002_remove_content_type_name
sessions
 [ ] 0001_initial

To get a feel for what a migration is, use manage.py sqlmigrate «app_label» «migration_name» to display one of the pending migrations.

(django) $ python3 manage.py sqlmigrate admin 0001_initial
BEGIN;
--
-- Create model LogEntry
--
CREATE TABLE "django_admin_log" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "action_time" datetime NOT NULL, "object_id" text NULL, "object_repr" varchar(200) NOT NULL, "action_flag" smallint unsigned NOT NULL, "change_message" text NOT NULL, "content_type_id" integer NULL REFERENCES "django_content_type" ("id") DEFERRABLE INITIALLY DEFERRED, "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "django_admin_log_content_type_id_c4bce8eb" ON "django_admin_log" ("content_type_id");
CREATE INDEX "django_admin_log_user_id_c564eba6" ON "django_admin_log" ("user_id");
COMMIT;

Let's apply all of the initial migrations using manage.py migrate. This will update the SQLite database.

(django) $ python3 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 auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK

(django) $ python3 manage.py showmigrations
admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
sessions
 [X] 0001_initial

If you're curious, run the SQLite CLI/shell with the database and see what's there (you'll need to install the SQLite CLI to do this; on Ubuntu that would be e.g. sudo apt-get install sqlite3). Most of the tables are empty at this point.

(django) $ sqlite3 db.sqlite3 
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite> 
sqlite> .tables
auth_group                  auth_user_user_permissions
auth_group_permissions      django_admin_log          
auth_permission             django_content_type       
auth_user                   django_migrations         
auth_user_groups            django_session            
sqlite> 
sqlite> select * from django_migrations;
1|contenttypes|0001_initial|2018-02-27 07:24:16.953003
2|auth|0001_initial|2018-02-27 07:24:16.993802
3|admin|0001_initial|2018-02-27 07:24:17.020733
4|admin|0002_logentry_remove_auto_add|2018-02-27 07:24:17.048242
5|contenttypes|0002_remove_content_type_name|2018-02-27 07:24:17.089386
6|auth|0002_alter_permission_name_max_length|2018-02-27 07:24:17.104793
7|auth|0003_alter_user_email_max_length|2018-02-27 07:24:17.123304
8|auth|0004_alter_user_username_opts|2018-02-27 07:24:17.142049
9|auth|0005_alter_user_last_login_null|2018-02-27 07:24:17.160298
10|auth|0006_require_contenttypes_0002|2018-02-27 07:24:17.166242
11|auth|0007_alter_validators_add_error_messages|2018-02-27 07:24:17.187816
12|auth|0008_alter_user_username_max_length|2018-02-27 07:24:17.205524
13|auth|0009_alter_user_last_name_max_length|2018-02-27 07:24:17.224902
14|sessions|0001_initial|2018-02-27 07:24:17.235992
sqlite> 
sqlite> .quit
(django) $ 

Creating a Superuser

Infrastructure-oriented data is typically managed via the builtin admin application located in site-packages/django/contrib/admin/. This application provides a very full-featured interface to infrastructure-oriented data as well as any application-level data that you want to manage through Django. It's necessary to create a superuser if one wants to use the application. To create a superuser, use the manage.py program and its createsuperuser subcommand.

(django) $ python3 manage.py createsuperuser
Username (leave blank to use 'keith'): djangoadmin
Email address: 
Password: 
Password (again): 
Superuser created successfully.

Let's check this in the database.

(django) $ sqlite3 db.sqlite3 
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite> 
sqlite> select * from auth_user;
1|pbkdf2_sha256$100000$vzzM470YY9rJ$aJ7bWZXxHdhZ+HbbWBNi4FPneAcjBxAMgPe/vE8GnHw=||1|djangoadmin|||1|1|2018-02-27 07:55:30.876765|
sqlite> 
sqlite> .quit
(django) $ 

With a superuser created, you can now also browse to <http://localhost:8000/admin/> and then login as the superuser to the admin application.

Creating an Application

A Django application is a collection of directories and files that typically implements a web site. An application is typically created using the manage.py program and its startapp subcommand.

Creating an application creates an application directory.

Let's create an application myapp (our working directory is the base directory).

(django) $ python3 manage.py startapp myapp

That's it. The directory structure now looks like this:

myprj/
    db.sqlite3
    manage.py
    myapp/
        migrations/
            __init__.py
        admin.py
        apps.py
        __init__.py
        models.py
        tests.py
        views.py
    myprj/
        __init__.py
        settings.py
        urls.py
        wsgi.py

Note that all __init__.py files are zero-byte files.

Although it's not important for us at the moment, we'll now go ahead and register our application. In order for an application to be recognized by certain Django operations, it must be registered. Registration doesn't happen automatically. For example, manage.py makemigrations will only make migrations for registered applications.

Registration of an application is performed by inserting the name of the application's configuration class in the list of installed applications as defined by the INSTALLED_APPS setting in settings.py. The default value of INSTALLED_APPS is:

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

An application's configuration class is defined by default in the application module apps.py in the application directory. It's essentially a class that defines application metadata by means of class attributes (more later).

from django.apps import AppConfig

class MyappConfig(AppConfig):
    name = 'myapp'

Let's register our application - we specify the name of its configuration class (before the existing applications) relative to the base directory.

INSTALLED_APPS = [
    'myapp.apps.MyappConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Creating URL Routes and Views

As usual with an MVC-style web application the input to the Django controller is a URL. A URL leads to a view - a function or method that typically manipulates a model (more later) and returns content by rendering a template.

In order for a URL to lead to a view, the URL must match a URL in one of the objects in the urlpatterns list in a URL configuration module (aka a "URLconf"). The default URLconf is the urls.py module in the project directory. This is determined by the ROOT_URLCONF = 'myprj.urls' setting in settings.py. The default urls.py is:

"""myprj URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

Now we can see why the URL <http://localhost:8000/admin/> worked earlier - there is a pattern or mapping or "route" for admin/ (the view that it's mapped to isn't important here). Note that there is no leading slash on the URL (it's implied), and that URLs in Django, by convention, end in a slash. Django will intelligently redirect slashless URLs to URLs with a trailing slash if APPEND_SLASH = True (the default) in global_settings.py.

Note also that, instead of the path function, the re_path function may be imported and used to do more sophisticated matching using regular expressions (this was always the approach in earlier versions of Django).

The URL <http://localhost:8000/> also worked because the / URL is hard-wired to map to site-packages/django/views/debug.py when DEBUG = True is in settings.py.

Let's map the / URL to a view of our own making. First, we'll create the view in the default views module views.py in the application directory. The default views.py is:

from django.shortcuts import render

# Create your views here.

A view must accept an HttpRequest object (the parameter request is traditionally used) and in some way return an HttpResponse object that contains content. We'll create a function named index and return content in the simplest way possible - by creating an HttpResponse object and passing markup in the constructor call. The HttpResponse class is imported from django.http.

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.
def index(request):
    return HttpResponse("<!doctype html><html><head></head><body><h1>It works!</h1><p>You're in the view function myapp.views.index.</p></body></html>")

We'll import this module in urls.py and add a route to urlpatterns that maps the URL '' to views.index - that's right, we don't use '/'.

"""myprj URL Configuration
...

"""
from django.contrib import admin
from django.urls import path

from myapp import views

urlpatterns = [
    path('', views.index),
    path('admin/', admin.site.urls),
]

If we were to use / we'd get a warning on the HTTP server console.

WARNINGS:
?: (urls.W002) Your URL pattern '/' [name='index'] has a route beginning with a '/'. Remove this slash as it is unnecessary. If this pattern is targeted in an include(), ensure the include() pattern has a trailing '/'.

You should now see your custom it-works page by browsing to <http://localhost:8000/.>

Note that the default Content-Type header in an HttpReponse object is Content-Type: text/html; charset=utf-8 based on settings in global_settings.py.

# Default content type and charset to use for all HttpResponse objects, if a
# MIME type isn't manually specified. These are used to construct the
# Content-Type header.
DEFAULT_CONTENT_TYPE = 'text/html'
DEFAULT_CHARSET = 'utf-8'

Let's do this exercise again, this time by rendering a "template" (we won't get into the Django Template Language (DTL) yet). We'll create an application templates directory named templates in the application directory and another myapp directory in templates (the reason for this directory structure has to do with the namespacing of templates; more later). We'll then create e.g. index.html in myapp/templates/myapp.

<!doctype html>
<html lang="en">
<head/>
<body>

<h1>It works!</h1>

<p>You're in the view function myapp.views.index.</p>

</body>
</html>

We now have:

myprj/
    manage.py
    myapp
        migrations/
            __init__.py
        templates/
            myapp/
                index.html
        admin.py
        apps.py
        __init__.py
        models.py
        tests.py
        views.py
    myprj/
        __init__.py
        settings.py
        urls.py
        wsgi.py

Instead of returning an HttpResponse object directly, we'll return the result of a call to the render function, passing it the request object and the name of our template relative to the application template directory templates.

from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request, 'myapp/index.html')

Again, you should now see your custom it-works page by browsing to <http://localhost:8000/.> Additional views could be added in the same way (although typically a separate application-specific "include URLconf" would be used for all of the URLs associated with a particular application; more later).

And there we are. A simple, locally-deployed Django 2.0 web site with just the basic scaffolding.

Current rating: 5

Categories

Tags

Authors

Feeds

RSS / Atom