前言
在Python越来越火的当下,感觉作为一个计算机专业的学生还是需要掌握一些Python的编程技能。《Python编程:从入门到实践》是一本好书,我主要学习书里最后的项目部分,因为有C/C++的学习基础,所以对Python的学习我是想通过实践,从项目中学习。这段时间打算学习书上这个Web应用程序的项目,Web的相关知识是我没接触到的,第一次看到这个项目我并没有很大的兴趣去做,因为怕现在做了之后等到工作的时候其实完全不需要用到,也早忘了。但这段时间,体验了一些生活,经历了一些选择,想在接下来的时间里尽快找到自己以后想做的事,所以想开始做这个项目。没想到现在再做,已是有兴趣,有想探索的心。
此外,我们使用的python是3.6版本,Django是2.1.7,跟书本里的版本不一样,所以我对部分代码做了修改以适应我的版本(参考网上的资料)。
一、Django和我们的项目
Django是一个Web框架,是一套用于帮助开发交互式网站的工具(Django官网)。Django能够响应网页请求,能轻松读写数据库、管理用户。这个项目将借助Django,在Windows下实现一个“学习笔记”的Web应用程序,让用户能够记录感兴趣的主题,并在学习每个主题的过程中添加日志条目。“学习笔记”的主页对这个网站进行描述,并邀请用户注册或登陆。用户登陆后可创建新主题,添加新条目以及阅读既有的条目。
二、建立环境
1、要使用Django我们首先需要建立一个虚拟工作环境。
虚拟环境是系统的一个位置,我们可以在其中安装包并将其与其他Python的包隔离。这样做有益于将我们的Web应用程序部署到服务器。
首先我们为项目建立一个新目录,并在终端中切换到这个目录(可以在这个目录的空白处按住Shift然后点击右键,选择在Shell中打开目录)。然后如果是使用Python3,则可以用命令:python -m venv ll_env 创建虚拟环境。(这里是运行了python3的venv模块,用它来创建一个名为ll_env的虚拟环境);如果不是用Python3则需要先安装virtualenv包(pip install --user virtualenv),然后用virtualenv ll_env创建虚拟环境。
2、激活虚拟环境
每次编辑或者使用前都要使用激活命令:ll_env\Scripts\activate 激活后虚拟环境处于活动状态;要停止虚拟环境,可以使用deactivate命令或者关闭终端。【Django仅在虚拟环境处于活动状态时才可用】
3、安装Django
在虚拟环境ll_env目录下安装Django:pip install Django
4、在Django中创建项目
使用命令:django-admin startproject learning_log . 为Django创建一个名为learning_log的项目。注意命令最后有一个句点'.',有了这个句点可以使我们在部署应用程序时不用遭遇一些配置问题(具体原因我还没搞懂)
创建的新项目包含了4个文件:
- settings.py:指定Django如何与系统交互以及管理项目,可以在其中修改/添加一些设置;
- urls.py:告诉Django应创建哪些网页来响应浏览器请求;
- wsgi.py:帮助Django提供它创建的文件(Web Server Gateway Interface);
- manage.py:接受命令并将命令交给Django的相关部分去运行,可以用来管理使用数据库和运行服务器等任务;
5、创建数据库
执行指令:python manage.py migrate
修改数据库称为迁移数据库,首次执行migrate指令时将让Django确保数据库与项目的当前状态匹配,且将新建一个数据库。
6、查看项目
执行指令:python manage.py runserver
可以核实Django是否正确地创建了项目。该指令启动了一个服务器,可以查看系统中的项目,了解它们的工作情况。
指令的运行结果会返回三种信息:
- 确认是否正确创建了项目(System check identified no issues);
- 指出Django的版本以及当前使用的设置文件的名称;
- 指出项目的URL(URL为本地计算机localhost时,只处理当前系统发出的请求,而不允许其他人查看你正在开发的网页服务器)
三、创建应用程序
Django项目是由一系列应用程序组成的,它们协同工作,使项目成为一个整体。
创建应用程序的指令为:python manage.py startapp "appname"。该指令建立了应用程序所需的基础设施:创建了文件名为appname的文件夹,包含了model.py、admin.py和views.py。
- model.py:定义应用程序中管理的数据;
- admin.py:
- views.py:
1、定义模型
对于本次项目,每位用户都需要在学习笔记中创建很多主题,用户输入的每个条目都与特定的主题相关联并以文本形式显示。
在model.py中,首先应该导入models模块:from django.db import models,每个模型都要继承自models.Model。
我们创建的模型是在告诉Django如何处理应用程序中存储的数据,在代码层面,模型就是一个类:
class Topic(models.Model):
'''用户学习的主题'''
#设置文本属性
#text:由字符或文本组成的数据,需要存储少量的文本(名称、标题等)
#CharField表示设置字符串字段,需要预设要最大长度
text = models.CharField(max_length=200)
#设置时间戳
#DateTimeField用于记录日期和时间的数据
#有两个bool型参数:auto_now表示保存时自动设置该字段为当前时间(最后修改日期)
#auto_now_add表示当对象第一次被创建时自动设置该字段为当前日期(创建时间戳)
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
'''返回模型的字符串(text中的字符串)表示'''
return self.text
__str__(self)告诉Django默认应使用哪个属性来显示有关主题的信息,Django将调用该函数来显示模型的简单表示。
2、激活模型
要使用模型必须让Django将应用程序包含到项目中,我们通过修改项目目录下的settings.py添加应用程序:
在settings.py中有一个INSTALLED_APPS的列表,表示该Django项目是由哪些应用程序组成的,我们在其中添加我们自己的应用程序名称即可。
#INSTALLED_APPS是一个列表,告诉Django项目是由哪些应用程序组成的
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#我的应用程序
'learning_logs',
]
然后我们要修改数据库使其能够存储与模型Topic相关的信息,修改数据库的命令如下:
python manage.py makemigrations learning_logs(该命令让Django确定该如何修改数据库使其能够存储与我们定义的新模型相关联的数据;该命令会创建一个迁移文件,这个文件将在数据库中为模型Topic创建一个表)
python manage.py migrate(该命令实现真正的数据库迁移)
因此每当需要修改项目管理的数据时,都需要三个步骤:
- 修改models.py
- 对learning_logs(应用程序名)调用makemigrations
- 执行迁移指令migrate
3、Django管理网站
Django提供了管理网站(admin site)来方便处理模型。网站的管理员可以使用管理网站,但普通用户不能使用。
1.创建超级用户
超级用户是具备所有权限的用户。权限限制了用户的可执行操作。在Django中创建超级用户的命令为:
python manage.py createsuperuser
随后你将需要输入用户名、电子邮件地址(可以为空)和密码(需要输入两遍)
Django并不存储你输入的密码,而是存储从该密码派生出来的一个字符串——散列值。每当输入密码时Django计算其散列值并将结果与存储的散列值进行比较,如果两个结果相同则通过验证。这种做法确保了密码的安全。
2.向管理网站注册模型
Django自动在管理网站中添加了一些模型,如User和Group,对于我们自己创建的模型则需要手工注册。
在我们创建应用程序learning_logs时,Django在models.py所在的目录中自动创建了一个名为admin.py的文件,我们需要在该文件中添加我们的模型。
from django.contrib import admin #自动创建时已有的代码
from learning_logs.models import Topic#导入我们注册的模型
admin.site.register(Topic)
所以向管理网站注册模型的步骤为:
- 先从应用程序中导入要注册的模型;
- 再使用admin.site.register向管理网站注册我们的模型;
4、定义模型Entry
每个条目与特定的主题相关联,是多对一的关系。
class Entry(models.Model):
'''在学习的有关某个主题的知识'''
#下面的代码将每个条目(entry)关联到特定的主题。每个主题创建时都给它分配了一个键(ID)
#需要在两项数据之间建立联系时,Django使用与每项信息相关联的键
#ForeignKey:外键,是一个数据库术语,引用了数据库中的另一条记录
#django2.0之后,定义外键和一对一关系的时候需要加on_delete选项,此参数为了避免两个表里的数据不一致的问题
#一般情况下使用models.CASCADE:级联删除
topic = models.ForeignKey(Topic,on_delete=models.CASCADE)
#text是一个TextField实例,不需要限制长度,可创建一个可编辑文本框
text = models.TextField()
#date_added让我们能够按创建顺序呈现条目,并在每个条目旁边放置时间戳
date_added = models.DateTimeField(auto_now_add=True)
class Meta:
'''存储用于管理模型的额外信息'''
#设置verbose_name_plural属性,让Django在需要时使用Entries来表示多个条目
#如果没有这个类,Django将使用Entrys来表示多个条目
verbose_name_plural = 'entries'
def __str__(self):
'''返回模型的字符串表示'''
#如果条目包含的文本过长,则我们只显示前50个字符
if len(self.text) > 50:
return self.text[:50] + '...'
else:
return self.text
因此定义的步骤为:
- 将条目与主题相关联:topic = models.ForeignKey(Topic,on_delete=models.CASCADE)
- 创建可编辑文本框:text = models.TextField()
- 按时间戳顺序呈现条目:date_added = models.DateTimeField(auto_now_add=True)
- 内嵌Meta类,存储用于管理模型的额外信息;
- __str__(self):模型的表示
定义好Entry模型后进行迁移,迁移完成后向管理网站注册Entry,按照上面讲过的步骤。
由此可知,创建新模型的步骤为:
- 定义新模型
- 迁移数据库
- 向管理网站注册新模型
5、Django shell
输入一些数据后就可以通过交互式终端会话以编程方式查看这些数据了,可以用来测试项目、排除故障。
python manage.py shell(开启shell)
from learning_logs.models import Topic(导入模型Topic,来测试Topic)
Topic.objects.all()(获取模型Topic的所有实例,返回一个列表称为查询集)
每个主题对象有一个ID,有属性text表示主题的名称、date_added表示时间戳。
我们还可以查看与主题相关联的条目,Entry中定义了外键topic,由这个外键获取数据,可以使用模型的小写名称+下划线+单词“set”,如topic.entry_set.all()获取各条目。
四、创建网页
使用Django创建网页的过程通常分为三个阶段:定义URL、编写视图和编写模板。
- 定义URL:让Django知道如何将浏览器请求与网站URL匹配,以确定返回哪个网;
- 编写视图:每个URL都被映射到特定的视图,视图函数获取并处理网页所需的数据。视图函数通常调用一个模板,后者生成浏览器能够理解的网页。
1、映射URL
用户通过在浏览器中输入URL以及单击链接来请求网页,因此我们需要确定项目需要哪些URL,这将通过在urls.py文件中添加URL实现。
项目主文件夹learning_log中的文件urls.py:
#导入为项目和管理网站管理URL的函数和模块
from django.contrib import admin
from django.urls import path,include
#urlpatterns包含了项目中应用程序的URL
#admin.site.urls定义了可在管理网站中请求的所有URL
urlpatterns = [
path('admin/', admin.site.urls),
#下面中的实参namespace将learning_logs的URL同项目中其他URL区分开
path('',include('learning_logs.urls',namespace='learning_logs')),
]
path('',include('learning_logs.urls',namespace='learning_logs')) : 使项目包含了模块learning_logs.urls,然后我们需要在文件夹learning_logs中创建另一个urls.py文件,该urls.py中将URL映射到视图。
from django.urls import path
from . import views #在当前目录导入视图
#数据库中的视图:把多个表连接起来形成一个新的表
app_name = 'learning_logs'
#urlpatterns包含了应用程序learning_logs中请求的网页
urlpatterns = [
#path的参数:第一个参数是路由(一个匹配URL的准则),通常可为'';
#第二个参数指定了要调用的视图函数,视图函数接受请求中的信息,准备好生成网页所需的数据,再将这些数据发送给浏览器
#第三个参数是将这个URL模式的名称指定为index,这样每当需要提供到这个主页的链接时我们可以直接使用这个名称而不用编写URL
#主页
path('',views.index,name='index'),
]
两个urls.py的区别是,前者添加了应用程序的所有URL,后者指定应用程序各URL对应的视图。
2、编写视图
视图函数接受请求中的信息,准备好生成网页所需的数据,再将这些数据发送给浏览器——这通常使用定义网页的模板实现。
视图函数编写在views.py中,该文件是创建应用程序(python manage.py startapp)时自动生成的。
下面是本项目主页的视图。
from django.shortcuts import render #render渲染:根据信息创建一个网页
#request为请求对象
def index(request):
#render的两个实参:原始请求对象 以及 一个可用于创建网页的模板
#模板定义了网页的结构
return render(request,'learning_logs/index.html')
3、编写模板
视图对应的模板是html文件,定义了网页的结构,指定了网页是什么样的,每个模板对应一个网页。为了使项目结构规范,我们在应用程序的目录下新建一个目录templates,然后再新建一个目录名为learning_logs(与应用程序名称相同)的目录,将往后所有模板都保存于此。
编写模板用到的是HTML标签,也可能需要用到CSS或者javascript这些前端常用语言。
初始的index.html文件如下:
<p>Learning Log</p>
<p>你想在主页上显示的话</p>
<p></p>标签用于指示一个段落的开始和结束。
4、模板的继承
创建网站时几乎都有一些所有网页都将包含的元素,这种情况下我们可以编写一个包含通用元素的父模板,并让每个网页都继承这个模板,而不必在每个网页中重复定义这些通用元素。
父模板base.html:
所有页面都包含的元素只有顶端的标题,我们将在每个页面中包含这个模板。
将标题设置为到主页的链接。
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a>
</p>
{% block content %}{% endblock content %}
模板标签{% %}:一小段代码,生成要在网页中显示的信息。其中的url 'learning_logs:index'生成了一个URL,该URL与learning_logs/urls.py中定义的名为index的URL模式匹配,其中learning_logs表示一个命名空间,index表示该命名空间中一个独特的URL模式。
<a href="link_url">link text</a>:锚定义标签,用于定义链接。结合模板标签可以使得链接容易保持最新。link text表示链接显示出来的文本。
块标签{% block content %}{% endblock content %}:这个块的名称为centent,该代码中的块是一个占位符,其包含的内容由子模版决定。
子模版index.html:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Learning Log</p>
<p>你想在主页上显示的话</p>
{% endblock content %}
{% extends "learning_logs/base.html" %}:表示继承了父模板base.html;
这样看来使用模板继承简化了每个模板的代码编写,也便于管理项目。
5、显示所有主题的页面
1.URL模式
首先应该定义显示所有主题的页面的URL,用于指出网页对应的视图。
from django.urls import path
from . import views #在当前目录导入视图
#数据库中的视图:把多个表连接起来形成一个新的表
app_name = 'learning_logs'
#urlpatterns包含了应用程序learning_logs中请求的网页
urlpatterns = [
#path的参数:第一个参数是路由(一个匹配URL的准则),通常可为'';
#第二个参数指定了要调用的视图函数,视图函数接受请求中的信息,准备好生成网页所需的数据,再将这些数据发送给浏览器
#第三个参数是将这个URL模式的名称指定为index,这样每当需要提供到这个主页的链接时我们可以直接使用这个名称而不用编写URL
#主页
path('',views.index,name='index'),
#URL与该模式匹配的请求都将交给views.py中的函数topics()处理
path('topics/',views.topics,name='topics'),
]
相比于主页的path,我们在正则表达式中添加了“topics/”,Django检查请求的URL时这个模式与这样的URL匹配。与该模式匹配的URL都将交给视图函数topics处理(位于views.py中)
2.视图
def topics(request):
'''显示所有主题'''
#按属性date_added排序
topics = Topic.objects.order_by('date_added')
#将要发送给模板的上下文(字典型),其中的键是我们将在模板中用来访问数据的名称,
#而值是我们要发送给模板的数据
context = {'topics':topics}
return render(request,'learning_logs/topics.html',context)
上面的视图需要数据交互,具体步骤为:
- topics = Topic.objects.order_by('date_added'):查询数据库获取所有Topic对象,按时间戳排序。
- context = {'topics':topics}:定义将要发送给模板的上下文。其中的context表示将要发送给模板的上下文(字典型数据,键是模板中用来访问数据的名称,值是要发送给模板的数据)。
- return render(request,'learning_logs/topics.html',context):渲染网页。创建使用数据的网页时,除对象request和模板的路径之外,还需要context(上下文)。
3.模板
显示所有主题的页面 的模板 接受字典context。在专门存放模板的目录下新建文件topics.html:
<!--继承base.html-->
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<ul>
{% for topic in topics %}
<li>{{ topic }}</li>
{% empty %}
<li>No topics have been added yet.</li>
{% endfor %}
</ul>
{% endblock content%}
- <ul>标签:表示无序的项目列表
- {% for topic in topics %}:相当于for循环的模板标签,遍历了字典context中的列表topics,并以{% endfor %}指出循环的结尾
- <li>标签:表示一个项目列表项,位于<ul>标签内
- {{topic}}要在模板中打印变量需要将变量名用双花括号括起来
- {% empty %}:告诉Django在列表topics为空时该怎么办
然后修改父模板使其包含到显示所有主题的页面的链接
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics'%}">Topic</a>
</p>
{% block content %}{% endblock content %}
注意上面两个锚标签之间有连字符“-“,这使得两个链接显示在用一行且中间有不属于链接的连字符显示出来。
6、显示特定主题的页面
接下来创建一个专注于特定主题的页面,用于显示该主题的名称以及该主题的所有条目。同样的,我们需要定义一个新的URL模式,编写一个视图并创建一个模板。
1.URL模式
显示特定主题的页面的URL模式与前面的所有URL模式都稍有不同,因为这是用主题的ID属性来指出请求的哪个主题。
在learning_logs目录下的urls.py中添加下面URL:
#特定主题的详细页面:http://localhost:8000/topics/1/
#/(?P<topic_id>\d+)/与包含在两个斜杠内的整数匹配(如上,为1),并将这个整数存储在一个名为topic_id的实参中
#()括号捕获了URL中的值,?P<topic_id>将匹配的值存储到topic_id中;
#\d+与包含在两个斜杆内的任何数字都匹配,不管这个数字为多少位
#当发现URL与这个模式匹配时,Django将调用视图函数topic(),并将topic_id传给它
path('topics/(?P<topic_id>\d+)/',views.topic,name='topic'),
上面用了较为复杂的正则表达式,正则表达式我觉得是熟能生巧。
2.视图
def topic(request,topic_id):
'''显示单个主题及其所有的条目'''
#topic和entries被称为查询,向数据库查询特定的信息,可以先在Django shell中查询
topic = Topic.objects.get(id=topic_id)
#根据topic查询与其相关的所有条目(外键)
entries = topic.entry_set.order_by('-date_added')#减号表示降序,使得先显示最新的条目
context = {'topic':topic,'entries':entries}
return render(request,'learning_logs/topic.html',context)
视图函数的参数必须包含request,也可以包含其他参数,如上面函数包含了topic_id。该函数接受URL模式中正则表达式捕获的整数值并将其存储在topic_id中。
3.模板
<!--继承base.html-->
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics:{{ topic }}</p>
<p>Entries:</p>
<ul>
{% for entry in entries %}
<li>
<p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
<p>{{ entry.text|linebreaks }}
</li>
{% empty %}
<li>No topics have been added yet.</li>
{% endfor %}
</ul>
{% endblock content%}
注意到视图中我们在渲染网页时传递了context参数,其包含了topic和entries,所以在模板中我们可以以这两个参数为变量。
在Django模板中,竖线“|”表示模板过滤器,是对模板变量的值进行修改的函数,指示出后面是过滤器。
过滤器date: 'M d, Y H:i'表示以“月 日,年 时:分”的格式显示时间。
过滤器linebreaks将包含换行符的长条目转为浏览器能够理解的格式,以免显示为一个不间断的文本块。
然后我们将显示所有主题的页面中的每个主题都设置为链接:(修改topics.html)
--snip--
{% for topic in topics %}
<li>
<a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
</li>
{% empty %}
--snip--
这里的每个topic对应不同的链接,所以我们需要向模板标签传递参数topic指出具体的话题,url 'learning_logs:topic' 与 urls.py文件中定义的URL模式 path('topics/(?P<topic_id>\d+)/',views.topic,name='topic') 相对应,并且该模式要求提供实参,为此我们在模板标签url中添加了属性topic.id(对应URL模式中的参数topic_id)。而topic.id是每个话题存进数据库时拥有的序号。
五、部分小结
至此,我们学习了如何使用Django框架来创建Web应用程序。
首先我们需要指定项目规范,明确项目流程,要有条理有目的。开始项目前要搭建虚拟环境,在虚拟环境中安装Django,然后创建一个项目。一个项目是由一个或多个应用程序构成的,因此我们需要在项目中创建应用程序,然后定义表示应用程序数据的模型(model),包括了数据的类型以及显示方式等属性,另外对于某些数据我们还可能需要设置外键,使其与另一种数据相关联。
此外我们还简单学习了Django的数据库。我们对项目模型修改后都需要迁移数据库,这可以直接在shell上通过两条迁移命令完成。将数据存放在数据库之后我们可以通过查询数据库来获取数据,如Topic.objects.get(id=topic_id)是通过话题的ID来获取话题对象。
我们还学习了如何创建可访问管理网站的超级用户,并且每次创建新模型后我们需要向管理网站注册新模型。
较为重要的,我们学习了创建网页的具体步骤:创建URL模式->编写视图->编写模板。URL模式是网页地址到视图的映射,视图决定了网页的内容和功能,模板根据视图提供的内容决定网页的样子。其中包含了后端的知识和前端的知识,虽然只是皮毛,但已经让我们对Django编写Web程序的思路有一个大致的了解。
六、用户账户
Web应用程序的核心是让任何用户都能够注册账户并能够使用它。所以我们应该创建一些表单,让用户能够添加主题和条目以及编辑既有的条目。同时需要防范对基于表单的网页发起的常见攻击。
此外我们还需要实现一个用户身份验证系统,为此我们将创建一个注册页面供用户创建账户,并让有些页面只能供已登录的用户访问。
七、让用户能够输入数据
当前只有超级用户能够通过管理网站输入数据,我们将使用Django的表单创建工具来创建让用户能够输入数据的页面,这样用户就不用与管理网站交互。
1、添加新主题
创建基于表单的页面的方法几乎与前面创建网页一样:定义一个URL,编写一个视图函数并编写一个模板。主要差别在于需要导入包含表单的模块forms.py。
1.用于添加主题的表单
让用户输入并提交信息的页面都是表单。用户输入信息时我们要进行验证,确认提供的信息是正确的数据类型且不是恶意信息(如中断服务器的代码)。然后我们再对有效的信息进行处理并将其保存到数据库的合适地方。(这些工作很多都是由Django自动完成的)
在Django中创建表单的最简单方式是使用ModelForm,它根据我们之前定义的模型的信息自动创建表单。为此我们在与models.py相同的目录下新建一个forms.py的文件来编写表单。
from django import forms
from .models import Topic,Entry
class TopicForm(forms.ModelForm):
#Meta高数Django根据哪个模型创建表单以及表单中包含哪些字段
#我们根据Topic模型创建一个表单,该表单只包含字段text
class Meta:
model = Topic
fields = {'text'}
labels = {'text':''} #让Django不要为字段text生成标签
- 首先必须导入Django的forms模块,而且也要导入我们已经定义的模型Topic、Entry;
- 每个表单是一个继承了forms.ModelForm的类,最简单的ModelForm版本只包含一个内嵌的Meta类,它告诉Django根据哪个模型创建表单以及在表单中包含哪些字段。
- model=Topic:根据模型Topic创建一个表单;
- fields={'text'}:表单只包含字段text;
2.定义添加新主题的URL模式
在learning_logs/urls.py中
#用于添加新主题的网页
path('new_topic/',views.new_topic,name='new_topic')
3.编写视图函数new_topic()
from django.shortcuts import render #render渲染:根据信息创建一个网页
from django.http import HttpResponseRedirect,Http404
from django.urls import reverse
from .models import Topic
from .forms import TopicForm
--snip--
def new_topic(request):
'''添加新主题'''
if request.method != 'POST': #未提交数据则创建一个新表单
form = TopicForm() #创建一个新表单
else: #对POST提交的数据进行处理
form = TopicForm(request.POST) #用户输入的数据存储在POST中
if form.is_valid(): #核实用户是否填写了所有必不可少的字段且输入符合要求
form.save() #保存表单到数据库
return HttpResponseRedirect(reverse('learning_logs:topics'))
context = {'form':form} #将表单通过上下文字典发送给模板
return render(request,'learning_logs/new_topic.html',context)
GET和POST:
Web应用程序用到两种主要请求类型GET请求和POST请求:
从服务器读取数据的页面使用GET请求;在用户需要通过表单提交信息时,通常使用POST请求。
所以用户请求的是空表单时服务器发送的是GET请求,用户要求对填写好的表单进行处理时服务器发送的是POST请求。
此外,HttpResponseRedirect类用于用户提交主题后将用户重定向到网页Topics。函数reverse()的作用是根据指定的URL模式确定URL。
要将提交的信息保存到数据库必须先检查它们是否有效:form.is_valid()。该函数核实用户填写了所有必不可少的字段且输入的数据与要求的字段类型一致。
这里顺便说说对渲染和重定向的简单理解,网页的渲染我们用了render函数,这是在创建一个新的网页;而重定向我们使用了HttpResponseRedirect类,这是跳转到之前已经创建过的网页。
4.new_topic模板的编写
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Add a new topic</p>
<form action="{% url 'learning_logs:new_topic' %}" method='post' class="form">
{% csrf_token %}
{{ form.as_p }}
<button name="submit" >add topic</button>
</form>
{% endblock content %}
- <form>标签定义了一个HTML表单,action告诉服务器将提交的表单数据要发送到哪里。这里我们将表单发回给视图函数new_topic(),method让浏览器以POST的请求方式提交数据。
- csrf_token模板标签用于防止攻击者利用表单来获得对服务器未经授权的访问;CSRF(跨站点伪造请求)
- form.as_p中的修饰符as_p是让Django以段落的格式渲染所有表单元素。
- <button>标签定义了提交表单的按钮
5.链接到页面new_topic
在页面topics中添加一个到页面new_topic的链接。(topics.html文件中)
<a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
2、添加新条目
添加新条目的流程还是一样:定义URL模式、编写视图函数、编写模板,然后链接到添加新条目的网页。但再次之前我们需要在forms.py中添加一个新的条目的类,表示条目的表单。
1.用于添加新条目的表单
创建一个与模型Entry相关联的表单(forms.py):
from django import forms
from .models import Topic,Entry
--snip--
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = {'text'}
labels = {'text':''}
#widgets(小部件)是一个HTML表单元素,如单行文本框等
#forms.Textarea将文本区域设置为80列
widgets = {'text':forms.Textarea(attrs={'cols':80})}
2.URL模式new_entry
条目必须与特定的主题相关联:(urls.py)
path('new_entry/(?P<topic_id>\d+)/',views.new_entry,name='new_entry')
3.视图函数new_entry()
--snip--
from .forms import TopicForm,EntryForm
--snip--
def new_entry(request,topic_id):
'''在特定的主题中添加新条目'''
#从数据库根据主题的ID获取特定主题
topic = Topic.objects.get(id=topic_id)
if request.method != 'POST':
form = EntryForm() #未提交数据,创建一个空表单
else:
#根据POST提交的数据对数据进行处理
form = EnrtyForm(date=request.POST)
#表单内容是否有效
if form.is_valid():
new_entry = form.save(commit=False)
new_entry.topic = topic
new_entry.save()
#条目内容有效则创建新条目后回到主题页面
return HttpResponseRedirect(reverse('learning_logs:topic',args=[topic_id]))
#GET请求或者POST请求的内容无效 则根据表单内容创建新页面
context = {'topic':topic,'form':form}
return render(request,'learning_logs/new_entry.html',context)
- EntryForm(data=request.POST):创建了一个以request对象中的POST数据来填充的EntryForm实例;
- form.save(commit=False):Django创建一个新的条目对象并将其存储到new_entry中但不保存到数据库,然后设置条目对于的主题为当前的主题然后再将新条目保存到数据库——new_entry.save()
- new_entry.topic = topic:这里new_entry对象有topic属性,是因为new_entry是EntryForm的实例,EntryForm表单使用了Entry模型,而Entry模型含有topic这个外键属性。
- reverse('learning_logs:topic',args=[topic_id]):args是一个包含了要包含在URL中的所有实参。
4.模板new_entry
new_entry.html:
{% extends "learning_logs/base.html" %}
{% block content %}
<p><a href="{% url 'learning_logs:topic' topic.id %}">{{topic}}</a></p>
<p>Add a new entry:</p>
<!--页面的顶端显示主题让用户知道他是在哪个主题中添加条目,同时可以通过该链接返回到该主题的页面-->
<form action="{% url 'learning_logs:new_entry' topic.id %}" method='post' class="form">
{% csrf_token %}
{{ form.as_p }}
<button name='submit' >add entry</button>
</form>
{% endblock content %}
5.链接到页面new_entry
在显示特定主题的页面中添加到页面new_entry的链接:(topic.html)
<a href="{% url 'learning_logs:new_entry' topic_id %}">add new entry</a>
3、编辑条目
创建一个让用户能够编辑既有条目的页面。
1.定义URL模式edit_entry
--snip--
urlpatterns=[
--snip--
path('edit_entry/(?P<entry_id>\d+)/',views.edit_entry,name='edit_entry')
]
2.视图函数edit_entry()
页面edit_entry收到GET请求时将返回一个表单,让用户能够对条目进行编辑;收到POST请求时它将修改后的文本保存到数据库中(veiws.py):
--snip--
def edit_entry(request,entry_id):
'''编辑既有的条目'''
entry = Entry.objects.get(id=entry_id)#获取需要修改的条目对象以及相关的主题
topic = entry.topic
if request.method != 'POST':
#初次请求时创建一个表单并使用当前条目填充表单:显示条目的现有信息
form = EntryForm(instance=entry)
else:
form = EntryForm(instance=entry,data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('learning_logs/topic',args=[topic.id]))
context = {'entry':entry,'topic':topic,'form':form}
return render(request,'learning_logs/edit_entry.html',context)
form = EntryForm(instance=entry,data=request.POST):让Django根据既有条目对象创建一个表单实例,并根据request.POST中的相关数据对其进行修改。(这样则做到就当前条目的修改)
3.模板edit_entry
{% extends "learning_logs/base.html" %}
{% block content %}
<p><a href ="{% url 'learning_logs:topic' topic.id %}">{{topic}}</a></p>
<p>Edit entry:</p>
<form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name="submit" >save changes</button>
</form>
{% endblock content %}
依次显示了返回当前主题的链接、Edit entry、编辑条目的表单、提交表单的按钮。
在标签{% url %}中将条目ID作为一个实参让视图对象能够修改正确的条目对象。
4.链接到页面edit_entry
topic.html:
<a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entry</a>
将上面的链接放在每个条目的日期和文本后面(在循环中)。
4、创建用户账户
我们将建立一个用户注册和身份验证系统,让用户能够注册账户,进而可以登录账户和注销账户。我们将创建一个新的应用程序,其中包含与处理用户账户相关的所有功能。我们还需要修改Topic模型,让每个主题归属特定用户。
1.应用程序users
还记得创建应用程序的命令吗?
python manage.py startapp users
这样就新建了一个名为users的目录,其结构与应用程序learning_logs相同。
然后我们需要将应用程序users添加到setting.py中
--snip--
INSTALLED_APPS = [
--snip--
#我的应用程序
'learning_logs',
'users',
]
接着我们还需要修改项目根目录中的urls.py,使其包含我们将为应用程序users定义的URL:(urls.py)
#导入为项目和管理网站管理URL的函数和模块
from django.contrib import admin
from django.urls import path,include
#urlpatterns包含了项目中应用程序的URL
#admin.site.urls定义了可在管理网站中请求的所有URL
urlpatterns = [
path('admin/', admin.site.urls),
#users的URL
path('users/',include('users.urls',namespace='users')),
#下面中的实参namespace将learning_logs的URL同项目中其他URL区分开
path('',include('learning_logs.urls',namespace='learning_logs')),
]
2.登录页面
实现登录页面的功能时,我们使用的是Django提供的默认登录视图,因此URL模式会稍有不同。在learning_log/users/中新建一个名为urls.py的文件,然后添加下面代码:
'''为应用程序users定义URL模式'''
from django.urls import path
from django.contrib.auth.views import LoginView
from . import views
LoginView.template_name = 'users/login.html'
app_name = 'users'
urlpatterns =[
#登录页面
#导入视图login,使得登录页面的URL模式与'http://localhost:8000/users/login/'匹配
#'template_name':'users/login.html'告诉Django去哪里查找我们将编写的模板
#视图实参为login,使Django使用默认视图login,而不是views.login
path('login/',LoginView.as_view(),name='login'),
]
LoginView是Django的默认登录视图,因此我们需要指明该视图对应的模板:LoginView.template_name = 'users/login.html'
模板login.html,需要放在loearning_log/users/template/users目录下,同前面的learning_logs:
{% extends "learning_logs/base.html" %}
<!--继承base.html使得登陆页面与网站其他页面的外观相同-->
{% block content %}
<p>Log in to your account.</p>
{% if form.errors %}
<p> Your username and password didn't match. Please try again.</p>
{% endif %}
<form method="post" action="{% url 'users:login; %}">
{% csrf_token %}
{{ form.as_p }}
<button name="submit>log in</button>
<input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
</form>
{% endblock content %}
使用了模板标签的if来处理表单错误的情况。我们要让登录视图处理表单,因此将实参action设置为登录页面的URL。登录视图将一个表单发送给模板,在模板中我们显示这个表单并添加一个提交按钮。
此外我们的模板还包好了一个隐藏的表单元素‘next’,其中的实参value告诉Django在用户成功登录后将其重定向到主页。
然后我们在base.html中添加到登录页面的链接,让所有页面都包含它:(base.html)
--snip--
<a href="{% url 'learning_logs:topics' %}">Topics</a>-
{% if user.is_authenticated %}
Hello,{{ user.username }} .
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
user.is_authenticated表示用户是否已经登录,若登录则不显示登录链接。
这里可以发现使用了Django自带的视图时,我们对用户的属性操作可以直接使用user.属性,其中is_authenticated表示用户是否已经登录,username直接返回用户名。
3.注销页面
在users/urls.py中添加注销的URL模式:
#注销功能
path('logout/',views.logout_view,name="logout"),
自己编写视图函数logout_view():
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import logout,login,authenticate
from django.contrib.auth.forms import UserCreationForm
def logout_view(request):
'''注销用户'''
#直接调用django.contrib.auth中的Logout函数注销用户
logout(request)
return HttpResponseRedirect(reverse("learning_logs:index"))
这个视图很简单,直接调用了Django的logout函数,该函数将request对象作为实参然后重定向到主页。
然后我们在base.html链接到注销视图并使用if模板标签使得只有在用户已经登录的情况下才可以进行注销:
--snip--
{% if user.is_authenticated %}
Hello,{{ user.username }} .
<a href="{% url 'users:logout' %}">log out</a>
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
--snip--
4.注册页面
创建新用户注册页面,我们将使用Django提供的表单UserCreationForm,然后自己编写视图和模板。
首先还是先定义URL模式:
#注册页面
path('register/',views.register,name='register'),
然后编写视图函数register():
我们需要显示一个空的注册表单,并在用户提交填写好的注册表单时对其进行处理。如果注册成功,这个函数还需要让用户自动登录。
from django.contrib.auth import logout,login,authenticate
from django.contrib.auth.forms import UserCreationForm
def register(request):
'''注册新用户'''
if request.method != 'POST':
form = UserCreationForm()
else:
form = UserCreationForm(data=request.POST)
#检查用户输入的数据是否有效:是否包含非法字符,输入的两个密码是否相同
#以及用户有没有试图做恶意的事
if form.is_valid():
#save返回新创建的用户对象
new_user = form.save()
#用户注册时被要求输入密码两次,当表单是有效时两个密码相同,所以任取其中一个:password1
#用户名和密码无误时authenticate将返回一个通过了身份验证的用户对象
authenticated_user = authenticate(username=new_user.username,
password=request.POST['password1'])
#login登录函数,需要一个HttpRequest对象和一个用户对象
login(request,authenticated_user)
return HttpResponseRedirect(reverse('learning_logs:index'))
context = {'form':form}
return render(request,'users/register.html',context)
- UserCreationForm:默认的用户注册表单,该表单保存后其用户名和密码的散列值保存到数据库中,save函数同时也返回了一个新创建的用户对象。
- 根据要响应的是否是POST请求区分用户将要填写注册信息或者用户已经填好注册信息然后提交;
- 保存信息后让用户自动登录需要两个步骤:首先调用authenticate函数,实参为用户名username和用户密码request.POST['password1'](因为用户注册时被要求输入两次密码,所以是password1,也可以是password2因为两个密码是一样的)。若用户名和密码匹配则该函数会返回一个通过了身份验证的用户对象。然后我们调用函数login()并将request对象和通过了身份验证的用户对象作为实参传递给它让用户登录。最后将用户重定向到主页。
编写注册模板register.html:
{% extends "learning_logs/base.html" %}
{% block content %}
<!--提交的表单发送到视图函数中的register-->
<form method="post" action="{% url 'users:register' %}">
<!--防止攻击者利用表单来获得对服务器未经授权的访问-->
{% csrf_token %}
<!--as_p让Django以段落格式渲染所有表单元素-->
{{ form.as_p}}
<button name="submit">register</button>
<!--自动跳转-->
<input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
</form>
{% endblock content%}
链接到注册页面:(base.html)
--snip--
{% if user.is_authenticated %}
Hello, {{ user.username }}
<a href="{% url 'users:logout' %}">log out</a>
{% else %}
<a href="{% url 'users:register' %}">register</a>-
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
--snip--
至此,已登录的用户将看到问候语和注销链接,而未登录的用户将看到注册链接和登录链接。
5、让用户拥有自己的数据
我们将创建一个系统,确定各项数据所属的用户,再限制对页面的访问,让用户只能使用自己的数据。
1.使用@login_required限制访问
Django提供了装饰器@login_required限制用户的访问权限,对于某些页面只允许已登录的用户访问它们。装饰器是放在函数定义前面的指令,Python在函数运行前根据装饰器来修改函数代码的行为。
首先我们限制用户对topics页面的访问,每个主题都归特定用户所有,因此只允许已登录的用户请求topics页面,为此,在learning_logs/views.py中添加下面代码:
--snip--
from django.contrib.auth.decorators import login_required
--snip--
@login_required
def topics(request):
--snip--
需要先导入login_required函数,然后在要限制的视图函数前加上一行@login_required,这样的话Python在允许topics()的代码前会先允许login_required()的代码,而login_required()会检查用户是否已登录,仅当用户已登录时Django才会运行topics()。若用户未登录,我们需重定向到登录页面(通过修改learning_log/settings.py):
'''
项目learning_log的Django设置
'''
--snip--
#我的设置
LOGIN_URL = '/users/login/'
如果未登录的用户请求装饰器@login_requeired的饱和页面,Django将重定向到settings.py中的LOGIN_URL指定的URL。
2.全面限制用户对整个项目的访问
根据上面的方式给其他需要保护的页面添加装饰器。对于此项目,我们将不限制对主页、注册页面和注销页面的访问,并限制对其他所有页面的访问。所以learning_logs/views.py中对除index()外的每个视图都应用了装饰器:
--snip--
@login_required
def topic(request,topic_id):
--snip--
@login_required
def new_topic(request):
--snip--
...
3.将数据关联到用户
我们只需要将最高层的数据关联到用户,这样更低层的数据将自动关联到用户。例如在本项目中最高层数据是主题,所以有条目都与特定主题相关联,只要每个主题都归属于特定用户,我们就能确定数据库中每个条目的所有者。
为此我们需要修改模型Topic,在其中添加一个关联到用户的外键(A关联到B的外键——可以认为A是属于B的),然后对数据库进行迁移,最后对部分视图进行必要的修改,使其只显示与当前用户有关联的信息。
--snip--
from django.contrib.auth.models import User
class Topic(models.Model):
--snip--
owner = models.ForeignKey(User)
先导入User模型再设置外键。
在迁移数据库之前我们先确定当前有哪些用户(获取用户ID)。这我们可以在Django shell下执行命令查看:
python manage.py shell
>>>from django.contrib.auth.models import User
>>>User.objects.all()
[<User:ll_admin>,<User:username>,...]
>>>for user in User.objects.all():
... print(user.username,user.id)
...
ll_admin 1
username userid
>>>
其中ll_admin是超级用户。
知道了用户ID后我们再迁移数据库:
(命令)python manage.py makemigrations learning_logs
(回应)You are trying to add a non_nullable field 'owner' to topic without a default;
we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option: 1 (输入1)
(回应)Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> 1(输入1,超级用户ID)
(回应) Migrations for 'learning_logs':
0003_topic_owner.py:
- Add field owner to topic
执行makemigrations命令后Django指出我们试图给既有模型Topic添加一个必不可少的字段但这个字段没有默认值,所以Django
给我们提供了两种选择:现在提供默认值,或者退出后在models.py中添加默认值。我们选择前者并将所有既有主题都关联到超级用户ll_admin(用户ID:1),并非必须是超级用户,我们也可以使用已创建的任何用户。现在我们可以执行迁移了:
python manage.py migrate
如果你想验证迁移符合预期,可在shell会话中像下面这样做:
>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
... print(topic,topic.owner)
...
(返回主题以及所属的用户名)
>>>
这样顺便提一下重构数据库的做法:执行命令python manage.py flush,这将重建数据库结构,然后需要重新创建超级用户,且原来的数据全部丢失。
4.只允许用户访问自己的主题
在views.py中对topics()做修改:
--snip--
@login_required
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics':topics}
return render(request,'learning_logs/topics.html', context)
--snip--
request对象有一个user属性,存储了有关该用户的信息,我们使用filter只从数据库中获取owner属性为当前用户的Topic对象,然后按'date_added'的方式排序。
5.保护用户的主题
我们还没有限制对显示单个主题的页面的访问。以拥有所有主题的用户的身份登录,访问特定的主题,并复制该页面的URL,或将其中的ID记录下来。然后注销并用另一个用户的身份登录,再输入刚才复制的URL,这时依然能查看该主题中的条目。
为了修复这种问题,我们在视图函数topic()获取请求的条目前执行检查:(views.py)
from Django.http import HttpResponseRedirect, Http404
@login_required
def topic(request,topic_id):
'''显示单个主题及其所有的条目'''
topic = Topic.objects.get(id=topic_id)
#确认请求的主题属于当前用户
if topic.owner != request.user:
raise Http404
entries = topic.entry_set.order_by('-date_added')
context = {'topic':topic,'entries':entries}
return render(request,'learning_logs/topic.html',context)
服务器上没有请求的资源时标准的做法是返回404响应。,所以我们导入了Http404,并在用户请求它不能查看的主题时引发这个异常,而判断用户是否有权查看请求的主题,则看topic.owner(主题所属用户)和request.user(发出请求的用户)是否为同一个用户。
topic.entry_set是利用了外键获取话题对应的所有条目;-date_added使得先显示最新的条目。
6.保护页面edit_entry
这个也跟上面一样。(view.py)
--snip--
@login_required
def edit_entry(request,entry_id):
'''编辑既有条目'''
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if topic.owner != request.user:
raise Http404
if request.method != 'POST':
--snip--
同样是判断用户身份是否匹配。上面的entry.topic是在模型中定义的Entry的外键。
7.将新主题关联到当前用户
即创建新主题的时候必须指定其owner字段的值。由于我们可以通过request对象获得当前用户,所以可以直接将新主题关联到当前用户。(views.py)
@login_required
def new_topic(request):
'''添加新主题'''
if request.method != 'POST':
form = TopicForm()
else:
form = TopicForm(request.POST)
if form.is_valid():
new_topic = form.save(commit=False)
new_topic.owner = request.user #指定主题所属的用户
new_topic.save()
return HttpResponseRedirect(reberse('learning_logs:topics'))
context = {'form':form}
return render(request,'learning_logs/new_topic.html',context)
form.save()获取一个新主题对象后才可以设置这个新主题所属的用户。
6、用户系统小结
至此我们基本完成了一个小用户系统,虽然比较简单,但有基本的功能。我们赋予了每个用户特定的权限。且我们添加了注册、登录和注销功能。这一过程我们用了很多Django自带模块和函数。如Django提供的表单UserCreationForm让用户能够创建新账户。建立简单的用户身份验证和注册系统后我们通过使用装饰器@login_required禁止未登录的用户访问特定页面。我们还通过外键将数据关联到特定用户。同时为了让用户只能看到属于他的数据,我们使用方法filter()来获取属于用户自己的数据。
此外我们还知道了如何将请求的数据的所有者同当前登录的用户进行比较。
八、总结
通过上面的学习我觉得已经可以基本了解到如何用Django编写Web应用程序:定义URL模式、编写对应的视图、编写视图对应的模板。同时也可以学到一点对Django自带的数据库的基本操作以及Web应用程序与数据库的联系。但其中的小细节和一些知识点需要认真学习并理解后自己才真正会用。而且通过这一系列的学习我觉得我自己也对Web应用程序有了一定认识,但要达到熟悉的程度的话还需要多练多做项目,实践出真知。
顺便说说,其实这篇文章的大概前5%是我第一次学习Django时写的,那时没坚持写下去,只顾看书打代码。现在因为一些原因需要用到Django,所以利用这次的复习我把这篇文章完善。我觉得写文章还是挺好的,帮助自己记忆,并且写的时候我常会想要怎样写出自己的理解,这一思考过程往往会给我新的启发。