本文由Scarb发表于金甲虫的博客,转载请注明出处

django 官方教程笔记

看了django官方教程,并跟着教程做了一个polls投票app。
知识点不少,结合自己写的时候遇到的问题做一下笔记。

学习环境:

  • python 2.7.15
  • django 1.11.15
  • MySQL 5.7.23

1. 创建项目和app

首先确保环境安装和配置完毕。
查看django版本

1
$ python -m django --version

1.1 创建django项目

创建项目:进入合适的目录,运行

1
$ django-admin startproject mysite

项目初始目录:

1
2
3
4
5
6
7
mysite/
manage.py # 命令行工具,用来与django工程交互
mysite/ # python包,表示整个项目
__init__.py
settings.py # 项目配置文件
urls.py # 项目url分发配置
wsgi.py

运行项目服务:用runserver可以将服务运行起来,后面的参数可以指定运行的地址和端口号(默认为127.0.0.1:8000)
如果要能够被其他域的计算机访问,那么需要运行在0.0.0.0端口

1
$ python manage.py runserver [0.0.0.0:8000/0:8000/8000]

1.2 创建app

app指的是web应用,比如一个博客系统或者一个数据库系统。项目是一个多app和配置组成的网站。
一个项目可以有多个app,一个app可以被多个项目重复使用

创建app的命令

1
$ python manage.py startapp [appname]

app的目录结构

1
2
3
4
5
6
7
8
9
polls/
__init__.py
admin.py # 管理系统
apps.py # app信息
migrations/
__init__.py
models.py # 数据库模型
tests.py # 测试
views.py # 视图

新建urls.pyurl函数用正则匹配的方式匹配请求的url,并运行第二个参数进行跳转。

1
2
3
urlpatterns = [
url(r'^$', views.index, name='index'),
]

然后在主项目的urls.pyinclude()该文件,可以将所有^polls/请求转到polls.urls进行处理

1
2
3
4
urlpatterns = [
url(r'^polls/', include('polls.urls')),
url(r'^admin/', admin.site.urls),
]

2. 数据库操作

2.1 连接数据库

django需要用数据库连接引擎来与数据库进行连接,默认的数据库为SQLite

我采用的数据库为MySQL。首先要安装MySQL-python

1
$ pip install MySQL-python

然后在settings.py中修改引擎为MySQL并进行配置

1
2
3
4
5
6
7
8
9
10
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': '',
'USER': 'root',
'PASSWORD': '',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}

在MySQL中创建一个名为NAME的数据库,然后运行migrate命令,让django自动生成一些默认的数据库表格。

1
$ python manage.py migrate

2.2 创建数据模型

django自带一套ORM(Object Relational Mapping关系对象映射),在models.py中定义数据模型后执行命令可以生成SQL语句并执行。
这样就可以自动在数据库中生成表,并用该ORM进行数据库操作。

models.py定义完数据库模型后,将该app装入项目

1
2
3
4
5
6
7
8
9
# mysite/settings.pyINSTALLED_APPS = [
'polls.apps.PollsConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

然后运行指令生成类SQL语句,和执行SQL语句

1
2
3
$ python manage.py makemigrations [polls]           # 会在/migrations目录下生成python格式的类SQL语句,每次更新数据模型都要运行改命令重新生成
$ python manage.py sqlmigrate polls 0001 # 会在命令行显示0001 类SQL语句翻译成SQL语句的结果,但不会运行
$ python manage.py migrate # 执行该项目中所有app的修改过的类SQL语句,将改动同步到数据库

接下来用电影信息搜索项目定义的数据模型作为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@python_2_unicode_compatible                        # 支持python2
class Actor(models.Model):
class Meta: # 类型信息
verbose_name = "演员" # 类型显示名称
verbose_name_plural = "演员" # ......复数
ordering = ["id"] # 根据什么排序
c_name = models.CharField(verbose_name="中文名", max_length=40, null=True) # verbose_name: 给人看的名字;null: 是否可以为空,默认为False
e_name = models.CharField(verbose_name="英文名", max_length=40, null=True) # 字符域对应数据库中VARCHAR(40)
nation = models.CharField(verbose_name="国籍", max_length=20, null=True)

def __str__(self): # 配置查看别名
return self.c_name

@python_2_unicode_compatible
class Company(models.Model):
class Meta:
verbose_name = "公司"
verbose_name_plural = "公司"
ordering = ["id"]
c_name = models.CharField(verbose_name="中文名", max_length=50, null=True)
e_name = models.CharField(verbose_name="英文名", max_length=50, null=True)
nation = models.CharField(verbose_name="国籍", max_length=20, null=True)

def __str__(self):
return self.c_name

@python_2_unicode_compatible
class Movie(models.Model):
class Meta:
verbose_name = "电影"
verbose_name_plural = "电影"
ordering = ["-id"]
c_name = models.CharField(max_length=50, null=True)
e_name = models.CharField(max_length=60, null=True)
types = models.CharField(max_length=20, null=True)
time_length = models.IntegerField(verbose_name="片长", default=0, null=True)
release_time = models.IntegerField(verbose_name="上映时间", null=True)
standard = models.CharField(verbose_name="制式", max_length=20, null=True)
sum_boxoffice = models.IntegerField(verbose_name="总票房", default=0, null=True)
avg_price = models.IntegerField(verbose_name="平均票价", default=0, null=True)
avg_people = models.IntegerField(verbose_name="平均上座人数", default=0, null=True)
wom_index = models.FloatField(verbose_name="口碑评分", default=0, null=True)

actors = models.ManyToManyField(Actor, through="MovieActor") # 多对多外键,through关键字指定一个多对多模型。如果不指定会自动生成多对多模型
companies = models.ManyToManyField(Company, through="MovieCompany") # 这里因为多对多模型中需要存额外的数据,所以人工指定

def __str__(self):
return self.c_name

# 人工定义连接表,因为连接表储存额外的信息
@python_2_unicode_compatible
class MovieActor(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
actor = models.ForeignKey(Actor, on_delete=models.CASCADE)
actor_rank = models.IntegerField(verbose_name="演员在该片的排名", default=sys.maxint)
actor_role = models.CharField(verbose_name="演员在该片的角色", max_length=10, null=True)

def __str__(self):
return self.movie.c_name + ':' + self.actor.c_name

@python_2_unicode_compatible
class MovieCompany(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
company_rank = models.IntegerField(verbose_name="公司在该影片的排名", default=sys.maxint)
company_role = models.CharField(verbose_name="公司在该影片的角色", max_length=10, null=True)

def __str__(self):
return self.movie.c_name + ':' + self.company.c_name

3. view和模板

3.1 view和模板之间传参

简单的url传参

1
2
3
4
5
6
7
8
9
10
# polls/urls.py
urlpatterns = [
# ex: /polls/5/results/
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'), # ?P<question_id>表示匹配[0-9]+这个正则表达式的数据的名字
]

# polls/views.py
def results(request, question_id): # 获取url中匹配的question_id数据
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)

view函数传参给模板,使用render()

1
2
3
4
5
6
7
8
9
10
<!--polls/templates/polls/index.html-->
{% 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 %}
1
2
3
4
5
6
7
8
9
# polls/views.py
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) # 将数据传给模板并渲染
# 用render()简化模板渲染过程
# =
# template = loader.get_template('polls/index.html')
# return HttpResponse(template.render(context, request))

获取一个类型数据,用get_object_or_404()

1
2
3
4
5
6
7
8
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
# =
# try:
# question = Question.objects.get(pk=question_id)
# except Question.DoesNotExist:
# raise Http404("Question does not exist")

3.2 模板应用

用django模板摆脱写死路径的问题

1
2
3
4
5
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

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

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

url 'detail'会寻找polls.urls里面定义的url去匹配

1
2
3
# polls/urls.py
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),

第三行中polls:detail是在polls/urls.py中定义了命名空间

1
2
3
4
5
# polls/urls.py
app_name = 'polls'
urlpatterns = [
...
]

这样可以指定是哪个app的detail路径

4. 使用generic view简化代码

Generic views 提供了一些常用的模式,可以简化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# polls/views.py
# old
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):
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})

# new
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'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# polls/urls.py
# old
urlpatterns = [
# ex: /polls/
url(r'^$', views.index, name='index'),
# ex: /polls/5/
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
# ex: /polls/5/results/
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
# ex: /polls/5/vote/
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
# new
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

5. 测试

tests/py中编写自动测试代码,然后运行

1
$ python manage.py test polls

会对该app中的test进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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
:return:
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)

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.
"""
create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past 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.
"""
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: Past question.>']
)

def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
create_question(question_text="Past question 1.", days=-30)
create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question 2.>', '<Question: Past question 1.>']
)

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(qustion_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)

6. 管理静态资源

polls下创建static目录,在static下创建polls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
polls/
__init__.py
admin.py # 管理系统
apps.py # app信息
migrations/
__init__.py
static/
polls/
images/
background.git
style.css
templates/
polls/
index.html
models.py # 数据库模型
tests.py # 测试
views.py # 视图

django的STATICFILES_FINDERS设置包含了一个会自己从一系列目录中找静态文件夹的配置,其中一个查找器会找每个INSTALLED_APPSstatic目录。

然后在html和css文件中就可以用相对路径使用静态文件。

1
2
3
4
<!-- polls/templates/polls/index.html -->
{% load static %}

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}" />
1
2
3
4
<!-- polls/static/polls/style.css -->
body {
background: white url("images/background.gif") no-repeat right bottom;
}

7. 自定义管理界面

django自带一个admin界面,可以对数据进行增删改查

7.1 创建管理界面超级管理员

1
$ python manage.py createsuperuser

7.2 自定义管理界面

polls/admin.py中进行修改可以自定义管理员界面

如果要让数据模型可以在管理员界面中修改需要进行注册,也可以自定义数据模型,覆盖原来的模型进行注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# polls/admin.py
# Register your models here.
class ChoiceInline(admin.TabularInline): # 可以在Question中编辑的子Choice,TabularInline表示编辑信息域显示在一行中
model = Choice
extra = 3

class QuestionAdmin(admin.ModelAdmin): # 自定义Question管理模型
fieldsets = [ # 重新编排可以编辑的数据域显示排版
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date']}),
]
inlines = [ChoiceInline] # 进入Question编辑页可以编辑的属性,这里为ChoiceInline,extra为3表示默认会多出3新个Choice以供编辑

list_display = ('question_text', 'pub_date', 'was_published_recently') # 在Question列表时显示的属性
list_filter = ['pub_date'] # 在Question列表时显示的分类过滤器
search_fields = ['question_text'] # 在Question列表时可以搜索的字段

admin.site.register(Question, QuestionAdmin) # admin.site.register(Question),用QuestionAdmin覆盖

7.3 覆盖模板

复制django目录中的base_site.htmltemplates/admin/base_site.html做一些修改,会覆盖原来的模板。

1
2
3
4
5
6
7
8
9
10
<!-- templates/admin/base_site.html -->
{% extends "admin/base.html" %}

{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}

{% block nav-global %}{% endblock %}

8. 收尾

最后的项目目录格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
wsgi.py
polls/
__init__.py
admin.py
migrations/
__init__.py
0001_initial.py
models.py
static/
polls/
images/
background.gif
style.css
templates/
polls/
detail.html
index.html
results.html
tests.py
urls.py
views.py
templates/
admin/
base_site.html

9. 其他注意事项

9.1 部署在linux上,其他机器无法访问

0.0.0.0:8000启动服务

1
2
# settings.py
ALLOWED_HOSTS = ["*"] # 允许其他HOST连接