一、获取表单提交的数据
在 [Python自学] day-18 (2) (MTV架构、Django框架) 中,我们使用过以下方式来获取表单数据:
user = request.POST.get('username', None)
这种获取方式可以获取来自表单的单个数据,例如<input type='text'/>的数据。
除了以上这种最简单的数据获取方式,我们还需要获取例如<input type='checkbox' />、<input type='file' />、<select>等标签的数据:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>MyPage</title> <style> p{ border: 1px solid #dddddd; display: inline-block; } </style> </head> <body> <form action="/mypage" method="post" enctype="multipart/form-data"> <!-- radio单选 --> <p>性别:</p> <div> 男:<input type="radio" name="gender" value="1"/> 女:<input type="radio" name="gender" value="2"/> </div> <!-- checkbox多选 --> <p>喜好:</p> <div> 足球:<input type="checkbox" name="favor" value="11"/> 篮球:<input type="checkbox" name="favor" value="22"/> 游泳:<input type="checkbox" name="favor" value="33"/> </div> <!-- 单选select --> <p>来自哪个城市:</p> <div> <select name="city"> <option value="cd">成都</option> <option value="bj">北京</option> <option value="sh">上海</option> </select> </div> <!-- 多选select --> <p>喜欢哪些城市:</p> <div> <select name="favorcity" multiple> <option value="cd">成都</option> <option value="bj">北京</option> <option value="sh">上海</option> </select> </div> <!-- 上传文件 --> <div> <input type="file" name="filetrans"/> </div> <div style="height: 48px;line-height: 48px;"> <input type="submit" value="提交"/> </div> </form> </body>
在views.py中,我们可以通过以下方式来获取对应的数据:
def mypage(request): if request.method == 'POST': print(request.POST.get('gender', None)) # 获取radio单选数据,打印单个数据,例如'2'表示"女" print(request.POST.getlist('favor', None)) # 获取checkbox的多选数据,打印value组成的列表 ['11','22'] print(request.POST.get('city', None)) # 获取select的单选数据,打印单个数据,例如'cd'表示"成都" print(request.POST.getlist('favorcity', None)) # 获取multiple select标签的多选数据,打印列表['cd','sh'] # 获取文件对象 recv_file = request.FILES.get('filetrans', None) print(recv_file.name) # 从obj.chunks()中循环获取文件的块,并写入同名文件 with open(os.path.join('upload', recv_file.name), 'wb') as f: for i in recv_file.chunks(): f.write(i) return render(request, 'mypage.html')
特别注意:在上传文件的时候,<form>表单必须要有 enctype="multipart/form-data" 属性,否则会将文件当做字符串提交(也就是说后台只能收到文件的名称)。
二、FBV和CBV
FBV:Function base view,基于函数的视图。
CBV:Class base view,基于类的视图。
1.FBV
在之前的章节中,我们在APP的views.py中写了很多请求处理函数(视图函数),使用函数来处理请求,就叫做FBV。
2.CBV
如果我们使用一个类来处理一个URL,则称为CBV,例如在APP的views.py中定义一个处理类:
from django.views import View # 处理类必须继承自View类 class MyPage(View): # get方法专门处理GET请求 def get(self, request): print(request.method) return render(request, 'mypage.html') # post方法专门处理POST请求 def post(self, request): print(request.method) return render(request, 'mypage.html')
我们查看View类的源码,可以看到:
class View: """ Intentionally simple parent class for all views. Only implements dispatch-by-method and simple sanity checking. """ http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] ...... ......
我们可以定义 http_method_names 列表中所列出的所有请求类型对应的方法。
3.CBV中的执行过程
我们查看View父类的源码:
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)
我们可以看到使用反射的时候,参数中将请求方式转化为小写。所以,我们实现的方法名必须是小写的。例如get()、post()、put()。如果请求的方法不在允许的列表中,则返回405错误(参考源码中的 self.http_method_not_allowed方法)。
4.重写父类中的dispatch方法,实现自定义功能
# 处理类必须继承自View类 class MyPage(View): # 重写父类方法 def dispatch(self, request, *args, **kwargs): print('before') result = super(MyPage, self).dispatch(request, *args, **kwargs) print('after') return result # get方法专门处理GET请求 def get(self, request): print(request.method) return render(request, 'mypage.html') # post方法专门处理POST请求 def post(self, request): obj = request.FILES.get('filetrans') if obj is not None: with open(os.path.join('upload',obj.name),'wb') as f: for item in obj.chunks(): f.write(item) return render(request, 'mypage.html')
在调用父类dispatch方法前后可以实现自定义功能。
总结:FBV和CBV用哪个更好?
FBV和CBV没有哪个好,哪个不好。在生产中都可以使用。
三、实现详情页面(动态url)
在后台管理页面中,我们经常看到一个列表(例如用户列表),点击其中一条,可以跳转到详情页面。
1.用户列表页面html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>UserList</title> </head> <body> <ul> {% for key,value in user_dict.items %} <li><a href="/details/?nid={{ key }}"> {{ value.name }} </a></li> {% endfor %} </ul> </body> </html>
2.添加urls.py映射关系
from cmdb import views urlpatterns = [ path('admin/', admin.site.urls), path('login', views.login), path('home', views.home), # 后台管理页面映射,映射到cmdb.views.home方法 path('mypage', views.MyPage.as_view()), path('users', views.user_page), ]
3.用户列表视图函数
USER_DICT = { '1': {'name': 'Alex', 'email': 'Alex@163.com'}, '2': {'name': 'Jone', 'email': 'Jone@163.com'}, '3': {'name': 'Leo', 'email': 'Leo@163.com'}, '4': {'name': 'Eric', 'email': 'Eric@163.com'} } # /users页面的视图函数 def user_page(request): return render(request, 'users.html', {'user_dict': USER_DICT})
4.用户列表页面实现效果:
5.用户详情页面html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Details</title> </head> <body> <h1>详细信息</h1> <h6>用户名: {{ detail_info.name }}</h6> <h6>邮箱: {{ detail_info.email }}</h6> </body> </html>
6.添加urls.py中的映射关系
from cmdb import views urlpatterns = [ path('admin/', admin.site.urls), path('login', views.login), path('home', views.home), # 后台管理页面映射,映射到cmdb.views.home方法 path('mypage', views.MyPage.as_view()), path('users', views.user_page), path('details/', views.details), ]
7.用户详情视图函数
# /details页面的视图函数 def details(request): detail_info = {} # 如果从GET数据中获取到nid,则取相应用户的详情 if request.method == 'GET': nid = request.GET.get('nid') detail_info = USER_DICT[nid] return render(request, 'details.html', {'detail_info': detail_info})
8.详情页面实现效果
四、伪静态URL方式实现详情页面
在第三节中,我们使用了"detail/?nid=3"这种形式的参数传递方式,可以从GET中获取相应的数据。但这种形式的URL为动态URL,在SEO中权重很低。
所以目前比较流行的做法是,使用"detail-3.html"这种方式才传递参数"3"。这种方式被SEO看做是静态URL,具有比较高的排名权重。
1.修改users.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>UserList</title> </head> <body> <ul> {% for key,value in user_dict.items %} <li><a href="/details-{{ key }}.html"> {{ value.name }} </a></li> {% endfor %} </ul> </body> </html>
2.修改urls.py映射
from django.contrib import admin from django.urls import path from django.urls import re_path from cmdb import views urlpatterns = [ path('admin/', admin.site.urls), path('login', views.login), path('home', views.home), # 后台管理页面映射,映射到cmdb.views.home方法 path('mypage', views.MyPage.as_view()), path('users', views.user_page), re_path('details-(d+).html', views.details), ]
这里要使用正则表达式(导入re_path模块)来进行映射匹配,Django会自动将"()"中匹配到的字符串作为参数传递给views.details()。
3.修改视图函数
# /details页面的视图函数 def details(request, nid): # 取相应用户的详情 detail_info = USER_DICT[nid] return render(request, 'details.html', {'detail_info': detail_info})
4.伪静态URL详情页面实现效果
五、基于正则的URL
前面第四节我们已经使用了基于正则的URL映射。如下代码所示:
from django.contrib import admin from django.urls import path from django.urls import re_path from cmdb import views urlpatterns = [ path('admin/', admin.site.urls), path('login', views.login), path('home', views.home), # 后台管理页面映射,映射到cmdb.views.home方法 path('mypage', views.MyPage.as_view()), path('users', views.user_page), re_path('details-(d+).html', views.details), ]
要使用正则,必须使用re_path模块。
如果正则中有两个分组:
re_path('details-(d+)-(d+).html', views.details)
那么对应视图函数就应该是:
def details(request, param1, param2): pass
正则表达式中匹配到的数据会按顺序传递给details()函数。
我们可以使用以下方式,让其指定传递的参数名:(推荐使用)
re_path('details-(?P<nid>d+)-(?P<uid>d+).html', views.details)
"<>"中的参数名就表示正则表达式匹配到的数据指定传递给哪个参数,所以我们的视图函数参数顺序可以任意:
def details(request, nid, uid): pass def details(request, uid, nid): pass
参数的顺序变了,但值都能传递正确。
当我们不确定参数个数,或者为了方便,可以将视图函数写成:
def details(request, *args, **kwargs): pass
当我们使用前面那种按顺序传递参数的方式,参数就会被传递到"*args"中(元组)。
当后者按名称传递的方式,参数就会被传递到"**kwargs"中(字典)。
六、修改URL的便捷方式({% url 'url_name' %})
当我们对urls.py中的某个映射进行修改时,使用这个URL的地方也要修改:
例如修改urls.py中的其中一条映射:
urlpatterns = [ path('admin/', admin.site.urls), path('login', views.login), path('home', views.home), # path('mypage', views.MyPage.as_view()), path('mypage12376sjhdfjnwjer', views.MyPage.as_view()), path('users', views.user_page), re_path('details-(d+).html', views.details), ]
我们此时需要修改html中表单提交的目的地址:
<form action="/mypage12376sjhdfjnwjer" method="post" enctype="multipart/form-data">
Django为我们提供了一种便捷的方式(其他框架可能没有):
我们在定义urls.py中的映射关系时,不管匹配字符串是什么我们都可以为其定义一个"name":
urlpatterns = [ path('admin/', admin.site.urls), path('login', views.login), path('home', views.home), # path('mypage', views.MyPage.as_view()), path('mypage12376sjhdfjnwjer', views.MyPage.as_view(), name='mypage'), path('users', views.user_page), re_path('details-(d+).html', views.details), ]
在html中,我们就不需要写匹配字符串了,而是如下:
<form action="{% url 'mypage' %}" method="post" enctype="multipart/form-data">
此时,访问对应的页面,可以正常提交:
Django提供name的目的:
让我们可以根据name来构建我们需要的URL,例如在上面的例子中,name="mypage"代表着URL "/mypage12376sjhdfjnwjer"。。
当我们的表单提交目的URL为这个URL时,我们可以直接使用{% url 'mypage' %}。
思考一个场景,当我们处于一个分页页面,例如第10页,该页面对应的URL映射为:
re_path('mypage12376sjhdfjnwjer/(d+)', views.MyPage.as_view(), name='mypage')
我们处在的页面URL为:http://127.0.0.1/mypage12376sjhdfjnwjer/10/
此时,假设该页面右上角有登录按钮,我们点击进行登录。页面会进行跳转,我们可以把{% url 'mypage' 10 %}串到登录按钮的href中。例如"/login/mypage12376sjhdfjnwjer/10/".
这样,/login页面可以拿到我们点击登录时正处于的页面,在登录完成后,页面还可以跳转回之前浏览的页面。
从上面可以看出,当我们的URL是正则匹配时,我们可以使用传参的方式生成想要的URL:
re_path('mypage12376sjhdfjnwjer/(d+)/', views.MyPage.as_view(), name='mypage') # 对应{% url 'mypage' 10 %}
re_path('mypage12376sjhdfjnwjer/(d+)/(d+)/', views.MyPage.as_view(), name='mypage') # 对应{% url 'mypage' 10 13 %}
re_path('mypage12376sjhdfjnwjer/(?P<nid>d+)/', views.MyPage.as_view(), name='mypage') # 对应{% url 'mypage' nid=10 %}
re_path('mypage12376sjhdfjnwjer/(?P<nid>d+)/(?P<uid>d+)/', views.MyPage.as_view(), name='mypage') # 对应{% url 'mypage' nid=10 uid=13 %}
当然,我们也可以在登录页面的视图函数中直接生成:
# 登录操作,登录完毕后跳转到某页 def login(request, *args, **kwargs): # 登录操作 # todo.. # 直接字符串拼接 url = '/mypage12376sjhdfjnwjer/10/13/' # 使用django提供的函数实现拼接 from django.urls import reverse url = reverse('mypage', args=(10,)) # 对应正则:mypage12376sjhdfjnwjer/(d+)/ url = reverse('mypage', args=(10, 13,)) # 对应正则:mypage12376sjhdfjnwjer/(d+)/(d+)/ url = reverse('mypage', kwargs={'nid': 10}) # 对应正则:mypage12376sjhdfjnwjer/(?P<nid>d+)/ url = reverse('mypage', kwargs={'nid': 10, 'uid': 13}) # 对应正则:mypage12376sjhdfjnwjer/(?P<nid>d+)/(?P<uid>d+)/ # 登录完毕后跳转回某页 return redirect(url)
七、路由分发
目前,我们只有一个urls.py,位于Django工程目录。我们所有的APP的映射都写在一起,显得比较杂乱,也不便于多人协作。
我们可以使用Django提供的路由分发功能。
首先,我们在最上层urls.py(工程目录中的urls.py)中,进行修改:
from django.contrib import admin from django.urls import path from django.urls import include
urlpatterns = [ path('cmdb/', include("cmdb.urls")), path('mgmt/', include("mgmt.urls")), ]
这个最上层urls.py会将所有http://127.0.0.1:8000/cmdb/xxx的URL分发给APP cmdb的urls.py进行处理。
将所有http://127.0.0.0:8000/mgmt/xxx的URL分发给APP mgmt的urls.py处理。
我们分别在两个APP文件夹中创建urls.py:
分别在cmdb和mgmt的urls.py写各自的路由映射,例如在cmdb的urls.py中有如下映射:
from django.contrib import admin from django.urls import path from django.urls import re_path from cmdb import views urlpatterns = [ path('admin/', admin.site.urls), path('login', views.login), path('home', views.home), # path('mypage', views.MyPage.as_view()), path('mypage12376sjhdfjnwjer', views.MyPage.as_view(), name='mypage'), path('users', views.user_page), re_path('details-(d+).html', views.details), ]
那么访问 "mypage12376sjhdfjnwjer"这个页面的话,要使用 http://127.0.0.1:8000/cmdb/mypage12376sjhdfjnwjer:
这样就完成了按APP进行路由分发的功能。