Django 是一个 使用 Python 编程语言的全栈 web 开发框架。
Documentation: Writing your first Django app
下载和安装
pip install Django
初始化项目
在 Django 项目的目录下执行:
$ django-admin startproject <SITENAME>
启动项目
$ python manage.py runserver
# more specifically:
$ python manage.py runserver 0.0.0.0:8000
启动 app
$ python manage.py startapp polls
项目文件结构
mysite/ # 项目名,对 Django 不重要
|--- manage.py # 命令行接口
|--- mysite/ # 项目包,用来 `import`
|--- __init__.py
|--- settings.py # 设置,类似 Jekyll 中的 __config.yml
|--- urls.py # 网站的路由设定
|--- wsgi.py # 兼容 WSGI 的 web 服务器的接口
|--- asgi.py # Asynchronous Server Gateway Interface
|--- templates/ # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#customizing-your-project-s-templates
|--- admin/ # 魔改管理员页面
|--- base_site.html # 从 Django 源码的此路径复制粘贴而来,然后修改
|--- index.html # 魔改的管理员首页
django-polls
|--- README.rst # 介绍,安装指南之类
|--- pyproject.toml
|--- setup.cfg
|--- setup.py
|--- MANIFEST.in # 指定需要一起打包的非 python 文件
|--- polls/ # 名为 polls 的 app,一个项目(project)中可以有多个app
|--- __init__.py
|--- apps.py # 教程中没有,https://docs.djangoproject.com/en/5.0/ref/applications/
|--- urls.py # polls 这一 app 中的路由设定,相对于 mysite/urls.py
|--- views.py # 指定页面渲染使用的模板
|--- templates/ # 模板
|--- polls/ # 帮助 Django 计算实际路径,防止和其他 app 撞车
|--- index.html
|--- detail.html
|--- results.html
|--- migrations/
|--- __init__.py
|--- admin.py # 将 app 中部分数据的读写权限赋予管理员
|--- models.py # 数据库的结构定义
|--- tests.py # 测试
|--- static/ # 静态资源的文件夹
|--- polls/ # 帮助 Django 计算实际路径,防止和其他 app 撞车
|--- style.css
|--- images/background.png
polls/apps.py
from django.apps import AppConfig
class PollsConfig(AppConfig):
name = "polls"
verbose_name = "<WHATEVER>"
前端:urls, views, templates, static
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),
]
include()
用来引入其他 python 模块的 url,只有 admin.site.urls
不需要 include()
polls/urls.py
from django.urls import path
from . import views
app_name = "polls"
urlpatterns = [
# ex: /polls/
path("", views.IndexView.as_view(), name="index"),
# ex: /polls/5/
path("<int:question_id>/", views.DetailView.as_view(), name="detail"),
# ex: /polls/5/results/
path("<int:question_id>/results/", views.ResultsView.as_view(), name="results"),
# ex: /polls/5/vote/
path("<int:question_id>/vote/", views.vote, name="vote"),
]
为防止不同的路由规则重名,用 app_name
进行区分,在模板部分使用。
polls/views.py
一个 view 就是一类网页,view + template 类似于 Jekyll 中的 layout。每个 view 由一个函数表示。
-
简单 view,教学用,后面被 abstract views 取代
# SIMPLE VIEWS REPLACED BY ABSTRACT ONES from .models import Question from django.http import HttpResponse from django.template import loader 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)) # OR: from django.shortcuts import render 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) 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}) # OR: from Django.shortcuts import get_object_or_404 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, "polls/detail.html", {"question": question}) def results(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, "polls/results.html", {"question": question})
把有相似之处的 views 抽象成类:
# ABSTRACT VIEWS
from django.urls import reverse
from django.views import generic
from django.utils import timezone
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 (not including those set to be
published in the future).
"""
# https://docs.djangoproject.com/en/5.0/intro/tutorial05/#id6
return Question.objects.filter(
pub_date__lte=timezone.now()
).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"
vote 这个 view 不同,成功时用 HttpResponseRedirect
跳转到另一个 view
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"])
# request.POST 是一个字典,各值均为 str
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,)))
polls/templates/polls/index.html
{% raw %}
<!-- https://docs.djangoproject.com/en/5.0/intro/tutorial06/#id2 -->
{% load static %}
<link rel="stylesheet" href="{% static 'polls/style.css' %}">
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endraw %}
在真实的项目中 HTML 应当完整,包括 <head />
之类
模板中的 url
应当使用 {% raw %} {% url %} {% endraw %}
, 该语句的第一个参数是 polls/url.py 中定义的各个 path 中的 name 参数,第二个参数是路由中的变量。
polls/templates/polls/detail.html
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %} <!-- against Cross Site Request Forgeries -->
<fieldset>
<legend><h1>{{ question.question_text }}</h1></legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% 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 %}
</fieldset>
<input type="submit" value="Vote">
</form>
polls/templates/polls/results.html
<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>
polls/static/polls/style.css
不是由 Django 生成的网页文件不能使用 {% raw %}{% static %}{% endraw %}
/* https://docs.djangoproject.com/en/5.0/intro/tutorial06/#id1 */
li a {
color: green;
}
/* https://docs.djangoproject.com/en/5.0/intro/tutorial06/#id3 */
body {
background: white url("images/background.png") no-repeat;
}
后端:数据库
https://docs.djangoproject.com/en/5.0/intro/tutorial02/
mysite/settings.py
文档写的不清楚,据说 polls/app.py
中应该定义一个名为 PollsConfig
的类,但是文档中完全没有这个内容。
在命令行中运行 python manage.py makemigrations polls
可以更新对数据库的修改
在命令行中运行 python manage.py sqlmigrate polls 0001
可以看到 python 语句对应的 SQL 命令
在命令行运行 python manage.py migrate
,会把 INSTALLED_APPS 中的数据建成数据表。
TIME_ZONE = 'America/Chicago'
INSTALLED_APPS = [
"polls.apps.PollsConfig", # 开发者自己创建的数据 table
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
# SQLite
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "mydatabase",
}
}
# Postgresql
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mydatabase",
"USER": "mydatabaseuser",
"PASSWORD": "mypassword",
"HOST": "127.0.0.1",
"PORT": "5432",
}
}
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
polls/models.py
建立数据 model,用的是 python 的类
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200) # 每个类变量都是一个 field
pub_date = models.DateTimeField("date published") # 可以赋予人类可读的名称
def __str__(self):
return self.question_text
@admin.display(
boolean=True,
ordering="pub_date",
description="Published recently?",
) # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id8
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE) # 外键,每个 Choice 对应一个 Question
choice_text = models.CharField(max_length=200) # 有些 field 有必填参数
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text
命令行中的 Django 数据库 API
在命令行中运行 python manage.py shell
# =========================================================
>>> 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
# 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=datetime.timezone.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)>]>
# =========================================================
# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>
# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith="What")
<QuerySet [<Question: What's up?>]>
# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>
# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
...
DoesNotExist: Question matching query does not exist.
# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>
# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True
# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. **Django creates
# a set to hold the "other side" of a ForeignKey relation**
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)
# Display any choices from the related object set -- none so far.
>>> q.**choice_set**.all()
<QuerySet []>
# Create three choices.
>>> q.choice_set.create(choice_text="Not much", votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text="The sky", votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text="Just hacking again", votes=0)
# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>
# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3
# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith="Just hacking")
>>> c.delete()
# =========================================================
polls/admin.py
admin.site.register
的第二个参数是 ModelAdmin
,用来指定第一个参数显示哪些列
admin.TabularInline
将本数据插到外键的下面,以表格的形式。
QuestionAdmin.list_disply
选择按行展示时显示的属性
from django.contrib import admin
from .models import Question
from .models import Choice # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id3
# https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id4:
class ChoiceInline(admin.TabularInline): # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id5
model = Choice
extra = 3
# https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id1
class QuestionAdmin(admin.ModelAdmin):
# https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id2
fieldsets = [
(None, {"fields": ["question_text"]}),
("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
]
inlines = [ChoiceInline]
list_display = ["question_text", "pub_date", "was_published_recently"] # https://docs.djangoproject.com/en/5.0/intro/tutorial07/#id6
# admin.site.register(Choice)
admin.site.register(Question,QuestionAdmin)
在命令行运行 python manage.py createsuperuser
,根据提示输入信息,建立管理员账户
在命令行运行 python manage.py runserver
,启动开发服务器
在浏览器输入 http://127.0.0.1:8000/admin/,可以看到管理员登陆界面
测试
polls/tests.py
测试用例继承自 django.test.TestCase
测试方法的名字开头必须是 test_
TestCase
自带 client,所以不需要“前端测试”一节中的 from django.test import Client
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)
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)
# Test View
# https://docs.djangoproject.com/en/5.0/intro/tutorial05/#id8
def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is displayed.
"""
response = self.client.get(reverse("polls:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question2, question1],
)
# https://docs.djangoproject.com/en/5.0/intro/tutorial05/#id10
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)
运行测试
$ python manage.py test polls
后端:用命令行发现 bug
$ python manage.py shell
>>> 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
前端
在命令行中进行.
$ python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> 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?>]>
第三方工具
先安装 debug tools
python -m pip install django-debug-toolbar
剩下的工作看各个第三方工具自己的文档。
第三方包的集合站:https://djangopackages.org/
对 app 进行打包(选做)
而不是对整个 project 进行打包
django-polls/README.rst
=====
Polls
=====
Polls is a Django app to conduct web-based polls. For each question,
visitors can choose between a fixed number of answers.
Detailed documentation is in the "docs" directory.
Quick start
-----------
1. Add "polls" to your INSTALLED_APPS setting like this::
INSTALLED_APPS = [
...,
"polls",
]
2. Include the polls URLconf in your project urls.py like this::
path("polls/", include("polls.urls")),
3. Run ``python manage.py migrate`` to create the polls models.
4. Start the development server and visit http://127.0.0.1:8000/admin/
to create a poll (you'll need the Admin app enabled).
5. Visit http://127.0.0.1:8000/polls/ to participate in the poll.
django-polls/pyproject.toml
[build-system]
requires = ['setuptools>=40.8.0']
build-backend = 'setuptools.build_meta'
django-polls/setup.cfg
[metadata]
name = django-polls
version = 0.1
description = A Django app to conduct web-based polls.
long_description = file: README.rst
url = https://www.example.com/
author = Your Name
author_email = y[email protected]
license = BSD-3-Clause # Example license
classifiers =
Environment :: Web Environment
Framework :: Django
Framework :: Django :: X.Y # Replace "X.Y" as appropriate
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
[options]
include_package_data = true
packages = find:
python_requires = >=3.10
install_requires =
Django >= X.Y # Replace "X.Y" as appropriate
django-polls/setup.py
from setuptools import setup
setup()
django-polls/MANIFEST.in
include LICENSE
include README.rst
recursive-include polls/static *
recursive-include polls/templates *
命令行操作
打包:在 django-polls/
执行:
python setup.py sdist
使用打包好的 app:
python -m pip install --user django-polls/dist/django-polls-0.1.tar.gz
卸载:
python -m pip uninstall django-polls
部署项目
两种 interface: WSGI 和 ASGI,后者支持异步请求,似乎更加先进,先只学这一个吧……
mysite/asgi.py
里面有个名为 application 的 callable, ASGI 服务器需要调用这个callable,要使用中间件,需要在这个文件 import,然后 application = Middleware(application)
环境变量 DJANGO_SETTINGS_MODULE
设定设置模块的路径
三种 ASGI 的服务器可供选择:Daphne, Hypercorn, Uvicorn, (Gunicorn )
ASGI Server | 安装命令 | 运行命令 |
---|---|---|
Daphne | python -m pip install daphne | daphne myproject.asgi:application |
Hypercorn | python -m pip install hypercorn | hypercorn myproject.asgi:application |
Uvicorn | python -m pip install uvicorn | python -m uvicorn myproject.asgi:application |
Gunicorn | python -m pip install uvicorn gunicorn | python -m gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker |
本文收录于以下合集: