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
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
-
Release cadence
-
2.0, 2.1, 2.2 (LTS), 3.0, 3.1, 3.2 (LTS)
-
-
Deprecation policy
Getting Started
Installing Django
- Download Django from djangoproject.com
-
Install it:
- python setup.py install
- pip install Django
-
Install it:
- 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
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
-
route is a string that contains a URL pattern
-
view
-
Call view function with an HttpRequest object
-
Call view function with an HttpRequest object
-
kwargs
- Arbitrary args can be passed in a dictionary to the target view.
- Arbitrary args can be passed in a dictionary to the target view.
-
name
- Naming your URL for refrencing
- Naming your URL for refrencing
-
route
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
- django.contrib.admin – The admin site. You’ll use it shortly.
- django.contrib.auth – An authentication system.
- django.contrib.contenttypes – A framework for content types.
- django.contrib.sessions – A session framework.
- django.contrib.messages – A messaging framework.
- django.contrib.staticfiles – A framework for managing static files.
$ 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
- Change your models (in models.py).
- Run python manage.py makemigrations to create migrations for those changes
- Run python manage.py migrate to apply those changes to the database
-
python manage.py check
- check for common errors
$ 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'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
- Django Tutorials
Title Text
The Django Web Framework
By Behnam Hatami
The Django Web Framework
The Django Web Framework / Web Programming Course @ SUT, Fall 2018
- 1,769