Django web开发 基础
1. Django的安装
pip install django
django和其他第三方Python模块一样,会在当前python环境下的lib>site-package
中,只是django是比较大的那种模块。
But,django这个包呢同时会生成django-admin.exe
在Scripts
文件夹中,这个exe可执行文件是帮助我们操作django项目的。目录情况大体如下:
python环境路径
- ...
- scripts
- pip.exe
- django-admin.exe 【工具、创建django项目的文件和文件夹】
- lib
- 内置模块
- site-package 【安装的第三方模块】
- flask
- ...
- django 【框架的源码】
- ...
- python.exe
- ...
2. 创建项目
django项目会有一些默认的文件和默认的文件夹,所以我们先得通过前面提到的django-admin.exe
来进行一些准备操作。
2.1 通过终端创建
-
"python环境路径scriptsdjango-admin.exe" startproject django项目名 # 如果python环境路径配置到了电脑的环境变量中就可以这么写了 django-admin.exe startproject django项目名
比如我们创建项目名为
track_chart_web
的django项目,输入django-admin startproject track_chart_web
,即可看到自动生成的目录结构,此时django项目就创建成功了:
2.2 通过Pycharm创建
菜单栏点击File
,选择New Project...
,就可以开始创建项目了。
点开上图两个红箭头所指的位置,环境我们选Existing interpreter
,其中的Interpreter
就默认即可。其他的都默认即可。
可以看到这里的界面两个选项:一个是
New environment using
,一个是Existing interpreter
。前者呢,是为项目创建一个新的 Python环境,然后单独进行管理;后者呢,是使用电脑上已有的 Python环境,如果你的电脑上存在多个 Python环境,那你点开Existing interpreter
其中的Interpreter
就可以看到所有的已有环境,接着进行选择。以普遍理性而言,写项目嘛,考虑移植性等长远目标,一个项目最好得有自己的独立 Python环境,如果多个项目同时使用一个 Python环境,那环境就不好管理了(eg:是某些项目需要用到的第三方模块是一个,但版本不同,可能就会导致项目跑不起来)。
But,我个人也不习惯用
New environment using
来创建项目的单独环境(因为很容易导致报错),而是用 Python的一个第三方模块——virtualenv
、virtualenvwrapper-win
。详细操作见链接:Django独立环境配置pip install virtualenv pip install virtualenvwrapper-win
好啦,最后我们点create就可以得到2.1 通过终端创建
一模一样的目录结构咯(只是会多一个templates空文件夹)。
2.3 对比差异
在终端的命令行方式创建项目是标准的方法;Pycharm的话,会在标准的基础上,多一些东西。
- 多创建了一个templates目录,我们删除,后面前端的模板文件不放在这里
- 项目文件
settings.py
中的TEAMPLATES
的字典下键为'DIRS'
对应的值不同,我们也不用它,值改成空列表[]
即可
项目文件介绍:
django_study_demo
│─ manage.py 【项目管理的脚本,不要修改,eg:启动、创建app、数据库管理等】
└─django_study_demo 【与项目同名的文件夹】
│─ asgi.py 【和wsgi.py一起,接收网络请求的】【不用修改】【Django接收异步的】
│─ settings.py 【项目的配置文件,eg:数据库连接信息、注册app等】【常操作】
│─ urls.py 【全部的URL和函数的对应关系】【常操作】
│─ wsgi.py 【和asgi.py一起,接收网络请求的】【不用修改】【Django接收同步的】
│─ __init__.py
3. 创建APP
这里的APP不是手机应用那个APP,而是一部分功能的意思。一个Django项目可能需要处理多个业务,我们将业务拆解,一部分一部分分开来管理代码会比较有条理,所以可以通过创建多个app来分别实现多个业务功能。
举栗来说,一个项目分别对用户管理、订单管理、后台管理等业务都创建相应的app去实现。这样每个app的表结构、函数、HTML模板、css等都可以分开管理,不会混乱。
But,app是为了分开实现那些大功能的,像增加用户信息和删除用户信息这两个小功能就大可不必分成两个app来写。所以,我们自己个人开发的时候,就可以只创建一个app来实现项目功能。
用Pycharm打开命令行界面(通过cmd打开也行,不过每次打开都要切换到当前项目路径下,很麻烦,所以最好直接用Pycharm的命令行界面直接打开),然后输入指令python manage.py startapp app名
(通过manage.py来创建app)。
app目录结构介绍(Line 1~8前面说了,这里就不再提了):
django_study_demo
│ manage.py
├─django_study_demo
│ asgi.py
│ settings.py
│ urls.py
│ wsgi.py
│ __init__.py
└─index 【app名命名的文件夹】
│ admin.py 【固定的不用动】django默认提供的后台管理,但实际开发不常用
│ apps.py 【固定的不用动】app启动相关
│ models.py 【☆很重要,对数据库进行操作】这里不用SQL写了,Django封装了ORM供调用
│ tests.py 【固定的不用动】用来单元功能测试的,个人小项目可以不用管
│ views.py 【☆很重要,撰写视图函数(得我们自己写的)】
│ __init__.py
└─migrations 【固定的不用动】数据库变更记录,会自动生成文件,我们不用动
__init__.py
4. 快速上手
想要让Django项目快速运行起来,我们还得按顺序操作以下步骤
4.1 确保app已注册
在前面3. 创建APP
中,我们只是创建的app,但是这样还是不能正常跑起来app中的功能的,我们还得让Django知道,我们写了一个新app,这个声明的过程就是app的注册。此过程需要修改settings.py,过程如下:
4.2 URL和函数的映射
大家使用网站时,在网站内的页面跳转都会改变URL,不同的URL就需要后台调用不同的功能给用户使用。所以每个URL都得有函数来执行相应的代码。此过程需要修改urls.py,过程如下:
-
path()
需要两个参数,前者是字符串,规定URL,后者是触发的函数名。规定的URL可以随便写,因为是我们自己规定哪个URL跳转哪个函数,我这就写'index/'
-
前面说过,每个app都有个
views.py
,里面是写函数的,所以我们第二个参数位导入相应函数即可(还是模块导入的知识点,通过.
来导入)
-
path('index/', views.index)
这句的映射关系即为访问http://www.xxxx.com/index
时,触发的函数是名为index的app下的views.py
中的index函数。 -
结束。
4.3 视图函数的撰写
好了,既然设置好了映射,但是这个函数我们还是没有写的(所以上面的截图中,index会被标黄的原因【没找到该函数】)。这个过程需修改相应app下的views.py,这个写就完了,函数大家都会写,这里就汇总一下小点即可。
注意点:
以后会经常见到 request和 response这两个单词,可以简单理解为 request是前端给后端发的数据,response是后端给前端传的数据;所以函数必须带 request就可以理解了,这里的 HttpResponse()先简单理解为给前端发简单数据的,后面是得传 html文件给前端的,不怎么用 HttpResponse()的。
4.4 运行Django
python manage.py runserver
如果你是按我们创建项目的步骤一步一步来的,这里直接点绿三角即可运行django项目
点击这个按钮运行时,其实也就是让pycharm帮我们运行runserver这个命令。
这一行和我们刚才的python manage.py runserver
很像,原理都是一样的。
好啦,运行起来项目到现在也没有什么报错,我们就可以访问本地8000端口(django占用端口默认是8000)的服务(localhost:8000
和127.0.0.1:8000
是一样的),来测试我们Django项目了。
好,点击访问http://127.0.0.1:8000/
,出现是这样的画面
Page not found
,就表示我们配置的URL有问题,这个空的URL没有函数对应。确实,我们找到我们当时写的urls.py,我们只写了一条index/
的URL所对应的函数。
所以,我们访问http://127.0.0.1:8000/index/
即可,页面显示的东西也是我们通过HttpResponse()
上传的字符串。
至此,我们这个快速搭建django的步骤已经完毕,已经可以正常访问网址,看到结果了。
4.5 创建页面
上面的过程中,包含了创建index界面的步骤。那我们现在如果还要创建新的页面,只需要两个步骤:
# 增加部分
# urls.py
path('user/add/', views.user_add),
path('user/delete/', views.user_del),
# views.py
def user_add(request):
return HttpResponse("用户添加")
def user_del(request):
return HttpResponse("用户删除")
上面这段代码加到项目代码中,再重新运行,这样就可以正常访问user/add/
和user/del/
两个URL。
4.6 模板文件&静态文件
4.6.1 render()使用
But,正常应用中,网页都得是html那些文件,我们这现在还是就单纯返回字符串,太单调了。
那我们return后面就不用HttpResponse()
了,而是用render()
。render()
的用法是这样。
# 导入render,一般django默认
from django.shortcuts import render
def user_list(request):
# 第一个位子是视图函数的request参数,第二个参数位是html文件路径
return render(request, "user_list.html")
4.6.2 模板路径问题
一开始我们改过了settings.py
中TEMPLATES
字典的DIRS
的值为空列表,所以默认的模板路径是当前app文件夹下的templates文件夹中的文件。所以我们在app下创建名为templates
的文件夹(必须得为templates,不要拼写错误!),并在templates
文件夹下创建html文件。
和4.5说的一样,设置函数和URL对应关系,就可以测试是否可以正常渲染模板文件。
然后我们运行django项目,然后访问我们设置的URL:http://127.0.0.1:8000/user/list/
,就可以看到我们html文件被成功渲染到了前端。
同样的,我们想把user_add()
那个函数也改写成模板文件渲染,我们就可以按图索骥,一步步操作了(这里就不放截图演示了)。
4.6.3 静态文件static
开发过程中,像图片、css、js这些文件都称为静态文件,而这些静态文件,在django项目中也是必须放在规定文件夹中的,这个文件夹必须得叫static
,和templates
一样不能出现拼写错误。为了方便查找,static中,我们再分img、css、js、plugins来分类存放不同的东西。
比如我们引入一个图片作为示范(会发现src那个属性值Pycharm标黄了,暂时这么写,我们后面再说):
# user_list.html中增加一行代码
<img src="/static/img/default.png" alt="">
4.6.4 static路径问题
django中呢,不推荐上面的那种路径写法(所以Pycharm标黄了),而是用下面的这种语法格式。
{% ... %}
这个格式是django的模板语法,load
是占位符(django的模板语法,不止load一个,我们用到就提一个,很简单,对于常用的有个印象即可,这个没有固定的语法规范),{% load static %}
可以理解为加载static路径;然后后面{% static 'img/default.png' %}
就是通过static调用路径,然后后面跟着的字符串就是static中相应文件的路径。
下面我们示范一下,如何引入一下js和bootstrap插件:
- 先把文件放到相应文件夹中(相应文件:jquery-3.6.0.min.js和bootstrap-3.4.1-dist都在这个压缩包的文件夹中)
- 成功。
注意点总结:
- 声明static语法:
{% load static %}
要写在顶部,写在html文件开头 - 声明static的原理:
- 调用语法:
{% static '...' %}
,其中...
是静态文件的路径 - 用
{% ... %}
的优点:
5. Django模板语法
本质:在html中写一些占位符,然后由数据对这些占位符进行替换和处理。
P.S. 上面提到的占位符 static就是和
/static/
(settings.py
中STATIC_URL
的值)做了替换。
<!-- templates_learn.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>模板语法学习</title>
</head>
<body>
<h1>模板语法学习</h1>
</body>
</html>
# views.py
def temp_learn(request):
return render(request, "templates_learn.html")
# urls.py
path('temp_learn/', views.temp_learn),
然后运行并访问http://localhost:8000/temp_learn/
即可。
5.1 调用后端数据
首先,render()
函数第三个参数位可以提供给视图函数传递数据,不过第三个参数必须是字典!
然后我们在templates_learn.html
中就可以通过{{ ... }}
的语法根据键名来调用相应的数据。
然后我们刷新项目,再看浏览器页面,即可看到前端可以成功调用后端数据了。
5.2 索引
从上面的例子中可以看出,{{ ... }}
的语法对于列表数据有些不适合,模板语法是否也提供了取其中元素的方法呢?
有的,通过.
运算调用(和python不同,python是通过[]
)
5.3 循环语句
不过,上面是4个元素的列表,如果有n个就得使用循环了,循环的模板语法为:
{% for i in ... %}
...
{% endfor %}
以我们上面stu_list
为例:
效果:
P.S. 是产生了四个 span标签的哦,因为模板循环语法中的 html语句会循环执行。
上面说了列表,字典也一样。
测试数据:
# views.py
user_info = {
"name": "zm",
"salary": 100000,
"role": "CTO",
}
return render(..., ..., {..., ..., "user_info": user_info})
html中调用:
效果:
5.4 条件语句
{% if name == "XXX" %}
...
{% elif name == "XX" %}
...
{% else %}
...
{% endif %}
和python一样,就是不用加:
,结束必须带个{% endif %}
,其他都一样。
观察结果,zm被加粗了。
5.5 模板语法原理
原理:
所以,浏览器拿到的永远都是纯正的HTML文件中内容的字符串,不带任何django模板语法。我们刚才上面写的那些语句,我们从浏览器看看是什么(右击网页空白处=>检查),可以看到是纯正的HTML代码。
6. Request&Response
我们单独创建一个something界面(这里不赘述过程了)来单独聊聊这个知识点。
6.1 Request
request就是一个对象,封装了用户通过浏览器(爬虫等许多途径,不止通过浏览器一种)发送过来的所有请求相关的数据,常用的有:
如果想体验获取URL传递的数据,就在网址后面加上?
,后面跟着键值对(k=v
),以&
隔开。如下图:
6.2 Response
视图函数中return所返回的,就是我们要写的response,常用的有:
-
redirect()
:让浏览器重定向到其他页面,返回的是网址。Q:重定向这个过程是怎么样的?
① 浏览器提交请求给Django,Django收到请求并提交请求给重定向的网址,Django获取到该网址返回的响应数据时,Django再返回给浏览器这个响应数据;
② 浏览器提交请求给Django,Django收到请求并直接返回给浏览器重定向的网址,浏览器重新去新的网址那里提交请求,然后新的网址给浏览器返回响应数据。
A:②是对的。
6.3 练习
这里做一个简单的登录功能:
P.S. 这个逻辑还是很常用的,浏览器一开始访问 login.html(GET请求),视图函数login()就render渲染login.html;填写完 form表单点击提交,再次访问就是POST请求了,从而触发不同的代码逻辑。
这段代码如果复刻到flask是没有问题的,但是django会报错,因为django自带了一个csrf,可以理解为是一个安全机制。解决方法大家也得记住,很简单,在form表单中,添加一行占位符:{% csrf_token %}
。
此时,我们在终端也可以看到后端接收到的数据
P.S. 这个
'csrf....'
参数就是我们刚才加的{% csrf_token %}
产生的。它的目的是检测该请求是否是网页传来的,防止黑客通过脚本,伪造请求攻击网站。简单说,csrf是一个安全机制。
But,一般网站登录无论成功失败,都会返回一个页面的,肯定不是HttpResponse()
这样只返回一个字符串。所以我们现在要改写成成功就跳转其他界面,失败就在原页面上显示登陆失败。
增加一个span标签,然后默认是空字符,登录失败时就传入值给tip,即可实现。
这里因为主页面没做,所以重定向到百度,道理都是一样的,就是网址不同罢了。
7. 数据库操作 ORM
上面的登录功能和实际应用中的登录功能还是差一些的,比如账号密码写死了,正常一个连接数据库的。所以这里我们来说说django连接数据库所采用的ORM。
# 导入
import pymysql
# 连接
conn = pymysql.connect(host="127.0.0.1", port=3306, user="root", password="123" charset="utf-8", db="test")
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
# 发送指令
sql = "insert into admin(username,password,moblie) values('zm', '123', '13345512345')"
cursor.execute(sql)
# 关闭
cursor.close()
conn.close()
可在Django开发中,我们不需要在手动去写了,django给我们封装好了,并且增加了安全机制,让用户更简单更安全的操作数据库。这个封装好的东西就是ORM。
使用了ORM,我们直接可以通过方法来调用数据库,而不用再去写SQL语句了,ORM可以帮助我们翻译。
7.1 安装第三方模块
因为新版本的django默认采用的是mysqlclient这个库,而不是pymysql,所以我们先pip安装mysqlclient。
pip install mysqlclient
windows可能会安装失败,百度mysqlclient wheel
,下载相应版本的wheel文件,然后pip本地安装即可。
P.S. 如果想用pymysql也是可以的,只是可能编码有些问题。
修改的方式是在
__init__.py
中增加代码即可,然后后续操作就和 mysqlclient一样了。
7.2 ORM
ORM可以帮助我们做两件事:
7.2.1 创建数据库
7.2.2 django连接数据库
django连接数据库只需要在settings.py
文件中配置连接参数即可。
-
'default': { 'ENGINE': 'django.db.backends.mysql', # Django的引擎,还可以用Oracle等 'NAME': 'dbname', # 数据库名 'USER': 'root', # 用户名 'PASSWORD': 'xxx', # 密码 'HOST': '', # 数据库服务器地址 'PORT': 3306, # 端口号(MySQL默认3306) }
-
我们连接的是本地的MySQL,所以
HOST
是localhost
或者127.0.0.1
;数据库名和密码大家各自填自己的即可。
7.2.3 django操作表
- 创建表
- 删除表
- 修改表
增加表
在相应app中的models.py中写一个类就是创建一张表。
# 在MySQL中创建对应的一个表
class UserInfo(models.Model):
# 创建字段
name = models.CharField(max_length=32)
password = models.CharField(max_length=64)
age = models.IntegerField()
ORM会帮我们把这个面向对象的代码转化成SQL(表名是app名称_类名
),与上面代码对应的SQL大致为:
create table index_userinfo(
id bigint auto_increment primary key, 【django创建表自带自增字段】
name varchar(32),
password varchar(64),
age int,
);
到terminal界面下执行命令,通过manage.py
来让ORM读取我们写好的models.py中的代码。
python manage.py makemigrations
python manage.py migrate
注意:
刷新一下数据库,就可以看到很多django创建出来的表了。
生成表的原理是django去所有app的models.py
中找需要创建的表,在settings.py
中可以看到,有很多django默认自带的app(如下图),所以除了index_userinfo
表,其他表都是django自带生成的表,我们暂先不用管。
如果还想新建表,就再在models.py里写类,然后执行那两个指令即可。然后刷新数据库就可以看到了。我们新建两个。
class StudentInfo(models.Model):
name = models.CharField(max_length=32)
class TeacherInfo(models.Model):
name = models.CharField(max_length=32)
执行那两个命令。
可以看到数据库中已有新的表了。
删除表
把models.py
中类注释掉,然后再执行那两个指令即可删除表。我们删除index_teacherinfo
表。
执行两条命令
修改表
增加字段
我们得先知道增加列的一些点:
修改表就是增删表中的字段,我们删除就直接注释,增加就直接加。比如我们对index_userinfo
表进行操作。
首先先确认有几个字段
可以看到,django给我们了两个选项,1是给一个默认值为增加的列赋值,2是取消这次操作,我们手动给新增加的列设置默认值。
接下来说选项2,我们再增加字段nickname
操作成功,再看看数据库表
再来,我们直接声明可以为空也可以实现这个目的。再增加字段data
。
再看看表
删除字段
删除字段就很简单了,不像增加字段会有限制,而是和删除表一样,注释掉,两条指令执行一下就行了。
修改字段
直接改变量名即可,然后django会问你是否真的改,输入y
即可。
表内增删改查
前面说了,我们可以通过操作对象的方式,免去一大堆SQL语句,从而通过用django来操作表的结构。同样的,django还给我们提供了方法来操作表中的数据。这些方法是写在视图函数中的,调用相应models.py
中的类即可,最后我们演示。
增
# insert into index_studentinfo(title) values("zm");
StudentInfo.objects.create(title="zm")
# insert into index_userinfo(name,password,age) values("gzh","123",18);
UserInfo.objects.create(name="gzh", password="123", age=18)
删
语法:
UserInfo.objects.filter(id=2).delete() # 删除表中id为2的删除
StudentInfo.objects.all().delete() # 删除表中所有内容
改
语法:
UserInfo.objects().all().update(password="123") # 把表中password全改成123
UserInfo.objects().filter(id=2).update(password="1") # 把表中id为2的那行数据的password改成1
查
语法:
类名.objects.all()
:查询表中所有数据类名.objects.filter()
:筛查相应的数据
注意:返回的数据都是一个QuerySet
类型数据,可以理解为List
,只是每个元素是一个对象罢了。
all_data = UserInfo.objects.all()
for obj in all_data:
print(obj.id, obj.name, obj.password, obj.age)
# UserInfo.objects.filter(id=1) # 获得一行数据,但是也是QuerySet的一个列表,只是只有一个元素
# 所以可以通过first()来取到第一个单独的元素
data = UserInfo.objects.filter(id=1).first()
print(data.id, data.name, data.password, data.age)
7.3 用户管理案例
然后我们改写之前写过的user_list.html
、对应的视图函数user_list()
,用表格把index_userinfo
的数据全渲染出来。
挺简单的,要不再来试试实现用户注册的功能,我们改写之前写过的user_add.html
然后,我们来测试一下。
可以看到,我们成功实现了增加功能,同理,条件筛选查找、删除都可以实现。
Django web开发 Day2
接下来是在做一个系统的实践中学习一些新的点。这个系统呢,我们做的是员工管理系统。有些点说过了,就一下带过了。
1. 创建项目
把django默认创建的templates删除,以及settings.py
中的TEMPLATES["DIRS"]
修改成[]
。
2. 创建app
前面说过了在terminal中输入指令python manage.py startapp em_web
创建app。(app名随便取,这里叫em_web
)
然后在下方就可以看到类似于terminal的界面,唯一不同的是,省略掉了python manage.py
,直接写后面的指令即可(也会有一些指令匹配显示)。
注册app:
3. ORM创建表结构
3.1 常见参数位
max_length
:一般是CharField()
中用,有关字符的一般都要带,字符的最大长度verbose_name
:是用来注释字段名的,方便人家知道该字段是什么意思,而且如果调用django的admin模块来后台管理的话,字段名会自动替换成verbose_name
primary_key
:声明主键,主键是唯一且非空的,一般都是写primary_key=True
,=False
的话不写这个参数即可,不需要特意写=False
default
:默认值max_digits
:DecimalField()
用,规定数字最长是多少(不包括正负号)decimal_places
:DecimalField()
用,规定显示小数点后几位null
&blank
:声明该字段可以为空,组合使用,一般都是写null=True, blank=True
,=False
的话不写这个参数即可,不需要特意写=False
;如果想知道blank和null有什么区别,详见这篇博文choices
:添加django的约束,注意,给这个参数赋值时得用元组
3.2 创建表结构
3.3 设置表间关系
3.3.1 一对多 ForeignKey
现在,我们得设置一个字段,让每个员工都有自己属于的部门。前面有数据库的笔记,就直接说了,不引导了,员工和部门之间是“多对一”的关系,所以在“多”的那个表设置外键。
P.S. 一般表间联系都是用 id,而不是用名称这种的字段名。因为这样【节省存储开销】,id这种字段一般比其他字段所耗存储都要小。
But,在一些比较大的公司,联系会直接用名称,因为大公司的某些数据表可能每天被查询上万上亿次,如果还仍采用 id的话,查询的时候得连表操作,速度较慢。【加速查找,允许有这种冗余】
我们自己写的小项目就用 id了。
在员工表中设置外键:dep = models.ForeignKey(to="Department", to_field="id")
,注意生成表后,django会自动在此字段名后加上_id
,所以实际表中的字段名是dep_id
。
不过,这样设置的外键还不行,如果直接执行生成表的操作的话,会报错的。现在假设一个场景:部门表有一个部门被裁撤了,员工表中,原属于该部门的员工改怎么办?
不管?肯定是不行的,因为上面说到了,这个关系是有约束的,dep_id
必须在部门表中存在此id
,故肯定要对这个情况设置参数来调用相应的解决方案。
有许多解决方法:
- 删除关联的员工:一般叫级联删除,相应配置为
on_delete=models.CASCADE
- 不删置为空:这样写,必须设置允许该字段可以为空,相应配置为
null=True, blank=True, on_delete=models.SET_NULL
这里我们就设置级联删除:dep = models.ForeignKey(to="Department", to_field="id", on_delete=models.CASCADE)
3.3.2 添加django约束
上面我们说的是数据库的约束条件,其实django也可以添加约束条件,通过choices这个参数来添加的。
3.4 创建数据库
好了,接着我们用MySQL创建数据库或者navicat创建,前面说过navicat创建了,这里说MySQL命令行的创建方式
# 先进mysql
mysql -u root -p
输入密码,回车确认
# 创建数据库
create databases 数据库名 DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
# 查看是否有刚才创建的数据库
show databases;
修改django中的配置文件,连接MySQL
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'dbname',
'USER': 'root',
'PASSWORD': 'xxx',
'HOST': '127.0.0.1',
'PORT': 3306,
}
如果没装
mysqlclient
模块,记得在settings.py
同目录下的__init.py__
中添加代码:import pymysql pymysql.install_as_MySQLdb()
3.5 指令生成
Terminal执行两条指令生成表
python manage.py makemigrations
python manage.py migrate
或者在pycharm提供的那个界面写
makemigrations
migrate
P.S. 当你没用mysqlclient时,如果报错:
django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.13 or newer is required; you have 0.9.3.
,说明pymysql版本太低了,删除再安装就是新版的了,指令如下,在 Terminal中执行:pip uninstall pymysql pip install pymysql
mysql或navicat中查看表,之前展示的是navicat,这里用mysql来看。
# 先进MySQL,前面已写,这里就不写了
# 进入相应数据库
use em_web;
# 展示所有表
show tables;
4. 静态文件的管理
先搞好static和templates
6. 部门管理
好了,接下来就开始功能实现了。
6.1 原始方法实现
6.1.1 部门列表
页面构思
先大体画一画页面的模样
三要素
创建dep_list.html
,撰写视图函数dep_list()
,设置url
构建页面
然后我们快速的通过bootstrap来构建页面的大体布局,从Bootstrap组件这里找需要的样式,添加进来修改。
修改好的导航栏代码(最好自己改好,不建议copy,这里只是给大家参考):
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Employee Manager</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="/dep_list/">部门管理 <span class="sr-only">(current)</span></a></li>
<li><a href="#">Link</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#">登录</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">Dropdown <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
相关代码为:
<div class="container">
<div style="margin-bottom: 10px">
<a class="btn btn-success" href="#">
<span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>
新建部门
</a>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-th" aria-hidden="true"></span>
部门列表
</div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>部门ID</th>
<th>部门名称</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mark</td>
<td>Otto</td>
<td>
<a class="btn btn-primary btn-xs">编辑</a>
<a class="btn btn-danger btn-xs">删除</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
然后也可以加一加注释,方便自己以后修改即可。
加载数据
因为我们现在暂时没有数据,所以先手动往数据库里插入几条数据。
insert into em_web_department(dep_name) values("IT部"),("销售部"),("运营部");
完善视图函数
然后我们再完善视图函数,把数据库中的数据传到前端。
然后前端调用
数据就成功渲染到了前端了。
6.1.2 部门添加
修改跳转连接,target是设置跳转的方式,默认是本页面跳转,_blank
就是新建页面跳转。
设置url、视图函数、html文件三要素
因为dep_add.html
有些东西和dep_list.html
有些代码是重复的,所以我们复制过来。
代码先不展示,在下面一起展示。和dep_list.html
一样,通过bootstrap官网组件,设置一个面板,面板中放一个表单。
{% load static %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>【EmployeeManager】部门添加</title>
<link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1-dist/css/bootstrap.css' %}">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Employee Manager</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="/dep_list/">部门管理 <span class="sr-only">(current)</span></a></li>
<li><a href="#">Link</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#">登录</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">Dropdown <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主体部分 -->
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>
部门添加
</h3>
</div>
<div class="panel-body">
<form class="form-horizontal">
<div class="form-group">
<label for="dep_name" class="col-sm-2 control-label">部门名称</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="dep_name" name="dep_name" placeholder="请输入部门名称 Please Input DepartmentName">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-success">添 加</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script rel="script" src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
<script rel="script" src="{% static 'plugins/bootstrap-3.4.1-dist/js/bootstrap.js' %}"></script>
</body>
</html>
接着我们完善视图函数,实现部门添加的功能
完善前端form表单
测试功能
再点“新建部门”,再添加试试
6.1.3 部门删除
这个功能不需要跳转页面,点击dep_list.html
中每行结尾的删除按钮就删除对应行的数据。
那问题来了,点id=2
对应的删除,怎么让后台也知道id=2
?很简单,前端页面跳转的时候,跳转链接增加?id=2
后缀,通过GET方法传递数据。所以,我们得设置一个删除按钮的跳转链接。
测试功能
点击谁,谁就会被删掉。(后续可以通过js加一个提示框,这样合理一些。)
6.1.4 部门编辑
编辑页面和添加页面很像,唯一不同的是,就是框里的内容应该有个默认值。所以,三要素先写好,html文件能copy过来的能copy过来。
再来,点击编辑按钮也得需要让前端告诉后端是修改谁的名称,然后后端跳转页面时,也得给前端传数据库中原始的名称。先完善前端跳转的链接。
再完善视图函数,这些都是GET
请求响应的处理逻辑,所以我们写在GET
中。然后还得想想POST
请求,用户点击修改后,应该通过POST
获取新名称,修改数据库的旧名称,再跳转dep_list.html
页面。
But,你会发现这样写是不行的,第一次通过GET
获取的id数据,在POST
中是调用不到的,所以我们还得让前端传POST
请求的时候,同时要传id和name两个数据,所以要写一个input
,然后设置type="hidden"
和name
,然后在视图函数中,重新接收id信息。
测试功能
修改,并提交
修改成功
再改回来也是可以的
不过,还有另一种传id的方式,就是直接写入url中。把urls.py
中写入这么一句:
# 访问链接'127.0.0.1:8000/dep/数字/update/'时触发
path('dep/<int:nid>/update/', views.dep_update)
# 不过,为了和update区分开,这里再写个edit,通过url来传递id,实现update同样的效果
path('dep/<int:nid>/edit/', views.dep_edit)
# P.S. django 1.x.x不支持此功能
与此url对应的视图函数,就必须带上nid
这个参数了,不然我们后端无法调用这个参数。
def dep_edit(request, nid):
...
此时,前端跳转的链接就不用写?
来传参了,之间跳转即可(这里再加一行<a></a>
,区别开两个方式)。
新的视图函数逻辑也不同了。
接着就是写POST
部分了,和之前一样的思路,就是id用参数nid即可,不需要再通过POST
获取id了。
功能也都可以正常跑。
可以看到,这个思路和我们一开始说的错误很像,唯一区别就是一开始错误是在视图函数中声明一个变量存第一次GET
发来的值,但是POST
中调用时,无法调用;而这个把需要用GET
发给后端的值,直接写入url中,把url当做一个中转站,POST
调用的时候直接调用参数即可。
6.1.5 模板的继承
使用方法
6.1.1到6.1.4,我们写了三个页面,可以发现有很多代码都是重复的,一模一样。eg:导航栏、引入文件等代码都是重复的。所以我们可以将这些重复的模板代码,单独写到一个html中,然后其他html需要就调用即可。
首先,我们随便从写好的三个html文件中复制全部代码,新建dep_base.html
,然后我们来看看哪些代码是可以不用变的(红框部分都可以不用变)。
P.S. 这三个html实现的都是部门管理这个功能,所以导航栏的
active
始终是“部门管理”那个li
标签,所以对于部门管理这部分的页面,可以共用这个导航栏,所以叫dep_base.html
。比如后面员工管理,导航栏的active就不是“部门管理”这个li
了,那我们可以换一个模板继承,比如修改出一个emp_base.html
。当然,你也可以把模板文件中,相同代码再继承,出一个base.html
,这样都是可以的。
然后我们就来学习继承的语法,首先将需要继承的部分不动,把不需要继承的地方换成这段代码:
{# XXX是这个块的名字,子模板想要往哪加代码,就是通过名字来确定的 #}
{% block XXX %}{% endblock %}
然后,我们修改其他三个页面,删除其中与dep_base.html
重复的代码,让那三个页面继承dep_base.html
。
eg:dep_list.html
我们就可以写成这样,只保留不同代码的部分,其他部分都删除。
同理,操作另外两个html文件。
我们再去前端看看,页面还是正常显示的,功能也都是正常跳转的。
模板继承的好处
模板继承是很灵活的,你可以定义多个block,给子模板添加东西,上面我们定义了title块给子模板修改页面标题,content块给子模板撰写自己的主体部分。
我们还可以定义css块,让子模板可以引入只有自己使用的样式;定义js块,让子模板引入自己使用的js,等等等等。大家可以自行按实际需求来写。
另外,如果你要对一些代码进行修改时,你可能需要把三个文件中的相应代码都要修改,而用了继承,你就只需要修改模板即可。eg:修改导航栏的布局。
语法总结
{# 定义块 #}
{% block XXX %}{% endblock %}
{# 继承语法 #}
{# 要写前面,先继承,再调用块;继承哪个html就写哪个 #}
{% extends 'XXXX.html' %}
{# 调用块,并写入代码 #}
{% block XXX %} {# 调用XXX块,在XXX块中,写入... #}
... {# 写入的模板代码 #}
{% endblock %}
6.1.6 员工管理
前面我们实现了部门管理,接下来我们就来接着实现员工管理。
员工列表
和部门列表很像,我们先修改出一个emp_base.html
,和dep_base.html
就差一个导航栏的active
不同。
好了,我们开始写emp_list.html
,和dep_list.html
很像,我们快速搭建,让大体页面先出来,有的东西就先注释不加。
{% extends 'emp_base.html' %}
{# 页面标题 #}
{% block title %}
<title>【EmployeeManager】员工列表</title>
{% endblock %}
{# 页面主体 #}
{% block content %}
<div class="container">
<div style="margin-bottom: 10px">
<a class="btn btn-success" href="" target="_blank">
<span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>
创建员工
</a>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-th" aria-hidden="true"></span>
员工列表
</div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>员工ID</th>
<th>员工名称</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{# {% for dep in dep_queryset %}#}
<tr>
<td>1</td>
<td>xxx</td>
<td>
<a class="btn btn-primary btn-xs" href="#">编辑</a>
<a class="btn btn-danger btn-xs" href="#">删除</a>
</td>
</tr>
{# {% endfor %}#}
</tbody>
</table>
</div>
</div>
{% endblock %}
然后我们来看怎么渲染列表数据,这么多列的数据,我们一个一个写上去。
和dep_list.html
一样,我们在数据库中插入一些数据再实现渲染功能。进入mysql,插入数据:
use em_web;
# 查看表的字段名
desc em_web_employee;
# 查看有哪些可用的部门id
select id from em_web_department;
# 插入数据
insert into em_web_employee(name,password,age,account,create_time,gender,dep_id) values("zm","123",20,10000.23,"2000-09-28",1,1);
insert into em_web_employee(name,password,age,account,create_time,gender,dep_id) values("lx","abc",21,1000.64,"2020-08-29",1,3);
insert into em_web_employee(name,password,age,account,create_time,gender,dep_id) values("gzh","321",22,50000.98,"1999-09-27",1,4);
insert into em_web_employee(name,password,age,account,create_time,gender,dep_id) values("xcq","213",19,1000.98,"2010-07-2",2,1);
# 查看表中是否有数据
select * from em_web_employee;
插入完成
渲染就很简单了,参考部门列表的view写,这里就不说过程了,直接展示结果。
可以看到,这里获取到的入职时间是datetime类型的数据,之前学习python模块那章时提过,把datetime类型数据转换成string是要用strftime("%Y-%m-%d-%H-%M")
,我们只需要在前端显示年月日即可,所以"%Y-%m-%d"
即可。
然后我们再调整员工性别,不能显示1和2,得显示对应的男和女,当时我们再models中用choice设置了,所以我们得用django的方法来调用,调用方法是get_字段名称_display()
再来,再改所属部门,可以想到代码怎么写:Department.objects.filter(id=emp.dep_id).first()
,但是,django也给这个提供了外键操作方法:emp.dep
,会把名为dep
的那个外键,自动链表,返回外表中相应的那行数据。
P.S. 注意:外键叫啥就写啥,不用加
_id
,eg:这里的外键叫 dep,所以emp.dep.dep_name
就可以获取部门名称了,而不是emp.dep_id.dep_name
。
简而言之,emp.dep
等价于Department.objects.filter(id=emp.dep_id).first()
。所以我们通过.
调用就可以获得所属部门名称了。
我们这样写就是在后端把数据格式调整好,再传到前端,也可以在前端实现数据格式的调整,但python语法和django模板语法是不同的,所以我们得改些代码。
可以看到,我们需要改两句。模板语法中,不允许加()
,如果在python中需要加括号的,在模板语法中去掉就好。但是像时间这种括号里带参数的呢?怎么删括号?哎,模板语法中用|
来写,而且Django模板改写了strftime()
,封装成了date
。
{{ emp.create_time|date:"Y-m-d H:i:s" }}
我们只需要"Y-m-d"
就行。
员工新建
和部门添加功能实现过程一样,这样的原始方式就是马上用的django组件的原理。所以大家可以参考部门添加功能的实现,来写员工新建。
这里我们就点出几个点:
- 新建的时候,每个输入框传入的数据都得判断非空,员工新建这么多栏信息,光写判断都麻烦死
- 如果有错误,页面应该有一些错误提示
- 如果需要改输入框,页面上每个字段可能都得重新写一遍
- 关联的数据还得手动获取并手动循环展示在页面(Form组件解决不了,还得自己写点逻辑实现)
接着,我们就开始用Form组件和ModelForm组件来重新实现员工新建。
6.2 Form&ModelForm
django中提供了Form和ModelForm组件,可以帮助我们快速方便达到开发目标。其中,Form组件时比较简便的,ModelForm组件是最简便的。不过要理解ModelForm组件,得先理解Form组件,理解Form组件也得先理解原始方法实现的逻辑。
6.2.1 Form概述
# views.py
class MyForm(Form):
{# widget=forms.Input表示在前端渲染成输入框 #}
user = forms.CharField(widget=forms.Input)
pwd = forms.CharField(widget=forms.Input)
email = forms.CharField(widget=forms.Input)
def emp_add(request):
"""员工新建"""
if request.method == "GET":
form = MyForm()
return render(request, "emp_add.html", {"form": form})
{# emp_add.html #}
<form method="post">
{{ form.user }}
{{ form.pwd }}
{{ form.email }}
</form>
{# 或者这么写 #}
</form>
{% for field in form %}
{{ field }}
{% endfor %}
</form>
可以发现,MyForm里面的内容和models.py中数据表的代码很像,所以我们可以用ModelForm组件更简化代码。
6.2.2 ModelForm概述
# models.py
class Employee(models.Model):
"""员工表"""
name = models.CharField(verbose_name="员工姓名", max_length=16)
password = models.CharField(verbose_name="员工密码", max_length=64)
age = models.IntegerField(verbose_name="员工年龄")
account = models.DecimalField(verbose_name="账户余额", max_digits=10, decimal_places=2, default=0)
create_time = models.DateTimeField(verbose_name="入职时间", )
# 添加django中的约束
gender_choices = (
(1, "男"),
(2, "女"),
)
gender = models.SmallIntegerField(verbose_name="员工性别", choices=gender_choices)
# 生成表时,django会生成dep_id字段而不是dep字段
dep = models.ForeignKey(verbose_name="所属部门", to="Department", to_field="id", on_delete=models.CASCADE)
# views.py
class MyForm(ModelForm):
class Meta:
model = EmployeeInfo
# 想加什么字段,放在列表中即可
fields = ["name", "password", "age"]
def emp_add(request):
"""员工新建"""
if request.method == "GET":
form = MyForm()
return render(request, "emp_add.html", {"form": form})
{# emp_add.html #}
</form>
{% for field in form %}
{{ field }}
{% endfor %}
</form>
ModelForm对于那些操作数据表增删改查的功能,是最简便的方法,使用也不止上面这些方法,我们只是简单说一说。下面我们边实现功能,边学习ModelForm的更多使用方法。
6.2.3 员工新建(ModelForm实现)
然后我们开始用ModelForm的逻辑来完善views.py
。
那我们前面的中文怎么办?修改html文件,通过.
运算,调用label
,就可以通过ModelForm来显示models.py
文件中相应字段名的verbose_name
属性。
现在我们继续加输入框,继续完善views.py中的列表("gender","dep"
暂不添加)
然后前端就直接可以渲染出来了,不用再改任何东西。
接着,我们实现关联数据,来添加gender
和dep
外键
刷新页面
问题又来了,这个所属部门下拉选框里显示的怎么不是中文了,看意思是一个个的Department对象,在ModelForm的源码中,就是直接输出对象的,所以导致了这个下拉选框里显示的都是这样的。
那如何解决呢?在python学习面向对象时,我们介绍了几个双下划线的方法,其中有一个__str__
的方法,是实现输出对象时所输出的内容。在这里,就可以解决这一问题了。
class Foo(object):
def __str__(self):
return "haha"
obj = Foo()
print(obj)
在models.py
中,为Department对象添加__str__
方法,并指定输出对象时,返回“部门名称”。
刷新页面,即可。
所以,当我们在写models.py
中的对象时,我们可以直接加上__str__
,防止以后需要输出。
But,这个界面和dep_add.html
上的输入框样式都不一样,我们可以对生成的input框添加样式嘛?答案是肯定可以的。我们找到views.py
中员工新建的ModelForm类,其中添加widgets
变量,在那里添加样式。
代码中,TextInput()
就表示所有type="text"
的input框,attrs参数就是传入的属性及属性值,eg:这里的{"class": "form-control"}
就是让前端的input框的class属性="form-control"
。
刷新页面,就可以看到页面上type="text"
的input框样式变了。
同理,我们可以一个个的把不同type的输入框样式给它添加上去。
Input类型有这些:
我们按英文意思可以对应上models.py
中不同的Field(eg:TextInput对应CharField)
但是,这样一个一个的添加样式,很麻烦,所以有个简单的方式:
刷新页面,可以在pycharm的run界面看到数据,django会根据Meta中的fields去models.py
找
然后,我们可以直接在这个循环里加attrs
就行了。
当然,其他样式都是可以加的哈,比如我们加一个placeholder
,后端和前端一样,通过label把models.py
的verbose_name
调用过来。
下面就来实现post请求了,也是用ModelForm的方法来实现。
调用我们定义好的ModelForm,把前端POST传来的数据传进ModelForm;ModelForm封装了校验代码,我们调用is_valid()
方法即可,它的返回值是Boolean类型,我们可以和if组合使用。如果是True,就可以通过cleaned_data属性查看数据;如果是False,就可以调用errors属性查看报错信息。
ModelForm还提供了写入数据库的封装方法:save()
,我们is_valid()
为True时,就得把cleaned_data
写入数据库。
But,我们通过save()
方法就可以实现了,不需要再通过object.create()
去写入数据库了。直接来看看是否能正常插入。
P.S. 可能大家会问,那 ModelForm怎么知道插入到哪个表上?其实是知道的,我们写 ModelForm类的时候就指定了一个 model的参数,传入了相应的 models.py中的对象。
ModelForm很方便,直接传新建的ModelForm对象即可,然后在前端.
调用errors
,从而显示报错信息。因为一个输入框可能有多个报错,所以emp_add_form.errors
ModelForm源码返回的是一个列表格式数据,我们一般只取第一个报错信息显示,所以前端我们得写成emp_add_form.errors.0
开始测试报错信息,比如我们不输入数据,直接提交
可以正常显示报错信息了,ModelForm也会帮我们显示在相应输入框的报错信息,极大程度方便了开发。我们再加一个样式,可以更好一些。当然,还有一些其他类型的报错信息,比如我们给name加上一个约束(在ModelForm中也能加)。
不过,这些报错信息都是英文,对于中文用户,比较的不友好,我们在django的settings.py中,设置语言为'zh-hans'
。
好了,至此,ModelForm的相关内容就大致到这,接下来我们再用它来实现员工的编辑和删除。
6.2.4 员工删除
因为删除不用输入框,也不用新界面,所以很容易,和dep_del()
一样,很好写。类比dep_del()
和dep_list.html
完善一下emp_list.html
和emp_del()
即可实现。
# urls.py
path('emp_del/', views.emp_del),
# views.py
def emp_del(request):
"""员工删除"""
emp_del_id = request.GET.get("emp_del_id")
Employee.objects.filter(id=emp_del_id).delete()
return redirect("/emp_list/")
<a class="btn btn-danger btn-xs" href="/emp_del/?emp_del_id={{ emp.id }}">删除</a>
6.2.5 员工编辑
步骤分析:
我们先把url搞好,保证可以跳转页面。
{# emp_list.html #}
<a class="btn btn-primary btn-xs" href="/emp/{{ emp.id }}/update/">编辑</a>
{# urls.py #}
path('emp/<int:nid>/update/', views.emp_update),
# views.py
def emp_update(request, nid):
"""员工编辑"""
return render(request, "emp_update.html", {})
测试一下跳转的url变的是否符合逻辑,这里就不截图了。然后我们开始完善views.py
,和emp_add()
的GET代码逻辑完全一样,先复制一个新ModelForm(可以不用,后面再说)。
然后前端页面也是和emp_add.html
一样的,把主体部分copy过来,把相应的部分改一下即可。
但默认值怎么设置呢?可能想到了之前dep_update()
时,在前端设置input框的value属性即可完成默认值的显示。
But,ModelForm也提供了相关方法来显示默认值。首先我们根据id获取数据库中相应的那行值(QuerySet类型数据),然后通过ModelForm提供的instance
参数传入数据,ModelForm就会默认把传入的那个对象展示到前端了。
刷新页面,即可看到效果。
接着,再来实现post部分,和之前员工新建时的逻辑一样。
此时,我们来试试功能。
这个修改,完全不符合我们的预期,这个修改更像是添加。在ModelForm中,如果要让save()
不是另外的去创建,而是修改数据表中的某行值,我们还是要在创建EmpUpdateForm
对象时,传入instance
参数,让ModelForm知道,是改哪一行的值。
再来测试
写到这,我们对代码做一些优化。可以发现,get和post都用到了Employee.objects.filter(id=nid).first()
这句代码,所以我们可以提到最前面。
再来看我们创建的两个ModelForm的对象,可以看到他们是一样的代码,所以两个视图函数可以共用一个ModelForm的,只要他们链接的数据表是一个,我们就可以只用一个ModelForm。所以我们就修改一下对象名,供emp_add(request)
和emp_update(request, nid)
调用即可。
P.S. save()方法会把前端用户输入的数据,把偶才能到数据库中。但假设一个场景,前端传入两个数据,数据库一行得要三个参数,剩一个参数是后台给一个默认值和前端的两个数据一起写入数据库。
这时,我们怎么在后端传入数据呢?可以引用 instance参数,手动赋值进去即可(不过这个字段名是必须得在数据表中存在的哦!)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DbPcjwoQ-1645843043686)(Django.assets/image-20220215165928863.png)]
还需要优化的就是,创建时间的格式问题,因为我们只需要显示年月日即可,不需要显示小时分钟秒,所以ModelForm展示的默认数据后面所带的00:00
我们想去掉的话,得改models.py
,原因是数据库表设计的时候不合理,把create_time
的DateTimeField()
改成DateField()
,然后两句指令跑一下,刷新页面即可。
6.3 条件搜索
我们现在想在列表页面上加上一个搜索框的功能。
6.3.1 所用知识点
暂定通过员工姓名来检索结果,可以很容易想到,我们得用models的filter()
来做。
filter()
远比我们之前使用的功能要多,它还支持字典设置条件。
search_dict = {
"name": "zhuming",
"dep": "IT部",
}
models.Employee.objects.filter(**search_dict)
# 等同于
models.Employee.objects.filter("name": "zhuming", "dep": "IT部")
不过,条件不只是要判断=
,大于小于那些也有相应的语法:
# 数字的一些符号
id=12 # id=12的筛选出来
id__gt=12 # id>12的筛选出来
id__gte=12 # id>=12的筛选出来
id__lt=12 # id<12的筛选出来
id__lte=12 # id<=12的筛选出来
search_dict = {
"id__lte": 12,
}
models.Employee.objects.filter(**search_dict)
# 字符串的一些符号
phone = "17732101234" # phone = "17732101234"的筛选出来
phone__startswith="177" # "177"开头的筛选出来
phone__endswith="1234" # "1234"结尾的筛选出来
phone__contains="321" # 包含"321"的筛选出来
search_dict = {
"phone__contains": "12",
}
models.Employee.objects.filter(**search_dict)
6.3.2 实现功能
懂了这些过后,我们开始做这个功能。先完善一下页面,搞一个搜索框出来。
该部分前端代码如下:
<div style="float: right; width: 300px">
<form method="get">
<div class="input-group">
<label for="search"></label>
<input type="text" id="search" name="search" class="form-control" placeholder="请输入员工姓名 支持模糊查询">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
接着完善一下视图函数。
这样就可以了,然后红线划去的部分是order_by("id")
,就是让QuerySet数据按id
升序排列,如果想降序就加一个-
,eg:order_by("-id")
。
不输入,直接搜索。
可以看出,有个缺点,输入框点击搜索后就清空了,用户很容易忘了自己是搜什么的,所以,后端传递参数时,多传一个;前端再设置input框的value值。然后自己测一测即可。
return render(request, "emp_list.html", {"emp_queryset": emp_data, "search_data": search_data})
<input type="text" id="search" name="search" class="form-control" value="{{ search_data }}" placeholder="请输入员工姓名 支持模糊查询">
此时,你会发现,一开始输入框显示None了,所以我们要给他赋值一个空字符串,不能不管(None就是因为不管才出现的),所以完善一下代码:
6.4 分页显示
6.4.1 所用知识点
和切片很像,就是不会越界(如果QuerySet的列表不够10个数据,它不会报错,而是有几个就输出几个)。
# filter和all都可以
Employee.objects.all() # 获取全部数据
Employee.objects.all()[0:10] # 获取前10条数据
我们先增多数据(放进视图函数emp_list()
中,刷新一遍页面就注释掉,只需要增多100个就可以了)
for i in range(100):
Employee.objects.create(name="test"+i, password="12", age=10, account=0, create_time="2000-09-28", gender=1, dep=1)
6.4.2 实现功能
还是和搜索一样,通过get来获取page参数,page若获取不到,就默认是1;然后用切片来
然后我们手动测试,修改网址的page参数,看看是否实现分页显示了。
下面我们就得找一找合适的bootstrap分页标签,然后放在页面中。
<!-- bootstrap 分页 -->
<ul class="pagination">
<li><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li>
<li class="active"><a href="?page=1">1</a></li>
<li><a href="?page=2">2</a></li>
<li><a href="?page=3">3</a></li>
<li><a href="?page=4">4</a></li>
<li><a href="?page=5">5</a></li>
<li><a href="#" aria-label="Next"><span aria-hidden="true">»</span></a></li>
</ul>
这个href
写?page=1
时,点击它就会默认给当前url后面加上?page=1
,然后访问。但是这样太死板了,我们这里不止5页的数据啊,我们得后端传值告诉前端有多少页,完善一下emp_list()
。
但是,这样是不行的,我们得给前端从1
到page_num
的列表,而不是page_num
,不然前端循环没办法写。
最终视图函数的代码是:
前端写一个循环渲染
前端就好了,点击也可以点击进去,但是问题又来了,如果现在是1w条数据,我们这个页码全渲染出来就会很占地方,所以我们还得改。
这个就有很多解决思路了,比如用下拉框来写(移动端用的较多),不过PC端比较常用的方法是显示当前页面的前x
条和后y
条页码。x
和y
是自己决定的。其实啊,不管是显示前后多少页,说到底都是把page_num
这个参数控制好。eg:我们来做x=2
,y=7
的情况。(这里为什么要写x和y而不直接写数据呢?是为了后期改成函数,方便多个视图函数调用)这里的ifelse
判断得慢慢写,还得注意range()
取前不取后的特点。
然后我们写上下页的逻辑
6.4.3 封装调用
因为我们这里要封装views.py
中分页的代码,为了统一,这里给出分页实现代码:
def emp_list(request):
"""员工管理"""
# 搜索
search_filter = {}
search_data = request.GET.get("search")
if search_data:
search_filter["name__contains"] = search_data
else:
search_data = ""
# 直接实现
# 获取get传来的page
page_str = request.GET.get("page")
# 如果用户未输入page参数,默认为1
if page_str:
page = int(request.GET.get("page"))
else:
page = 1
page_max_item = 10 # 一页最多多少行数据
start = (page - 1) * page_max_item # 切片开始位
end = page * page_max_item # 切片结束位
emp_data = Employee.objects.filter(**search_filter).order_by("id")
total = emp_data.count() # 数据总量
emp_data = emp_data[start:end]
# 页码
page_num = int(total / page_max_item)
if total % page_max_item:
page_num += 1
# 前2页,后7页
x = 2
y = 7
if page <= x:
pre_list = range(1, page)
else:
pre_list = range(page-x, page)
if page > page_num-y:
next_list = range(page+1, page_num+1)
else:
next_list = range(page+1, page+y+1)
return render(request, "emp_list.html", {
"emp_queryset": emp_data, "search_data": search_data,
"pre_list": pre_list, "next_list": next_list,
"now_page": page, "pre": page-1, "next": page+1, "page_max": page_num
})
还有一个输入框跳转指定页的功能,我们没有实现,但逻辑和条件搜索的逻辑一模一样。接下来,我们要把这一大堆代码封装,这样我们以后需要实现分页这个功能时,直接调用就行了,不需要再写这么一大堆代码。
我们先创建utils
文件夹(按照规范,我们自己封装的工具一般都要放在utils
文件夹中),再创建py文件封装我们的分页工具类,我们这里命名为pagination.py
。
我们先简单封装一下,设置几个参数。
然后改写views.py
中emp_list()
函数,调用我们的Pagination对象(现成代码在后面)来实现分页。
开始测试,发现报错AttributeError: 'int' object has no attribute 'isdecimal'
,定位报错地点是pagination.py
。经分析,一开始访问emp_list
页面时,now_page
通过get
获取不到当前页,默认为1,是int类型,如果直接进if判断,int类型是没有isdecimal()
的方法的,所以报错。两个方法:
然后页面就可以正常显示了,不过还有问题
这个没关系,我们完全封装好了之后再改。现在要来思考一件事,如何让这个分页组件可以更灵活,比如可以自由选择是否有“首尾页跳转”、是否有“form跳转部分”。而且不知道大家发现了没有,我们为了一个分页传了太多的参数了。
我们换个思路,我们在Pagination对象中,把前端代码写好,然后最终只传一个HTML代码至前端,这样不光可以方便自由选择分页的部分功能,也可以减少往前端传的参数。基于这个思路,我们再继续封装。
# pagination.py
from django.utils.safestring import mark_safe # django为了安全会默认拒绝渲染后端的html代码,所以我们需要mark_safe()给html代码标记为安全,才可以在前端渲染
class Pagination(object):
"""自定义分页组件"""
def __init__(self, request, temp_name_param, queryset, page_form_method="get",
page_size=10, nex=5, prev=5, form_switch=False, first_switch=False,
last_switch=False, a_next=False, a_prev=False):
# form跳转页码的name值
self.temp_name_param = temp_name_param
# 获取当前页
if page_form_method == "post":
# 通过POST获取当前页,默认为1
now_page = request.POST.get(temp_name_param, 1)
else:
# 通过GET获取当前页,默认为1
now_page = request.GET.get(temp_name_param, 1)
# 是否是数字
if now_page == 1:
pass
elif now_page.isdecimal():
now_page = int(now_page)
else:
# 不是数据就默认为1
now_page = 1
# 当前页
self.now_page = now_page
# 每页显示数
self.page_size = page_size
# 数据总量
self.total = queryset.count()
# 切片开始位
self.start = (now_page - 1) * page_size
# 切片结束位
self.end = now_page * page_size
# 对ORM从数据库中拿到的数据进行切片,获得当前页面所对应的数据
self.page_queryset = queryset[self.start:self.end]
# 总页数
self.page_num = int(self.total / self.page_size)
if self.total % self.page_size:
self.page_num += 1
# 显示当前页的前多少页
self.nex = nex
# 显示当前页的后多少页
self.prev = prev
# 组件调用
self.form_switch = form_switch # 是否显示 form跳转页码 组件
self.first_switch = first_switch # 是否显示 首页跳转 组件
self.last_switch = last_switch # 是否显示 尾页跳转 组件
self.a_next = a_next # 是否显示 上一页跳转 组件
self.a_prev = a_prev # 是否显示 下一页跳转 组件
def page_form(self):
""" 添加form部分的html代码
:return: (str)form部分的html代码
"""
return f'''
<li>
<form method="get" style="float: left;">
<label for="page"></label>
<input id="page" name="{self.temp_name_param}"
style="border-radius: 0; float: left; display: inline-block; margin-left: -1px; width: 80px"
type="text" class="form-control" placeholder="页码">
<button type="submit" style="-moz-border-radius-topleft: 0; -moz-border-radius-bottomleft: 0;
margin-left: -5px" class="btn btn-default">跳转</button>
</form>
</li>
'''
@staticmethod
def page_first():
""" 添加首页页码的html代码
:return: (str)html代码
"""
return '<li><a href="?page=1">首页</a></li>'
def page_last(self):
""" 添加尾页页码的html代码
:return: (str)html代码
"""
return f'<li><a href="?page={self.page_num}">尾页</a></li>'
def page_next(self):
""" 添加下一页的html代码
:return: (str)html代码
"""
if self.now_page == self.page_num:
html = '<li class="disabled"><a href="#" aria-label="Next"><span aria-hidden="true">»</span></a></li>'
else:
html = f'<li><a href="?page={self.now_page + 1}" aria-label="Next"><span aria-hidden="true">»</span></a></li>'
return html
def page_prev(self):
if self.now_page == 1:
html = '<li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li>'
else:
html = f'<li><a href="?page={self.now_page - 1}" aria-label="Previous"><span aria-hidden="true">«</span></a></li>'
return html
def basic(self):
""" 生成基础的html代码
:return: (str)html代码
"""
nex_html = ""
if self.now_page <= self.nex:
nex_list = range(1, self.now_page)
else:
nex_list = range(self.now_page - self.nex, self.now_page)
for page in nex_list:
nex_html += f'<li><a href="?page={page}">{page}</a></li>'
now_html = f'<li class="active"><a href="?page={self.now_page}">{self.now_page}</a></li>'
prev_html = ""
if self.page_num - self.now_page <= self.prev:
prev_list = range(self.now_page + 1, self.page_num + 1)
else:
prev_list = range(self.now_page + 1, self.now_page + self.prev + 1)
for page in prev_list:
prev_html += f'<li><a href="?page={page}">{page}</a></li>'
return nex_html + now_html + prev_html
def html(self):
""" 生成全部的html
:return: (str) html代码
"""
html = ""
if self.first_switch:
html += self.page_first()
if self.a_prev:
html += self.page_prev()
html += self.basic()
if self.a_next:
html += self.page_next()
if self.last_switch:
html += self.page_last()
if self.form_switch:
html += self.page_form()
return mark_safe(html)
# views.py
def emp_list(request):
"""员工管理"""
# 搜索
search_filter = {}
search_data = request.GET.get("search")
if search_data:
search_filter["name__contains"] = search_data
else:
search_data = ""
# 搜索条件筛选从数据库拿的数据
emp_data = Employee.objects.filter(**search_filter).order_by("id")
# 分页
# 封装对象实现
from em_web.utils.pagination import Pagination
pagination = Pagination(request, "page", emp_data, page_form_method="get", page_size=10)
return render(request, "emp_list.html", {
"emp_queryset": pagination.page_queryset, "search_data": search_data, "page_html_string": pagination.html()
})
{# emp_list.html部分代码 #}
<ul class="pagination">
{{ page_html_string }}
</ul>
可以自己测试,是符合逻辑的(默认显示前5页和后5页)。
然后我们对参数进行配置,自由搭配组件。
pagination = Pagination(request, "page", emp_data, page_form_method="get", page_size=10, nex=3, prev=4, first_switch=True, last_switch=True, form_switch=True, a_next=True, a_prev=True)
可以看出,原本views.py
中大段的代码,已经被我们封装至utils
中了,views.py
一下变得清爽了许多,而且以后我们需要实现分页的效果时,直接创建对象调用方法即可,还能应对各种场景下的需求,对组件进行增删。
But,如果你的页面中其他地方没有form表单,那么这个Pagination就够用了,不幸的是,我们这个页面有其他的form表单,就是右上角的搜索框,如果你先搜索一个值,在点击分页的按钮,我们会发现,原本的搜索条件失效了。也就是说,如果我们想解决这个Bug,我们就得把除开分页的form表单的条件数据都带着,然后一起去跳转其他分页。
# 我们原来是这样跳转
127.0.0.1:8000?page=1 -> 127.0.0.1:8000?page=3
# 但如果有其他的form表单的条件数据,就产生了Bug
127.0.0.1:8000?search=zm&page=1 -> 127.0.0.1:8000?page=3 # 错误!
# 按照正确的逻辑,应该是这样(所以一定要带着其他form表单的数据一起跳转其他分页)
127.0.0.1:8000?search=zm&page=1 -> 127.0.0.1:8000?search=zm&page=3 # 正确!
这里要解决这个问题,我们先说一个知识点:
# 加到视图函数中,然后访问相应的页面
print(request.GET) # 会获取所有的get传来的数据
print(request.GET.urlencode()) # 将get获取到的数据再转化成url中的格式
我们访问http://127.0.0.1:8000/emp_list/?search=z&page=2
可以看到,request.GET
返回的是一个QueryDict的一个数据类型,这个数据类型是不允许手动增加参数的。但是通过阅读完源码后,我们可以修改默认参数来实现允许手动增加参数。But,如果直接修改默认参数的话,不是很安全,所以我们copy一份,再修改它的默认参数,因为我们只需要让跳转链接里有数据即可。
再访问上面的链接,会发现输出会不同。
通过这个知识点,我们就可以来完善Pagination了。
最终,我们测试一下效果,都是可以的。不过,可能细心的你发现,form页码跳转还是不会携带search的参数。
这个问题呢,这里无法避免。如果要实现的话,得用Ajax或者js来做,如果不怕麻烦的话,可以手动写input框,设置type
为hidden
,然后添加进后端的html代码中,从而实现某个参数的携带。所以呢,这里就不考虑这个问题了。
好的,那么接下来,我们再实现部门管理的分页显示。
# views.py
def dep_list(request):
"""部门列表"""
# 获取数据库中数据
all_dep_data = Department.objects.all() # QuerySet(<对象>, <对象>, ...)
pagination = Pagination(request, "page", all_dep_data, page_form_method="get", page_size=2, nex=3, prev=4,
first_switch=True, last_switch=True, form_switch=True, a_next=True, a_prev=True)
return render(request, "dep_list.html", {
"dep_queryset": pagination.page_queryset,
"page_html_string": pagination.html()
})
{# dep_list.html #}
<ul class="pagination">
{{ page_html_string }}
</ul>
很方便吧。
6.5 时间选择组件
这里我们调用的是bootstrap-datepicker插件,调用的语法如下:
<link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1-dist/css/bootstrap.css' %}">
<!-- datepicker必须提前引入bootstrap的css -->
<link rel="stylesheet" href="{% static 'plugins/datepicker.css' %}">
<!-- 后面会利用id来写js -->
<input id="dt" type="text" class="form-control" placehorder="入职时间">
<script rel="script" src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
<script rel="script" src="{% static 'plugins/bootstrap-3.4.1-dist/js/bootstrap.js' %}"></script>
<!-- datepicker必须提前引入JQuery和bootstrap的js -->
<script rel="script" src="{% static 'plugins/bootstrap-datepicker.js' %}"></script>
<!-- 调用datepicker设置显示格式 -->
<script>
$(function () {
$('#dt').datepicker({
format: 'yyyy-mm-dd',
startDate: '0',
language: 'zh-CN',
autoclose: true,
});
})
</script>
我们用这个插件来完善一下我们的emp_add.html
,因为我们是用ModelForm来生成的页面,所以我们怎么通过调用input框的id来实现插件呢?
其实,我们右击网页点检查,会发现,ModelForm在帮我们渲染前端输入框时,默认都会有一个id(命名格式:id_+字段名
),所以我们直接用即可。
6.6 ModelForm样式优化
前面我们说ModelForm说过,ModelForm渲染在前端的输入框没有任何样式,需要我们通过widget来设置其属性及属性值。
然后我们又介绍了快速给所有输入框上样式的方法,通过__init__()
去实现。
但是,现在我们再看这个方法,有些不严谨。如果输入框原本有一些样式的话,通过这段代码,不管你原本有无样式,field.widget.attrs
都会给你定死成"class": "form-control", "placeholder": field.label
。
所以我们要来完善一下逻辑。
But,如果以后我们用到ModelForm的话,我们这段代码得重复写。所以我们还可以将这个__init__()
方法写到utils中去。
然后我们在views.py
中需要写ModelForm的时候,就不去继承forms.ModelForm
了,而是去继承em_web.utils.bootstrap
中的BootstrapModelForm了。
这样,我们以后就不需要每次都继承__init__()
方法去上样式了。
结束,撒花>▽<!如果觉得有帮助,欢迎点赞打赏。
原文地址:https://blog.csdn.net/Ans_min/article/details/123146335
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_13219.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!