The Django Web
Framework 101

Web Programming Course

SUT • Fall 2018

Outline

  • Introduction
    • Getting Started
  • Creating a Project
  • Running the Development Server
    • Creating a Poll Application
  • The Admin Site

Introduction

Introduction

  • high-level Python Web framework
    • rapid development
    • clean
    • pragmatic design
  • focus on writing your app without needing to reinvent the wheel.
  • It’s free and open source
  • Released publicly under BSD
    • license in July 2005

Django Slogans

  • Ridiculously fast

    • designed to help developers take applications from concept to completion as quickly as possible

  • Reassuringly secure

    • takes security seriously and helps developers avoid many common security mistakes

  • Exceedingly scalable

    • Some of the busiest sites on the Web leverage Django’s ability to quickly and flexibly scale

FAQ

  • Is Django stable?

  • Does Django scale?

    • Compared to development time, hardware is cheap

    • shared-nothing” architecture

    • separates components such as its database layer and application layer.

  • ...

Design philosophies

  • Loose coupling

  • Less code

  • Quick development

  • Don’t repeat yourself (DRY)

  • Explicit is better than implicit

  • Consistency

Django Architecture

  • Django follows the Model–View–Controller architectural pattern
  • The core Django MVC framework consists of an object-relational mapper which mediates between
  • data models (defined as Python classes) and a relational database (Model)
  • a system for processing requests with a web templating system (View)
  • a regular-expression-based URL dispatcher (Controller)

Django Release Plans

  • Feature releases (A.B, A.B+1, etc.) will happen roughly every eight months.
  • Patch releases (A.B.C, etc.) will be issued as needed, to fix bugs and/or security issues.

Django’s release process

Getting Started

Installing Django

  • Download Django from djangoproject.com
    • Install it:
      • python setup.py install
      • pip install Django
  • Verify it
In [1]: import django                                                                                                                                                                                       

In [2]: django.VERSION                                                                                                                                                                                      
Out[2]: (2, 1, 2, 'final', 0)

$ python -m django --version
2.1.2

Example: Building a Poll app

  • Django describes itself as “the Web framework
    for perfectionists with deadlines”
  • So, let's put ourselves on deadline and see how
    fast we can produce a simple blog using Django
  • We will address the perfectionist side later

Creating a Project

  • The easiest way to organize your Django code is
    to use what Django calls a project
  • A project is a directory, containing settings for an
    instance of Django
  • Django comes with a utility called django-admin.py to streamline tasks such as the creation of the project directories
$ django-admin startproject mysite
$ tree mysite
mysite
├── db.sqlite3
├── manage.py
└── mysite
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

Project Files

  • Besides __init__.py, the project contains the
  • followings files:
    • manage.py: a utility for working with this Django project
    • settings.py: a file containing default settings for your project, including database information, debugging flags, and other important variables
    • urls.py: a configuration file that maps URL patterns to actions your applications perform
    • wsgi.py: an entry-point for WSGI-compatible web servers to serve your project

The Development Server

  • Django comes with a built-in lightweight Web server written purely in Python
  • It helps you develop things rapidly, without having to deal with configuring a production server – such as Apache/nginx – until you’re ready for production
  • It automatically detects when you make changes to your Python source files and reloads those modules

Running the Server

  • Running the development server is as simple as issuing a single command:

 

 

 

 

 

 

behnam@behnam-HP-Z230-SFF-Workstation:~/temp/mysite$ python3 manage.py runserver 8080
Performing system checks...

System check identified no issues (0 silenced).

You have 15 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.

November 27, 2018 - 10:02:01
Django version 2.1.2, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8080/
Quit the server with CONTROL-C.

Creating the Polls app

Contents

  • Now that we have a project, we can create applications (or “apps”) within it
  •  
  •  
  •  
  •  
  •  

 

 

 

  • To activate the app, we need to add its name 'poll' to INSTALLED_APPS in settings.py
$ python manage.py startapp polls
$ tree polls

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

Write your first view

from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")
  • Open the file polls/views.py and put the following Python code in it

 

 

 

  • map it to a URL (create file polls/urls.py)
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

Write your first view

  • edit mysite/urls.py

 

 

 

 

 

 

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]
$ python manage.py runserver

Path

  • The path() function is passed four arguments
    • route
      • route is a string that contains a URL pattern
    • view
    • kwargs
      • Arbitrary args can be passed in a dictionary to the target view.
    • name
      • Naming your URL for refrencing

Database setup

Database Setting

  • open up mysite/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydatabase',
        'USER': 'mydatabaseuser',
        'PASSWORD': 'mypassword',
        'HOST': '127.0.0.1',
        'PORT': '5432',
    }
}
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'mydatabase',
    }
}

 Installed App

$ python manage.py migrate

Models

  • database layout, with additional metadata
from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Creating Migrations

$ python manage.py makemigrations polls

Migrations for 'polls':
  polls/migrations/0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice
  • migration file
    • human-editable
$ python manage.py sqlmigrate polls 0001

BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL
);
--
-- Create model Question
--
CREATE TABLE "polls_question" (
    "id" serial NOT NULL PRIMARY KEY,
    "question_text" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
);
--
-- Add field question to choice
--
ALTER TABLE "polls_choice" ADD COLUMN "question_id" integer NOT NULL;
ALTER TABLE "polls_choice" ALTER COLUMN "question_id" DROP DEFAULT;
CREATE INDEX "polls_choice_7aa0f6ee" ON "polls_choice" ("question_id");
ALTER TABLE "polls_choice"
  ADD CONSTRAINT "polls_choice_question_id_246c99a640fbbd72_fk_polls_question_id"
    FOREIGN KEY ("question_id")
    REFERENCES "polls_question" ("id")
    DEFERRABLE INITIALLY DEFERRED;

COMMIT;

Database Change Life Cycle

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Rendering model states... DONE
  Applying polls.0001_initial... OK

Playing with the API

>> from polls.models import Choice, Question  # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID.
>>> q.id
1

Playing with the API

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

Django Admin

Philosophy

  • for your staff or clients to add, change, and delete content
  • separation between “content publishers” and the “public” site
  • unified interface for site administrators to edit content
  • The admin isn’t intended to be used by site visitors. It’s for site managers.

Admin User

 % python3 manage.py createsuperuser
Username (leave blank to use 'behnam'): behnam
Email address: behnam.hatami@gmail.com
Password: 
Password (again): 
Superuser created successfully.

Admin page

Add Poll App to Admin Page

  • edit polls/admin.py
from django.contrib import admin

from .models import Question

admin.site.register(Question)

Explore the free admin functionality

Creating the Public Interface

Overview

  • A view is a “type” of Web page in your Django application that generally serves a specific function and has a specific template
    • Blog homepage – displays the latest few entries.
    • Year-based archive page – displays all months with entries in the given year.
    • Day-based archive page – displays all entries in the given day.
    • Comment action – handles posting comments to a given entry.

Poll Application

  • Question “index” page – displays the latest few questions.
  • Question “detail” page – displays a question text, with no results but with a form to vote.
  • Question “results” page – displays results for a particular question.
  • Vote action – handles voting for a particular choice in a particular question.

Writing More Views

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)
from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

More Complex Views

from django.http import HttpResponse

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged

Basic Templating

  • Template namespacing

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li>
            <a href="/polls/{{ question.id }}/">
                {{ question.question_text }}
            </a>
        </li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

View With Template

from django.http import HttpResponse
from django.template import loader

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))

shortcuts

from django.shortcuts import render

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

Raising a 404 error

from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})
from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
  • shortcut:

<li>
    <a href="/polls/{{ question.id }}/">
        {{ question.question_text }}
    </a>
</li>
<li>
    <a href="{% url 'detail' question.id %}">
        {{ question.question_text }}
    </a>
</li>

Removing Hardcoded URLs

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

Namespacing URL names

<li>
    <a href="{% url 'polls:detail' question.id %}">
        {{ question.question_text }}
    </a>
</li>

More on Views

HTML Form

<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" 
           id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">
        {{ choice.choice_text }}
    </label>
    <br>
{% endfor %}
<input type="submit" value="Vote">
</form>

Vote View

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Result View

from django.shortcuts import get_object_or_404, render


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})
<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- 
    {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

Generic views

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

Generic views

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    ... # same as above, no changes needed.

Result View

from django.shortcuts import get_object_or_404, render


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})
<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>
        {{ choice.choice_text }} -- {{ choice.votes }} 
        vote{{ choice.votes|pluralize }}
    </li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
  • Template:

Generic Views

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'

Generic Views URL Conf

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

Test

Introduction

  • Tests are simple routines that check the operation of your code
  • Automated Testing
  • Tests will save you time

  • Tests don’t just identify problems, they prevent them

  • Tests make your code more attractive

  • Tests help teams work together

Finding a Bug

>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True

Create a test to expose the bug

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question


class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

More comprehensive tests

def test_was_published_recently_with_old_question(self):
    """
    was_published_recently() returns False for questions whose pub_date
    is older than 1 day.
    """
    time = timezone.now() - datetime.timedelta(days=1, seconds=1)
    old_question = Question(pub_date=time)
    self.assertIs(old_question.was_published_recently(), False)

def test_was_published_recently_with_recent_question(self):
    """
    was_published_recently() returns True for questions whose pub_date
    is within the last day.
    """
    time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
    recent_question = Question(pub_date=time)
    self.assertIs(recent_question.was_published_recently(), True)

The Django test client

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()

>>> # get a response from '/'
>>> response = client.get('/')
Not Found: /
>>> # we should expect a 404 from that address; if you instead see an
>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
>>> # omitted the setup_test_environment() call described earlier.
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded URL
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#39;s up?</a></li>\n    \n    </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>

Improving our view

# polls/views.py
 
class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


# polls/views.py

from django.utils import timezone

def get_queryset(self):
    """
    Return the last five published questions (not including those set to be
    published in the future).
    """
    return Question.objects.filter(
        pub_date__lte=timezone.now()
    ).order_by('-pub_date')[:5]

Testing our new view

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text='Future question.', days=5)
        url = reverse('polls:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text='Past Question.', days=-5)
        url = reverse('polls:detail', args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

Static Files

Introduction

  • web applications generally need to serve additional files
    • images
    • JavaScript
    • CSS
    • ...
  • multiple sets of static files provided by each application
  • django.contrib.staticfiles

Customize your app look and feel

  • Create a directory called static in your polls directory
  • STATICFILES_FINDERS
  • create static file (polls/static/polls/style.css):
# polls/static/polls/style.css
li a {
    color: green;
}

# polls/templates/polls/index.htm
{% load static %}

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">

Adding a background-image

  • create a subdirectory for images
  • add background image
    • polls/static/polls/images/background.gif
# polls/static/polls/style.css

body {
    background: white url("images/background.gif") no-repeat;
}

More on Admin site

References

Title Text

Made with Slides.com