创建项目
安装依赖包:
pip install Django
python -m django --version
创建项目:
django-admin startproject mysite
得到:
mysite/ --项目目录,名字可以随意起
manage.py --用于与项目交互的命令行脚本
mysite/ --项目的Python包,后面需要引入的话需要写成类似 import mysite.urls
__init__.py --标记该目录为一个Python包
settings.py --项目配置文件
urls.py --项目url说明
asgi.py --兼容asgi web服务器的入口
wsgi.py --兼容wsgi web服务器的入口
运行网站:
cd mysite
python manage.py runserver
访问http://127.0.0.1:8000/能看到:
自定义端口:
python manage.py runserver 8080
自定义IP(0是0.0.0.0的简写):
python manage.py runserver 0:8000
现在用的是Django自带的测试环境服务器,不建议用于生产环境。
创建app
项目= app + 配置。一个项目能包含多个app,一个app可属于多个项目。
在当前项目下创建一个新的app:
python manage.py startapp polls
得到如下目录结构:
polls/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
编辑views.py,添加:
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello Django!")
新建urls.py,添加:
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index')
]
编辑mysite\mysite\urls.py,添加:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('polls/', include('polls.urls')), #引用其它URLconf文件
]
include能引用其它URLconf文件,当Django遇到这个include时,会将url中匹配该入口点的部分’polls/'截掉,剩下的部分发给polls.url进一步处理。
现在重新启动服务器,访问http://127.0.0.1:8000/polls/,即可看到:
Hello Django!
urls.py中的path函数很关键:
path('', views.index, name='index')
# views.index
def index(request):
return HttpResponse("Hello Django!")
path()函数有4个参数:
- route,url 模式
- view,当找到匹配的route时,调用对应的view函数,返回的是一个HttpRequest对象
- kwargs (可选)
- name (可选)
修改settings.py
配置数据库
mysite\settings.py中有数据库配置选项:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
默认用python自带的sqlite3。
ENGINE可以替换为:
‘django.db.backends.sqlite3’,
‘django.db.backends.postgresql’,
‘django.db.backends.mysql’,
‘django.db.backends.oracle’
NAME为数据库名,默认在项目根目录(BASE_DIR)存储sqlite数据库文件:
配置语言和时区
setting.py配置语言和时区为中国:
# Chinese Shanghai
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
# db does not use utc time
USE_TZ = False
配置INSTALLED_APPS
Django的app是“可插拔的”,INSTALLED_APPS指定了项目中所有活动的app名称(app可被用于多个项目),默认有:
INSTALLED_APPS = [
'django.contrib.admin', # 管理页面
'django.contrib.auth', # 认证系统
'django.contrib.contenttypes', # 内容类型框架
'django.contrib.sessions', # session框架
'django.contrib.messages', # 消息框架
'django.contrib.staticfiles', # 静态文件管理框架
]
这些app可能会用到数据库中的一个或多个表,所以在使用这些app前要创建数据库:
python manage.py migrate
migrate指令会检查所有活动app,并创建它们所需的数据库表。上述指令创建出的表如下:
如果不需要某些app,可以在执行migrate命令前注释掉。
创建Model
在polls/models.py中添加:
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published') # human-readable name
class Choice(models.Model):
questions = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
接着要在settings.py中添加poll app。
polls/app.py默认为:
from django.apps import AppConfig
class PollsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'polls'
只需把PollsConfig包含到INSTALLED_APPS即可:
INSTALLED_APPS = [
'polls.apps.PollsConfig', # 添加polls
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
现在可以执行:
python manage.py makemigrations polls
显示:
Migrations for 'polls':
polls/migrations/0001_initial.py
- Create model Question
- Create model Choice
makemigrations命令告诉Django models有变化,需要将变化存储为migration(migration是Django用来存储models变化的文件)。
然后运行命令:
python manage.py sqlmigrate polls 0001
能看到针对项目数据库类型自动生成的sql语句:
BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "questions_id" bigint NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "polls_choice_questions_id_d356887e" ON "polls_choice" ("questions_id");
COMMIT;
注意:
- 表名默认为 app名_模型名
- 主键会自动生成(可以覆写)
- 外键名为’关联表名_id’ (也可以覆写)
- 外键关系会被 FOREIGN KEY 约束显式指出
sqlmigrate指令只是打印出建表语句,并没真正执行这些sql。
python manage.py check
现在可以再次执行migrate指令了:
python manage.py migrate
它会为上述models在database中建表。现在数据库中的表如下:
django_migrations表中记录了所有未应用的migration,migrate指令会执行这些migration。
至此,总结一下修改model的步骤:
python manage.py makemigrationspython manage.py migrate
专门记录migration,可以便于在版本控制系统中管理,也能让其它开发者使用。
玩转API
在python shell中可以使用Django提供的各种api。
打开python shell:
python manage.py shell
python
在shell中可以尝试database 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
# 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)>]>
__str__()
class Question(models.Model):
# ...
def __str__(self):
return self.question_text
class Choice(models.Model):
# ...
def __str__(self):
return self.choice_text
然后再在polls/models.py中添加一个方法:
import datetime
from django.db import models
from django.utils import timezone
class Question(models.Model):
# ...
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
注意,这里分别引入了标准库的datetime和Django中的时间工具库:
import datetime
from django.utils import timezone
python manage.py shell
>>> from polls.models import Choice, Question
# 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()
Django Admin
创建超级用户:
python manage.py createsuperuser
用户名 (leave blank to use 'jiaqi'): admin
电子邮件地址: admin@126.com
Password:
Password (again):
python manage.py runserverdjango.contrib.auth
polls/admin.py
from django.contrib import admin
from .models import Question
admin.site.register(Question)
保存代码,刷新页面:
太方便了。
创建更多的view
view是提供特定功能的一种网页,并有特定的模板。
polls/views.py
def detail(request, question_id):
return HttpResponse("Question %s." % question_id)
def results(request, question_id):
response = "Result of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
response = "Voting question %s."
return HttpResponse(response % question_id)
polls/urls.py
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'),
]
Voting question 1.
settings.pyROOT_URLCONF = 'mysite.urls'mysite/mysite/urls.pyurlpatterns
使用模板
在polls目录下创建文件夹:templates。
settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True, # BACKEND是DjangoTemplates,且该选项为True。DjangoTemplates会自动查找所有INSTALLED_APP中的templates子目录
'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',
],
},
},
]
在templates文件夹下再建立一个polls文件夹,然后创建一个index.html:
这里涉及到了模板的命名空间问题。之所以不把index.html直接放到templates目录,是因为Django总会选择第一个名字匹配的模板,如果不同的app间有命名相同的模板,Djang就无法区别。所以最好的方法是为模板创建命名空间, 即在templates目录下再创建一层与app同名的目录。
index.htm中写入以下内容:
{% 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 %}
修改views.py,使用模板:
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))
保存代码,刷新网页:
上述代码可以简写为:
from django.shortcuts import render
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {
'latest_question_list' : latest_question_list,
}
return render(request, 'polls/index.html', context)
再写一个detail.html模板:
{{ question }}
重写detail的view:
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})
如果找不到就触发404异常。
上述代码可简写为:
from django.shortcuts import get_object_or_404, render
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
get_list_or_404()
修改detail.html:
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
效果:
为了去除index.html中url的硬编码:
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
可以将其修改为:
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
上面的detail表示polls.urls.py中定义的url name为detail的path:
path('<int:question_id>/', views.detail, name='detail'),
这样,当polls.urls.py中的url定义改变时,只要name不变,template中就无需改变:
...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...
这就实现了前后端关于url名称的解耦。
为了区分不同app之间相同命名的path,需要指定app_name作为url名称的命名空间:
app_name = 'polls' # 指定url命名空间
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'),
]
在模板中使用带命名空间的url:
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
使用表单
修改detail.html:
<form action="{% url 'polls:vote' question.id %}" method="post">
<!-- 防止post方法可能产生的csrf攻击,所有指向内部url的post form都应该加上这句 -->
{% csrf_token %}
<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 %}
<!-- forloop.counter:当前循环计数,这里分别是 1、2 -->
<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>
legend 元素为 fieldset 元素定义标题,
效果:
生成的html代码:
它的action是’polls:vote’,所以接下来重写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,)))
使用Form获取用户数据时,有以下惯用做法(不单是Django架构):
- 当会修改服务端数据时,用post方法;
- 处理完post请求后,返回一个HttpResponseRedirect,以防用户在刷新网页或点击返回按钮后,表单被重复提交;
处理完post请求,会跳转到results页面,下面添加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>
对应html:
投票完成后,效果如下:
注意这里的投票逻辑可能会存在竞争问题,即多个用户同时投票,结果可能会不正确。如何解决这一问题在这里暂不考虑。
generic views
上述view其实都做了类似的事:
- 根据url中的参数,从数据库取数据;
- 载入模板并用返回结果渲染;
这时可以用Django提供的generic views。
将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]
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.
然后将urls.py修改为:
from django.urls import path
from . import views
app_name = 'polls' # url命名空间
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'),
]
views.DetailView.as_view()
自动测试
在大型项目中,代码的一些改动可能牵一发动全身。写自动化测试的好处:
- 节省时间
- 避免错误(而非确定错误)
- 有利于团队合作(“Code without tests is broken by design.”——Jacob Kaplan-Moss, Django’s创造人之一)
TDD(Test-driven development)是指先写测试用例,再写代码的开发模式。
polls/tests.py
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)
在控制台执行测试:
python manage.py test polls
该指令进行了以下操作:
manage.py test pollsdjango.test.TestCasetest_was_published_recently_with_future_questionassertIs()was_published_recently()True
打印结果如下:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
was_published_recently() returns False for questions whose pub_date
----------------------------------------------------------------------
Traceback (most recent call last):
File "E:\workspace\django_mysite\polls\tests.py", line 21, in test_was_published_recently_with_future_questi
on
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (failures=1)
Destroying test database for alias 'default'...
polls/models.py
def was_published_recently(self):
return timezone.now() >= self.pub_date >= timezone.now() - datetime.timedelta(days=1)
再次运行test,打印:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
tests.py越写越多怎么办?无所谓,越多越好,有时产生冗余也不是坏事。
有一些准则来让测试用例不那么乱:
- 每个model或view都单独写一个TestClass
- 每个测试场景都写个单独的test方法
- test方法名称就是测试的具体描述
关于测试,详见:Testing in Django
静态文件
和模板类似,Django的STATICFILES_FINDERS也会自动扫描所有INSTALLED_APPS中叫static的子文件夹寻找静态文件,所以最好也用与app同名的命名空间区分:
新建polls/static/polls/style.css:
li a {
color: green;
}
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">
重启server,刷新网页(注意这里需要重启server才能生效),效果如下:
加入背景图片:
修改style.css如下:
li a {
color: blue;
}
body {
background-image: url("images/morty.jpg");
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
}
效果如下:
重写admin模板
管理显示内容
修改polls/admin.py如下:
class QuestionAdmin(admin.ModelAdmin):
fields = ['pub_date', 'question_text']
admin.site.register(Question, QuestionAdmin)
fieldsets
class QuestionAdmin(admin.ModelAdmin):
# fields = ['pub_date', 'question_text']
fieldsets = [
('Text', {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date']}),
]
将关联表choice整合到question界面:
class ChoiceInline(admin.StackedInline):
model = Choice # Choice objects are edited on the Question admin page
extra = 3 # provide enough fields for 3 choices
class QuestionAdmin(admin.ModelAdmin):
# fields = ['pub_date', 'question_text']
fieldsets = [
('Text', {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'],
'classes' : ['collapse']}), # 可折叠
]
inlines = [ChoiceInline]
效果:
可以换成Tabular的样式嵌入:
class ChoiceInline(admin.TabularInline):
model = Choice # Choice objects are edited on the Question admin page
extra = 3 # provide enough fields for 3 choices
在展示所有Question的列表页面,也可以增加显示字段:
# fields = ['pub_date', 'question_text']
list_display = ('question_text', 'pub_date', 'was_published_recently') # 调用was_published_recently()作为新字段值
fieldsets = [
('Text', {'fields': ['question_text']}),
('Date information', {
'fields': ['pub_date'],
'classes': ['collapse']}),
]
inlines = [ChoiceInline]
前两个字段是可以点击排序的,自定义字段则不支持。而且自定义字段的名称默认也是函数名。
为了让这个方法支持排序,且改下名字,可以在models.py里给它加个装饰器:
from django.contrib import admin
@admin.display(
boolean=True,
ordering='pub_date',
description='Published recently?',
)
def was_published_recently(self):
return timezone.now() >= self.pub_date >= timezone.now() - datetime.timedelta(days=1)
效果如下:
接下来在QuestionAdmin里加一个过滤器:
list_filter = ['pub_date']
界面右侧多了个日期过滤器:
还可以增加搜索器:
search_fields = ['question_text']
LIKE
列表管理页面还默认提供了100条目/页的分区功能。
太方便了!
更改admin样式
运行下面指令找到Django源码目录:
python -c "import django; print(django.__path__)"
路径为:
/Users/xxx/miniconda3/envs/django_env/lib/python3.8/site-packages/django'
django/contrib/admin/templates/admin/base_site.htmlmysite/templates/admin/
{{ site_header|default:_('Django administration') }}
<h1 id="site-name"><a href="{% url 'admin:index' %}"> POLLS管理页 </a></h1>
settings.pyTEMPLATESDIRS
'DIRS': [BASE_DIR / 'templates'],
django.contrib.admin.AdminSite.site_header
app_list
至此,新手教程结束!