Python自动化开发学习19-Django
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python自动化开发学习19-Django相关的知识,希望对你有一定的参考价值。
接下来,我们把Django分为视图(View)、路由系统(URL)、ORM(Model)、模板(Templates )这4块进行学习。
视图
提交数据
上节课已经用过 request.POST.get()
获取提交的数据了,现在来看看有多选框的情况,多选的话应该要提交多个数据。先写一个有单选、多选、下拉列表的html:
<body>
<form action="/choice/" method="post">
<p>
性别:
<input type="radio" name="gender" id="man" value="male" />
<label for="man">男性</label>
<input type="radio" name="gender" id="female" value="female" />
<label for="female">女性</label>
</p>
<p>
爱好:
<input type="checkbox" id="football" name="favor" value="football" />
<label for="football">足球</label>
<input type="checkbox" id="basketball" name="favor" value="basketball" />
<label for="basketball">篮球</label>
<input type="checkbox" id="volleyball" name="favor" value="volleyball" />
<label for="volleyball">排球</label>
<input type="checkbox" id="baseball" name="favor" value="baseball" />
<label for="baseball">棒球</label>
</p>
<p>
<label for="city">城市:</label>
<select name="city" id="city">
<option value="BJ">北京</option>
<option value="SH">上海</option>
<option value="GJ">广州</option>
<option value="SZ">深圳</option>
</select>
</p>
<p>
<label for="skill">技能:</label>
<select name="skill" id="skill" multiple="multiple">
<option value="python">Python</option>
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="js">JavaScript</option>
</select>
</p>
<p>
上传:<input type="file" name="file" />
</p>
<input type="submit" value="提交" />
</form>
</body>
然后写一个处理函数,用get方法获取一下提交的值:
def choice(request):
if request.method == ‘GET‘:
return render(request, ‘choice.html‘)
elif request.method == ‘POST‘:
gender = request.POST.get(‘gender‘)
favor = request.POST.get(‘favor‘)
city = request.POST.get(‘city‘)
skill = request.POST.get(‘skill‘)
file = request.POST.get(‘file‘)
print(gender, favor, city, skill)
print(file, type(file))
return render(request, ‘choice.html‘)
# 处理POST和GET,还有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
else:
return redirect(‘/admin/‘)
所有的值都能获取到,但是对于多选的值也只能获取到一个,对于这种情况,我们需要用到另一个方法 request.POST.getlist()
,把上面的代码都替换成新的方法:
def choice(request):
if request.method == ‘GET‘:
return render(request, ‘choice.html‘)
elif request.method == ‘POST‘:
gender = request.POST.getlist(‘gender‘)
favor = request.POST.getlist(‘favor‘)
city = request.POST.getlist(‘city‘)
skill = request.POST.getlist(‘skill‘)
file = request.POST.getlist(‘file‘)
print(gender, favor, city, skill)
print(file, type(file))
return render(request, ‘choice.html‘)
# 除了POST和GET,客户端还可能有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
else:
return redirect(‘/admin/‘)
使用 getlist() 方法,返回的是一个列表,多个值的情况也能获取完整。当然,单选的话还是继续使用 get() 方法方便。
例子中还有个上传文件的input,这里只能获取到文件名,下面接着讲。
上传文件
普通的form接收不了文件,需要在form标签中要定义 enctype="multipart/form-data" 。然后把 type="file" 的input标签放在这个form里。所以得为上传文件单独写一个form。另外POST里并没有文件内容,文件内容再FILES里:
<form action="/upload/" method="post" enctype="multipart/form-data">
<p>
上传:<input type="file" name="file" />
</p>
<input type="submit" value="提交" />
</form>
然后再处理函数里先尝试获取一下接收到的内容:
def upload(request):
if request.method == ‘GET‘:
return render(request, ‘upload.html‘)
elif request.method == ‘POST‘:
obj = request.FILES.get(‘file‘)
print(obj, type(obj), obj.name)
return render(request, ‘upload.html‘)
# 处理POST和GET,还有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
else:
return redirect(‘/admin/‘)
上面打印了3个变量,obj打印出来是个文件名,但是实际是个class。打印type(obj)可以看到它的类型。obj.name才是真正的文件名。现在要把文件保存到本地,先建一个专门的文件夹upload,准备存放接收到的文件。要接收文件的内容,我们需要读取 obj.chunks() ,所以要接收文件上传参考下面的方法:
def upload(request):
if request.method == ‘GET‘:
return render(request, ‘upload.html‘)
elif request.method == ‘POST‘:
# 获取文件对象
obj = request.FILES.get(‘file‘)
# 本地创建一个文件用来接收上传的文件内容
with open(‘%s/%s‘ % (‘upload‘, obj.name), ‘wb‘) as file:
# 循环接收文件的内容,写入到本地的文件中去
for data in obj.chunks():
file.write(data)
return render(request, ‘upload.html‘)
# 处理POST和GET,还有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
else:
return redirect(‘/admin/‘)
CBV 和 FBV
到目前为止,所有的处理都是写在一个函数里的。Django还提供另外一个方式,我们也可以通过类来处理。
- FBV(function base views) 就是在视图里使用函数处理请求。
- CBV(class base views) 就是在视图里使用类处理请求。
创建处理请求的类
现在使用CBV把上面提交数据里的choice方法重新写一下:
from django.views import View
# CBV 的类需要继承上面的View
class Choice(View):
def get(self, request):
"""GET请求提交到这里"""
print(‘get‘)
return render(request, ‘choice.html‘)
def post(self, request):
"""POST请求提交到这里"""
gender = request.POST.getlist(‘gender‘)
favor = request.POST.getlist(‘favor‘)
city = request.POST.getlist(‘city‘)
skill = request.POST.getlist(‘skill‘)
file = request.POST.getlist(‘file‘)
print(gender, favor, city, skill)
print(file, type(file))
return render(request, ‘choice.html‘)
创建对应关系
urls.py 里的对应关系也要修改一下,中间写上类名,后面固定跟一个 .as_view()
,就是执行这个类的as_view()方法,具体如下:
from cmdb import views
urlpatterns = [
# path(‘choice/‘, views.choice),
path(‘choice/‘, views.Choice.as_view()),
]
各种类型的提交方法
先去看看继承的View类里有什么,在源码的base.py这个文件里。首先里面定义了一个公有属性:
http_method_names = [‘get‘, ‘post‘, ‘put‘, ‘patch‘, ‘delete‘, ‘head‘, ‘options‘, ‘trace‘]
所以处理post和get,还可以处理这么多的请求方法,用一起来也很简单,在类里照着别的一样定义一个同名方法就可以了。
处理执行前后自定义操作
继续看源码的View。这里可以跳过只看结论,调用了as_view()方法里面会再调用一个dispatch()方法。这个dispatch()方法里是通过映射获取我们的 request.method 即提交的方法来调用我们的处理方法的。dispatch()的源码如下:
def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn‘t exist,
# defer to the error handler. Also defer to the error handler if the
# request method isn‘t on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
结论就是,根据不同的请求类型提交到不同的处理方法,是用过dispatch()方法里通过映射来实现的。先执行dispatch()方法然后再调用对应的提交类型的处理方法。所以通过继承和重构dispatch()方法,可以在处理方法执行前和执行后自定义一些操作。如果需要的话就在我们的类里继承并重构,参考这里:
from django.views import View
# CBV 的类需要继承上面的View
class Choice(View):
def dispatch(self, request, *args, **kwargs):
print(‘before‘) # 处理前执行的操作
# 完全执行父类的这个方法
obj = super(Choice, self).dispatch(request, *args, **kwargs)
print(‘after‘) # 处理后执行的操作
# 这里一定要把处理结果返回
return obj
所有提交类型都不匹配的情况
还是上面的dispatch()方法,在最后return之前,也就是提交的类型没有匹配的处理方法的时候,默认调用执行 http_method_not_allowed() 方法,返回一个405页面。如果需要按照之前FBV中的else那样匹配其余所有类型的提交方法的话,那就在我们的类里重构这个方法,把之前FBV中if里面最后的else的逻辑补上:
def http_method_not_allowed(self, request, *args, **kwargs):
return redirect(‘/admin/‘)
路由系统,URL
模板言语循环字典
模板语言不属于路由系统,由于后面的例子会用到,先讲一点。
先看一下模板语言如何处理字典的,在 views.py 里添加一个字典,然后在页面里返回:
DICT = {
‘k1‘: ‘value1‘,
‘k2‘: ‘value2‘,
‘k3‘: ‘value3‘,
}
def dict(request):
return render(request, ‘dict.html‘, {‘dict‘: DICT})
下面的html里演示了用法:
<body>
<p>返回整个字典</p>
{{ dict }}
<p>返回的是key</p>
<ul>
{% for i in dict %}
<li>{{ i }}</li>
{% endfor %}
</ul>
<p>返回的是key</p>
<ul>
{% for key in dict.keys %}
<li>{{ key }}</li>
{% endfor %}
</ul>
<p>返回的是value</p>
<ul>
{% for value in dict.values %}
<li>{{ value }}</li>
{% endfor %}
</ul>
<p>返回key和value</p>
<ul>
{% for k,v in dict.items %}
<li>{{ k }}: {{ v }}</li>
{% endfor %}
</ul>
</body>
循环字典,和python里是差不多的,就是后面没有括号():
- 直接dict :循环的是key,不明确所以不推荐
- dict.keys :循环key
- dict.values :循环values
- dict.items :循环key和values
一条对应关系对应多个页面
现在我们已经可以用模板语言处理字典了,先来一个有点数据的字典:
USER_DICT = {
‘1‘: {‘name‘: ‘Adam‘, ‘age‘: 22, ‘dept‘: ‘IT‘},
‘2‘: {‘name‘: ‘Bob‘, ‘age‘: 32, ‘dept‘: ‘IT‘},
‘3‘: {‘name‘: ‘Carmen‘, ‘age‘: 30, ‘dept‘: ‘Sales‘},
‘4‘: {‘name‘: ‘David‘, ‘age‘: 40, ‘dept‘: ‘HR‘},
‘5‘: {‘name‘: ‘Edda‘, ‘age‘: 26, ‘dept‘: ‘HR‘},
}
def users(request):
return render(request, ‘users.html‘, {‘user_dict‘: USER_DICT})
上面的处理函数只是把内存的数据变的复杂了一点。另外这里的key用的是数字,我们可以把它当做是数据库获取到的数据的自增id。
基于get方法的实现
接下来重新写一个简单的html,页面里只显示字典的name的值,其他的值都不显示出来。换做提供一个a标签,可以通过点击a标签打开一个显示详细内容的页面:
<ul>
{% for k,v in user_dict.items %}
<li><a target=‘_blank‘ href=‘/detail/?nid={{ k }}‘>{{ v.name }}</a></li>
{% endfor %}
</ul>
去urls.py里添加完对应关系后,就可以打开这个页面。上面a标签里的连接指向的是一个detail的页面,并且提交的同时也提交一个nid值用于detail页面查找并显示出详细的内容。
显示详细的处理函数:
def detail(request):
# 用get方法获取到nid
nid = request.GET.get(‘nid‘)
# 通过nid获取到详细数据,最后给return返回
detail_info = USER_DICT[nid]
return render(request, ‘detail.html‘, {‘detail_info‘: detail_info})
还要写一个detail.html 的页面。上面处理函数已经通过get请求的nid去获取到具体得详细数据并返回了,这里直接把数据显示出来:
<body>
<h1>详细信息</h1>
<h3>用户名:{{ detail_info.name }}</h3>
<h3>年龄:{{ detail_info.age }}</h3>
<h3>部门:{{ detail_info.dept }}</h3>
</body>
上面的方法是在users页面以get形式提交到detail页面,然后detail页面里分析get的请求内容,获取到对应的详细信息,在页面里显示出来。
基于正则表达式的url来实现
还有另外一种实现方式。下面说的效果一样,但是这种方式更好。不传入参数,而是不同的urel ‘/detail-1/‘ 这样,这个就需要用到正则表达式。先把urls.py里的对应关系改成正则的形式:
from django.urls import path, re_path
from cmdb import views
urlpatterns = [
# path(‘detail/‘, views.detail),
re_path(‘^detail-(\d+).html‘, views.detail),
]
因为这里要匹配正则了,之前的path不再适用,这里要导入re_path来匹配正则。url的正则表达式都以^开头,从头开始匹配
users.html显示不用改,但是要修改一个a标签里的内容,现在url后面不需要用get方式提交任何数据,但是请求的url本事是会变化的:
<ul>
{% for k,v in user_dict.items %}
<!--
<li><a target=‘_blank‘ href=‘/detail/?nid={{ k }}‘>{{ v.name }}</a></li>
-->
<li><a target=‘_blank‘ href=‘/detail-{{ k }}.html‘>{{ v.name }}</a></li>
{% endfor %}
</ul>
最后是显示详细信息的页面,detail.html不需要任何变动,只要views.py里处理函数的return不变就好,但是获取数据的方式变了:
def detail(request, nid):
# print(nid)
detail_info = USER_DICT[nid]
return render(request, ‘detail.html‘, {‘detail_info‘: detail_info})
这里的处理函数多传入了一下参数nid。名字不重要,但是这个值是正确分组匹配的结果。正则是这个 ‘detail-(\d+).html‘
,里面括号中的 \d+
的内容就传给了后面的第一个参数。也可以传多个参数(用多个括号),但是数量要一致(处理函数开头的形式参数),否则打开的页面会报错。
为什么这种更好:路由关系是一个动态的关系,一对多,一类url对应一个函数或类。
捕获参数
捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用。当然,这种引用既可以是在正则表达式内部,也可以是在正则表达式外部。捕获组有两种形式:
- 普通捕获组:
(Expression)
- 命名捕获组:
(?P<name>Expression)
,这个是python中的语法,其他语言了有的有,但是可能有点小差别,比如没有这个P,比如不用尖括号换成引号
前面的就是普通捕获组的例子。如果你的正则有多个子表达式,比如:‘detail-(\d+)-(\d+).html’ 。那么定义函数的时候必须注意参数的位置(名字为所谓)。这里可以使用命名捕获组来写正则表达式,正则本身没有任何变化,只是在子表达式前面加上加上一个命名。views.py里的对应关系可以这么写from django.urls import path, re_path from cmdb import views urlpatterns = [ # re_path(‘^detail2-(\d+)-(\d+).html‘, views.detail2), re_path(‘^detail2-(?P<nid>\d?)-(?P<uid>\d?).html‘, views.detail2), ]
上面被注释的是普通捕获组的写法,下面的是命名捕获组的写法。使用了命名捕获组后,我们的处理函数的参数名字就是正则中的命名,但是位置无所谓了。下面的处理函数直接在页面输出2个参数的值,就不写页面了:
def detail2(request, uid, nid): return HttpResponse(‘%s-%s‘ % (nid, uid))
可以用这样的url测试
http://127.0.0.1:8000/detail2-1-3.html
。用 path() 方法实现捕获参数
课上讲的是旧版本,现在Django已经2.0了,url()方法被path()方法替代,用法也有区别。
re_path() 可以看做是2.0里向下兼容的一个方法,也就是旧的1.0的 url() 方法。在2.0里用 path() 方法也可以实现捕获组的对应关系。使用 path() 方法需要注意:- 要捕获一段url中的值,需要使用尖括号,而不是之前的圆括号;
- 可以转换捕获到的值为指定类型,比如int。默认情况下,捕获到的结果保存为字符串类型,不包含 ‘/‘ 这个特殊字符;
- 匹配模式的最开头不需要添加 ‘/‘ ,因为默认情况下,每个url都带一个最前面的 ‘/‘ ,既然大家都有的部分,就不用浪费时间特别写一个了。
那么现在 urls.py 里的对应关系可以这么写:
# re_path(‘detail2-(\d+)-(\d+).html‘, views.detail2),
# re_path(‘^detail2-(?P<nid>\d+)-(?P<uid>\d+).html‘, views.detail2),
path(‘detail2-<int:nid>-<int:uid>.html‘, views.detail2),
上面的例子,就是捕获一个0或正整数,并且返回一个int类型,再用冒号把命名也完成了。除了int,还有下面这些。
默认情况下,Django内置下面的路径转换器:
- str:匹配任何非空字符串,但不含斜杠/,如果你没有专门指定转换器,那么这个是默认使用的;
- int:匹配0和正整数,返回一个int类型;
- slug:可理解为注释、后缀、附属等概念,是url拖在最后的一部分解释性字符。该转换器匹配任何ASCII字符以及连接符和下划线,比如’ building-your-1st-django-site‘ ;
- uuid:匹配一个uuid格式的对象。为了防止冲突,规定必须使用破折号,所有字母必须小写,例如’075194d3-6885-417e-a8a8-6c931e272f00‘ 。返回一个UUID对象;
- path:匹配任何非空字符串,重点是可以包含路径分隔符’/‘。这个转换器可以帮助你匹配整个url而不是一段一段的url字符串。
小结
上面各种实现的方法由浅入深,并且一个比一个好,推荐用最后面的实现方式:
- 基于正则的url比使用get方式获取参数的好
- 命名捕获组比普通捕获组好
- 推荐还是用最后的 path() 方法来实现,如果是1.x的版本,那么就是推荐基于正则的命名捕获组的方法。
另外,在定义函数的时候也可以写成这种万能的模式: def detail2(request, *args, **kwargs):
,这样的话,要使用 args[0]
(普通捕获组)或 kwargs[‘nid‘]
(命名捕获组)来取值。
路由对应的名称
还可以对url的关系进行命名,完成命名后,以后可以通过这个名字获取到对应的url。好处是html里使用url的名称而不是写死,那么urls.py里修改了url,不用到html里修改了。
命名是在写对应关系的时候,加上一个参数 name=[url的名称]
path(‘myurl/‘, views.myurl, name=‘myurl‘),
然后再网页中使用的时候用 {% url [url的名称] %}
替代原来写死的url。
<form action="{% url ‘myurl‘ %}" method="get">
<input type="text" name="name" placeholder="NAME" />
<input type="submit" value="提交" />
</form>
打开页面后可以按F12进入开发这模式查看页面中的url已经是转化后的具体的url了,就是我们在 urls.py 的对应关系里写的内容。
带捕获参数的情况:
url如果带有捕获参数,比如要捕获2个参数:
path(‘jump-<int:nid>-<int:uid>/‘, views.jump, name=‘jump‘),
那么首先处理函数你必须写上这2个参数(不写会报错),也可以用通用的 *args,**kwargs
的形式:
def jump(request, nid, uid):
# return HttpResponse(‘%s-%s‘ % (nid, uid))
return render(request, ‘jump.html‘)
然后页面上做使用模板语言的时候,引用jump作为动态的url,但是后面也要跟上需要捕获的值:
<a href="{% url ‘jump‘ 3 4 %}">跳转</a>
<a href="{% url ‘jump‘ uid=3 nid=4 %}">跳转</a>
上面两种写法都可以,上面的是按照位置传参。下面故意先写了uid,会按照名字来传参。
访问测试页面随便输一个url,比如 “http://127.0.0.1:8000/jump-1-2/” 。然后页面里的两个a连接生成的是各自新的url。新url整体不变,但是捕获参数的值是在url名字后面的参数决定的。
引用当前页面的url:引申一下,{{ request.path_info }} 就是当前页面的url,如要要用的话,request默认就是处理函数里要返回的变量,所以页面里直接用就可以了。
在处理函数中根据名称获取url:
先 from django.urls import reverse
使用这个reverse也能获取到url。直接看获取带捕获的url的方法:
def jump(request, nid, uid):
from django.urls import reverse
r1 = reverse(‘jump‘, args=(3, 4))
r2 = reverse(‘jump‘, kwargs={‘uid‘:3, ‘nid‘:4})
print(r1, r2, request.path_info)
# return HttpResponse(‘%s-%s‘ % (nid, uid))
return render(request, ‘jump.html‘)
也是2种形式都可以。
路由分发
之前所有的对应关系都是写在app目录外的urls文件里的。当我们的项目有多个app的时候,所有的页面都写在一起也不好。应该是每个app各自管理自己的那部分url。这就需要路由分发。
首先我们app目录外的公共url文件中导入一个include方法,声明好app的url文件的位置:
from django.urls import include
urlpatterns = [
path(‘admin/‘, admin.site.urls),
path(‘cmdb/‘, include("cmdb.urls"))
]
上面的例子中保留了原有的admin的对应关系,如果有别的公共页面还是可以在这里写的。然后是声明了cmdb这个app的url文件的位置。现在去 cmdb 目录下创建一个 urls.py 的文件。在这里写这个app的对应关系:
from django.urls import path
from cmdb import views
urlpatterns = [
path(‘login/‘, views.login),
]
上面例子中的url是在cmdb这个app中的,所以访问的时候要带先加上app的名字,应该是 http://127.0.0.1:8000/cmdb/login/
。这样app里可以有一个login页面,外面也可以有一个login页面。如果还有别的app,那个app也可以有login页面。名字前面会加上自己的app的名字,命名空间不冲突。
梳理一下逻辑:当一个url请求过来之后,先到达项目目录下的urls文件进行匹配。这里如果匹配到了项目名,比如cmdb。那么会再把它分发给之后的(就是app目录里的)urls文件继续处理。所以配置过分发后,首先还是到项目目录下的urls文件里进行匹配,然后再用这里的规则分发出去。
ORM
连接sqlite数据库
默认使用的是sqlite3作为数据库,使用数据库需要一下步骤
一、创建你的数据库表结构
app目录下的models.py文件就是用来写你的表结构的:
from django.db import models
# Create your models here.
# 文件默认会有上面的2行,下面是我们添加的内容
class UserInfo(models.Model):
# 默认会自动创建自增id并作为主键
username = models.CharField(max_length=32) # 字符串,长度32
password = models.CharField(max_length=64)
上面的类等到去数据库创建表的时候,表名是 “cmdb_userinfo” ,也就是 [app名]_[类名] 。
二、设置settings.py文件
在 INSTALLED_APPS 注册你的app,把你的app追加到这个列表里:
INSTALLED_APPS = [
‘django.contrib.admin‘,
‘django.contrib.auth‘,
‘django.contrib.contenttypes‘,
‘django.contrib.sessions‘,
‘django.contrib.messages‘,
‘django.contrib.staticfiles‘,
‘cmdb‘,
]
配置你的数据库连接,默认已经配置好了一个sqlite3,所以不需要修改:
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
‘default‘: {
‘ENGINE‘: ‘django.db.backends.sqlite3‘,
‘NAME‘: os.path.join(BASE_DIR, ‘db.sqlite3‘),
}
}
三、去终端执行2条命令
python manage.py makemigrations
python manage.py migrate
第一条命令会在 app 目录下的 migrations 目录下创建一个文件(0001_initial.py),记录我们对 models.py 所做的变动。
第二条命令是真正去操作数据库了,除了创建我们自己写的表以外,还创建了很多 django 自己的表。
上面两条命令都是作用于全局的,如果要限定作用于只在某个app,可以在最后加上app的名称:
python manage.py makemigrations cmbd
python manage.py migrate cmdb
关于SQLite:
SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。
Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。
连接mysql数据库
步骤同上,理论上只要修改一下 settings.py 里的 DATABASES 的值就好了。但是还有一些别的坑。这里主要演示一下怎么连上mysql数据库,连上之后,后面的操作还是在SQLite下来做。
DATABASES 设置的上面就是官方的帮助文档的连接,或者直接参考下面的进行设置就好了:
DATABASES = {
‘default‘: {
‘ENGINE‘: ‘django.db.backends.mysql‘,
‘NAME‘: ‘cmdb‘,
‘USER‘: ‘admin‘,
‘PASSWORD‘: ‘admin123‘,
‘HOST‘: ‘192.168.246.12‘,
‘port‘: ‘3306‘,
}
}
然后是坑,首先用户我们得自己去数据库上创建好,注意如果不是本地的数据库,需要能够远程访问。库也要自己创建好,创建库:
CREATE DATABASE cmdb CHARSET "utf8";
然后可以试着执行终端的2条命令,但是可能会报错:
import MySQLdb as Database
ModuleNotFoundError: No module named ‘MySQLdb‘
意思是找不到这个库,在python3里mysql我们用 pymysql 这个库。不过装好了pymysql还是会提示找不到库,因为django就是耿直的要找MySQLdb。解决办法是编辑项目名同名目录下的 __init__.py 文件,在这里导入我们的pymysql并且会把它的名字就当做是 MySQLdb :
import pymysql
pymysql.install_as_MySQLdb()
ORM操作
添加
添加数据有2种方法,推荐用第一种。下面是写在app目录的views.py里的处理函数:
from cmdb import models
def add_user(request):
models.UserInfo.objects.create(
username=‘root‘,
password=‘123456‘
)
# 另外一个方法,先创建一个实例,然后调用它的save()方法
obj = models.UserInfo(
username=‘admin‘,
password=‘admin123‘
)
obj.save()
# 方法一的变种,把字典直接作为参数传入
dic = {‘username‘: ‘user‘, ‘password‘: ‘user123‘}
models.UserInfo.objects.create(**dic)
return HttpResponse("add user")
首先我们要操作某个表,就要先把这个创建这个表的那个类导入进来,例子的第一行。上面一个创建了3条数据了。
查询
用all方法查询到的数据,首先是放在一个列表里,列表的元素是一个一个的对象,每一个对象就是一条记录。
筛选的方法有filter,这个返回的也是个列表,因为可能返回多条。
def show_user(request):
res = models.UserInfo.objects.all()
for row in res:
print(row.id, row.username, row.password)
users = models.UserInfo.objects.filter(username=‘root‘)
for row in users:
print(row.id, row.username, row.password)
return HttpResponse("show user")
filter()里面还可以传入多个参数,就是多个条件,他们之间的关系是逻辑与(and)。
还有一个first()方法,取出第一个值,这样返回就不是列表而直接就是对象了。可以直接用,也可以用在filter()方法后面。all()方法后面也是可以用的,不过没意义:
models.UserInfo.objects.first()
models.UserInfo.objects.filter(username=‘root‘, password=‘123456‘).first()
# 上面就是一个验证用户登录的逻辑了,返回结果是None或者是找到的第一个对象
另外还有一个get方法也可以获取到一条数据,但是如果数据不存在不是返回空而是会报错。如果要用那就得写个try:
models.UserInfo.objects.get(id=10)
QuerySet 对象,分别打印出查询结果和这个对象的query属性:
res = models.UserInfo.objects.all()
print(res) # 结果在下面
# <QuerySet [<UserInfo: UserInfo object (1)>, <UserInfo: UserInfo object (2)>, <UserInfo: UserInfo object (3)>]>
print(res.query) # 结果写下面
# SELECT "cmdb_userinfo"."id", "cmdb_userinfo"."username", "cmdb_userinfo"."password" FROM "cmdb_userinfo"
可以看到这是一个 QuerySet 对象,不是一个普通的列表。这里要引出它的一个属性 query 。
这个对象有一个query属性,该属性的内容是获取这个对象时对应的SQL语句。
删除
删除前首先要先做查找,调用查找结果的delete()方法,就完成了删除:
models.UserInfo.objects.all().delete
models.UserInfo.objects.filter(id=2).delete
修改
修改也是在查找的基础上,调用update()方法来完成的:
models.UserInfo.objects.filter(id=1).update(password=‘root123‘)
示例
先来写一个登录页面 index.html 用来提交用户名和密码进行验证:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
label{
width: 80px;
text-align: right;
display: inline-block;
}
</style>
</head>
<body>
<form action="/login/" method="post">
<p>
<label for="usernmae">用户名:</label>
<input id="usernmae" name="user" type="text" />
</p>
<p>
<label for="password">密码:</label>
<input id="password" name="pwd" type="password" />
<input type="submit" value="提交">
<span style="color: red">{{ error_msg }}</span>
</p>
</form>
</body>
</html>
然后是index的处理函数,用户验证失败报错误信息,验证成功跳转的下一个页面:
def login(request):
if request.method == ‘GET‘:
return render(request, ‘login.html‘)
elif request.method == ‘POST‘:
user = request.POST.get(‘user‘)
pwd = request.POST.get(‘pwd‘)
obj = models.UserInfo.objects.filter(username=user, password=pwd).first()
if obj:
# 先跳转到admin,可以测一下,之后再写index页面
return redirect(‘/admin/‘)
# return redirect(‘/userlist/‘)
else:
return render(request,
‘login.html‘,
{‘error_msg‘: ‘用户名或密码错误‘})
else:
return HttpResponse("不支持您的请求方式")
测试跳转没问题之后,就可以把上面的跳转从admin页面换到userlist页面,然后就来写这个userlist页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h2>添加用户</h2>
<form method="post" action="/userlist/">
<input type="text" name="user" placeholder="username">
<input type="text" name="pwd" placeholder="password">
<input type="submit" value="添加">
</form>
</div>
<div>
<h2>用户列表</h2>
<table border="1">
<thead>
<tr>
<th>id</th>
<th>username</th>
<th>password</th>
<th>按钮</th>
</tr>
</thead>
<tbody>
{% for row in users %}
<tr>
<td>{{ row.id }}</td>
<td>{{ row.username }}</td>
<td>{{ row.password }}</td>
<td>
<a href="/userdel-{{ row.id }}/">删除</a>
<span>|</span>
<a href="/useredit-{{ row.id }}/">编辑</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
首先页面的中要实现数据库查询的功能,就是显示用户列表,通过GET方法来实现。
另外还有一个增加数据的功能,页面上面的添加用户,请求是通过POST方法提交过来,完成的数据添加。POST方法可以有2中return的方式,直接的方式就是和GET方法一样。或者也可以用例子里使用的方法,就是再提交一次GET请求:
def user_list(request):
if request.method == ‘GET‘:
users = models.UserInfo.objects.all()
return render(request, ‘userlist.html‘, {‘users‘: users})
elif request.method == ‘POST‘:
username = request.POST.get(‘user‘)
password = request.POST.get(‘pwd‘)
models.UserInfo.objects.create(username=username, password=password)
# 这里可以和get返回的一样
# return render(request, ‘userlist.html‘, {‘users‘: users})
# 这里选择用redirect()方法返回,就是再调用一次get方法返回页面
return redirect(‘/userlist/‘)
删除功能不需要写页面,只需要一个处理函数:
def user_del(request, nid):
models.UserInfo.objects.filter(id=nid).delete()
return redirect(‘/userlist/‘)
最后还有一个编辑功能,现在只能用写一个新的页面然后再那个页面里提交。这样实现起来比较简单,主要就是通过这个示例把数据库的最删改查都用一遍:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h2>编辑用户</h2>
<form method="post" action="/useredit-{{ obj.id }}/">
<input type="text" name="user" value="{{ obj.username }}">
<input type="text" name="pwd" value="{{ obj.password }}">
<input type="submit" value="提交">
</form>
</div>
</body>
</html>
这个页面对应的处理函数是如下:
def user_edit(request, nid):
if request.method == ‘GET‘:
obj = models.UserInfo.objects.filter(id=nid).first()
return render(request, ‘useredit.html‘, {‘obj‘: obj})
elif request.method == ‘POST‘:
username = request.POST.get(‘user‘)
password = request.POST.get(‘pwd‘)
models.UserInfo.objects.filter(id=nid).update(
username=username, password=password)
return redirect(‘/userlist/‘)
在userlist页面点击编辑按钮后,GET请求跳转到useredit页面。在编辑页面提交后向useredit发送一个POST请求修改数据,然后返回userlist页面,完成一次编辑。
上面一个有4个处理函数,其中3个有html页面,urls.py的对应关系如下:
path(‘login/‘, views.login),
path(‘userlist/‘, views.user_list),
path(‘userdel-<int:nid>/‘, views.user_del),
path(‘useredit-<int:nid>/‘, views.user_edit),
ORM表结构
修改表结构
修改过表结构之后,需要再执行一下下面的2行命令,把新的表结构应用到数据库。
python manage.py makemigrations
python manage.py migrate
修改数据长度、删除一列,这类情况没什么特别的问题。
增加一列,默认情况下字段值不允许为空,此时会有提示。要么全部都设为空,要么你给个默认值,全部都设为默认值。另外还可以直接定义到表结构中:
class UserInfo(models.Model):
# 默认会自动创建自增id并作为主键
username = models.CharField(max_length=32) # 字符串,长度32
password = models.CharField(max_length=64)
email = models.CharField(max_length=64, null=True) # 设置为允许空值
字段类型
基本的字段类型有:字符串、数字、时间、二进制。
Django的ORM提供了非常多的字段类型,比如:EmailField、URLField、GenericIPAddressField。这些其实都是字符串类型而已,并且确实对我们没任何用(并不能帮我们做数据验证)。这些字段类型的只有在用Django的后台管理页面 admin 的时候才能发挥数据验证的效果。只有通过admin提交数据的时候才会验证你的数据格式是否正确。接下来就先讲怎么登进去
自增id,之前定义表结构的时候,省略了主键,让Django帮我创建了自增id。也可以自己定义主键和自增id:
class UserGroup(models.Model):
uid = models.AutoField(primary_key=True) # 数据类型是自增,并且设为主键
group = models.CharField(max_length=32)
登录Admin
admin具体要到后面讲,这里先让我们登录进去
- 创建超级管理员,输入命令后会提示你输入用户名、邮箱(可以直接回车)、密码(似乎有长度和复杂度的要求):
python manage.py createsuperuser
- 配置后台管理url,就是admin页面的对应关系,默认urls.py里面已经配好了:
path(‘admin/‘, admin.site.urls),
- 注册和配置 admin 后台管理页面,把你的表注册号之后,就可以通过admin进行管理了:
from django.contrib import admin # Register your models here. # 上面是文件默认就有的内容 from cmdb import models admin.site.register(models.UserInfo)
参数
null :数据库中字段是否可以为空
default :数据库中字段的默认值
db_column :数据库中字段的列名。默认列明就是我们的变量名,可以通过这个参数设置成不一样的
class UserInfo(models.Model):
username = models.CharField(max_length=32) # 字段名就是变量名 username
password = models.CharField(max_length=64, db_column=‘pwd‘) # 数据库中的字段名会是 pwd
db_index :是否建立索引
unique :是否可以建立索引
unique_for_date :只对字段中【日期】部分建立唯一索引
unique_for_month :只对字段中【月】部分建立唯一索引
unique_for_year :只对字段中【年】部分建立唯一索引
auto_now :自动生成一个当前时间,数据更新时(包括创建)
auto_now_add :自动生成一个当前时间,数据创建时
class UserInfo(models.Model):
# 比如用户注册时会生成用户名和密码
username = models.CharField(max_length=32)
password = models.CharField(max_length=64)
# 创建记录时会生成当前时间存放在ctime里,这个就是用户的注册时间
ctime = models.DataTimeField(auto_now_add=True)
# 用户修改密码会更新uptime的时间,这个就是上次修改密码的时间
uptime = models.DataTimeField(auto_now=True)
# models.UserInfo.objects.filter(id=nid).update(password=‘123456‘) 这种方法更新是不会刷新 auto_now 的时间的
# 用save()方法更新可以刷新 auto_now 的时间
# obj = models.UserInfo.objects.filter(id=nid).first()
# obj.password = ‘654321‘
# obj.save()
Admin中有效果的参数
choices :Admin中显示选择框的内容。(用不变动的数据放在内存中从而避免跨表操作,跨表操作会涉及到性能问题)
class UserInfo(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=64)
# 用户有各种类型
user_type_choices = (
(1, ‘管理员‘)
(2, ‘普通用户‘)
(3, ‘访客‘)
)
# 定义一个用户类型的字段
user_type_id = models.IntegerField(choices=user_type_choices, default=2)
# 这样数据库里是一个整数类型,值是1、2、3
# 但是我们在admin里看选择框的时候看到的是“管理员”、“普通用户”、“访客”,这就是因为把选项所对应的内容放到了内存中了
# 有了Django这个功能就不用再搞一张表,存放各个数值对应的内容了,还要做外键关联,用的时候还要连表查询
# 即使不用admin,我们也可以在自己的代码里读取这个属性获取到内容,避免连表查询
blank :Admin中是否允许用户输入为空
verbose_name :Admin中显示的字段名称,默认显示为变量名
editable :Admin中是否可以编辑。默认是True,设为False后就是在admin中不可编辑了,也不会显示出来了。
error_messages :自定义错误信息(字典类型)。字典key:null、blank、invalid、invalid_choice、unique、unique_for_date
class UserInfo(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=64, error_messages={‘null‘: "不能为空", ‘invalid‘: ‘格式错误‘})
help_text :Admin中该字段的提示信息。默认没有提示信息,设置后会显示在input框的下方
validators :自定义错误验证(列表类型),具体要等到后面讲
外键操作-一对多
上面讲的choices参数,提供了一种将数据存在内存中来提高效率的方法。好处是避免了跨表操作提高了效率。坏处也有,就是数据不方便修改。如果要修改,那就要修改好之后重启一下服务使你的修改生效。而重启操作是有风险的应该避免,那么对于这种经常要修改的内容就不适合放在内存中了,而是要放到另外一张表里。
创建外键关联-修改表结构
在models.py里修改我们的表结构,新增一张用户部门表,原来的用户信息表中新增一列部门id:
from django.db import models
# Create your models here.
# 新增一张表
class UserGroup(models.Model):
group_id = models.AutoField(primary_key=True) # 这次自己写自增id
dept = models.CharField(max_length=32, unique=True)
class UserInfo(models.Model):
# 默认会自动创建自增id并作为主键
username = models.CharField(max_length=32) # 字符串,长度32
password = models.CharField(max_length=64)
# 新增一列存放部门
# to_field参数可以缺省,默认就是主键
# on_delete=models.CASCADE,这个在1里应该是默认值,现在不能缺省了
user_group = models.ForeignKey(‘UserGroup‘, on_delete=models.CASCADE, to_field=‘group_id‘)
然后去终端执行那2条命令使新的表结构生效:
python manage.py makemigrations
python manage.py migrate
添加部门数据
这里可以的话最好直接是直接去操作数据库,否则简单搞个网页来添加数据。参看之前的userlist,简单搞个只做显示和添加的页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h2>添加部门</h2>
<form method="post" action="/grouplist/">
<input type="text" name="dept" placeholder="部门名称">
<input type="submit" value="添加">
</form>
</div>
<div>
<h2>部门列表</h2>
<table border="1">
<thead>
<tr>
<th>id</th>
<th>dept</th>
</tr>
</thead>
<tbody>
{% for row in dept %}
<tr>
<td>{{ row.group_id }}</td>
<td>{{ row.dept }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
然后是views.py里的处理函数:
def group_list(request):
if request.method == ‘GET‘:
dept = models.UserGroup.objects.all()
return render(request, ‘grouplist.html‘, {‘dept‘: dept})
elif request.method == ‘POST‘:
dept = request.POST.get(‘dept‘)
models.UserGroup.objects.create(dept=dept)
return redirect(‘/grouplist/‘)
urls.py里的对应g关系:
path(‘grouplist/‘, views.group_list),
查看被关联的属性
对于UserInfo中新增的一列,在类中我们的属性名称是 "user_group" ,而实在在数据库中创建的自动名是 "user_group_id"。
我们再操作的时候就有2个属性可以操作:
.user_group_id
:就是这个字段里的值,也就是数据库里实际存放的内容.user_group
:这是一个对象,通过这个对象取到UserGroup里的内容,比如:.user_group.group_id
:就是UserGroup表里的自增id,结果和.user_group_id
应该是一样的.user_group.dept
:就是这个username锁关联的部门名称了
修改之前的userlist页面,现在把部门名称也显示出来。这里只需要改html,处理函数时不用修改的。实际也只需要在表格中加上一列直接可以去到关联的表里的属性值。下面是userlist表格的部分内容:
<div>
<h2>用户列表</h2>
<table border="1">
<thead>
<tr>
<th>id</th>
<!-- 这里加一列 -->
<th>username</th>
<th>dept</th>
<th>password</th>
<th>按钮</th>
</tr>
</thead>
<tbody>
{% for row in users %}
<tr>
<td>{{ row.id }}</td>
<td>{{ row.username }}</td>
<!-- 这里加一列,直接就能取到部门名称 -->
<td>{{ row.user_group.dept }}</td>
<td>{{ row.password }}</td>
<td>
<a href="/userdel-{{ row.id }}/">删除</a>
<span>|</span>
<a href="/useredit-{{ row.id }}/">编辑</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
添加用户时选择部门
显示没问题了,页面的上部还有添加用户,现在再要添加用户就需要把用户部门也加上了。部门搞成一个下拉框,不过下拉框的内容还需要修改处理函数传值过来。处理函数还要处理页面提交的内容:
def user_list(request):
if request.method == ‘GET‘:
users = models.UserInfo.objects.all()
# 这里多获取一个部门的列表,传给页面,页面的下来列表会用到。直接找UserGroup获取数据
# 把对象传给页面的下拉列表,列表的value就是对象的id,列表的内容就是对象的dept
depts = models.UserGroup.objects.all()
return render(request, ‘userlist.html‘, {‘users‘: users, ‘depts‘: depts})
elif request.method == ‘POST‘:
username = request.POST.get(‘user‘)
password = request.POST.get(‘pwd‘)
# 这里通过select获取到的直接就是id的值,所以提交的时候也简单的提交值就可以了
group_id = request.POST.get(‘group_id‘)
models.UserInfo.objects.create(username=username, password=password, user_group_id=group_id)
return redirect(‘/userlist/‘)
页面里加上下拉列表,下面是添加用户的部分:
<div>
<h2>添加用户</h2>
<form method="post" action="/userlist/">
<input type="text" name="user" placeholder="username">
<select name="group_id">
{% for item in depts %}
<option value="{{ item.group_id }}">{{ item.dept }}</option>
{% endfor %}
</select>
<input type="text" name="pwd" placeholder="password">
<input type="submit" value="添加">
</form>
</div>
数据添加的另外一种方法
上面通过下拉列表方便的获取到了部门id的值,所以直接通过传值给user_group_id完成了数据的添加。也可以通过传对象给user_group完成数据的添加,大概是这样的:
group = models.UserGroup.objects.filter(id=1).first()
models.UserInfo.objects.create(username=username, password=password, user_group=group)
两种方法根据实际情况选择,不过传值的方法更好,少一次数据库的操作。
还有更多内容要下节讲了
模板
这天没讲到
课后练习
用户管理:
- 一张用户表、一张用户组表。做好外键关联。分别实现对两张表的增删改查
- 添加,做成模态对话框的形式
- 修改,目前可以用页面跳转的形式,但是要显示默认值
- 做一个比较好看的页面,推荐套用模板
以上是关于Python自动化开发学习19-Django的主要内容,如果未能解决你的问题,请参考以下文章
Python range 数据类型 [学习 Python 必备基础知识][看此一篇就够了][range()][range 元素元素检测元素索引查找切片负索引][检测 range 对象是否相等](代码片
Python range 数据类型 [学习 Python 必备基础知识][看此一篇就够了][range()][range 元素元素检测元素索引查找切片负索引][检测 range 对象是否相等](代码片