代码实现详解
本章中我将会介绍templates和view下的文件和函数。
templates目录下都是html模板文件,内容如下
├── index.html
├── layout.html
└── login
├── change_password.html
├── login.html
├── role_add.html
├── role_edit.html
├── role_management.html
├── user_add.html
└── user_management.html
views目录下只有两个文件,其中__init__.py是空文件,login.py是主要文件
├──
init
.py
├── login.py
因为这些文件都是互相调用和指向,所以我会从功能的实现来解读
用户登录
我们通过判断session中是否存在 username来检查用户是否登录,所以在login.py中有如下的修饰函数
def login_required(view):
"""View decorator that redirects anonymous users to the login page."""
@functools.wraps(view)
def wrapped_view(**kwargs):
if session.get("username") == None:
return redirect(url_for('login.login'))
return view(**kwargs)
return wrapped_view
这个函数如果检测到session中不存在username,则会转向login.py文件中的login函数
@bp.route('/', methods=['GET', 'POST'])
def login():
if request.method == 'POST' :
username = request.form['username']
password = hashlib.sha224(request.form['password'].encode()).hexdigest()
user_info = USERS.query.filter_by(username=username,password=password).all()
if len(user_info)==0:
flash ("Wrong Username or Password")
return render_template('login/login.html')
else:
userrole = user_info[0].userrole
session['username']=username
session['userrole']=userrole
return redirect(url_for('login.success'))
return render_template('login/login.html')
因为这个转页不是post方法,所以login函数则会直接弹出templates目录下login.html文件让用户输入用户名和密码。
<form class="form-signin" method="POST" action="{{url_for('login.login') }}">
{% if get_flashed_messages() %}
<h1 class="h4 mb-3 font-weight-normal">{{ get_flashed_messages()[0] }}</h1>
{% endif %}
<h1 class="h4 mb-3 font-weight-normal">Please sign in</h1>
<label for="username" class="sr-only">Username</label>
<input type="text" name="username" id="username" class="form-control" placeholder="Username" required autofocus>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
这个文件中的将会把username和password传给上面login函数,因为这一次是post的方法,所以login函数会去验证user表中是否有相同的用户名和密码。
- 如果用户名密码正确,就把username和userrole加到session中,并转入login.success函数
-
如果错误,则会转回登录界面,并提示。
其中密码我们使用了sha224加密,这也是为什么我们设置密码为Password123!但是插入数据库的时候是一串很长字符的原因。
而login.success函数,则主要做如下事情:
- 从menus获取所有的菜单信息,包括menuid,menuname和parentmenu
- 从rolemapping表中获得用户角色对应的所有menuid
- 将这些信息存入session,然后渲染index.html页面
@bp.route('/success')
@login_required
def success():
roleid=session['userrole']
all_menus = MENUS.query.order_by(MENUS.menuorder).all()
menus=[]
for menu in all_menus:
menu_detail=[menu.menuid,menu.menuname,menu.parentmenu]
menus.append(menu_detail)
session['menus']=menus
mappings = ROLEMAPPINGS.query.filter_by(roleid=roleid).all()
mapping_menus=[]
for mapping in mappings:
mapping_menus.append(mapping.menuid)
session['mapping_menus']=mapping_menus
return render_template('index.html')
index.html就是登陆成功以后的主页面,包括下拉菜单和主要内容,因为这个教程主要是讲怎么实现基于角色的菜单控制,所以主要内容基本为空。
{% extends 'layout.html' %}
{% block content %}
<div class="col-xl-9">
<div class="mb-3">
<h3 class="display-8 card p-3">Welcome {{session['username']}}</h3>
</div>
</div>
{%endblock%}
而第一行的layout.html,才是我们下拉菜单需要的主要文件。下拉菜单的实现是使用了Bootstrap的navbar。
其实现模式可以参考https://getbootstrap.com/docs/4.4/components/dropdowns/
一个最简单的下拉菜单如下
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Dropdown button
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</div>
我的代码中只是在这个菜单中增加了部分内容,如果想要详细了解,可以参考我的代码和官方链接做对比。
我这里主要是讲述如何实现的动态菜单内容
- 用户名下的下拉菜单,针对用户角色是admin的,我们将会显示所有菜单,而其他用户则只显示修改密码以及退出
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{%if session['userole']==1%}
<a class="dropdown-item" href="{{url_for('login.usermanagement')}}">User Management</a>
<a class="dropdown-item" href="{{url_for('login.rolemanagement')}}">Role Management</a>
{%endif%}
<a class="dropdown-item" href="{{url_for('login.passwordmanagement')}}">Password Management</a>
<a class="dropdown-item" href="{{url_for('login.logout')}}">Logout</a>
</div>
-
功能菜单,由于我们已经在session中保存了所有菜单的相关信息,以及对应角色的所有菜单id,所以我们第一步将会查询该用户所有的parentmenu,然后查询该用户对应角色在parentmenu下面的menu。
我们认为在所有菜单中,如果该菜单没有parentmenu,则其本身就是parentmenu。
{%for parentmenu in session['menus']%}
{%if not parentmenu[2]%}
{%if parentmenu[0] in session['mapping_menus']%}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownParent" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{parentmenu[1]}}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{%for submenu in session['menus']%}
{%if submenu[2]|int==parentmenu[0]%}
{%if submenu[0] in session['mapping_menus']%}
<a class="dropdown-item" href="{{submenu[3]}}">{{submenu[1]}}</a>
{%endif%}
{%endif%}
{%endfor%}
</div>
</li>
{%endif%}
{%endif%}
{%endfor%}
用户退出
当用户点击logout的时候,我们将session中的用户置为false,并重新定位到login页面, logout对应的html代码
<a class="dropdown-item" href="{{url_for('login.logout')}}">Logout</a>
对应的python代码
def logout():
session['username']=False
return redirect(url_for('login.login'))
修改密码
修改密码的时候主要是需要验证两次输入的密码是一样的,所以在html部分包括了jquery, html form如下,有两个输入框password和confirmpassword
<form class="card p-3 mb-5" action="{{ url_for('login.passwordmanagement')}}" id="passwordmanagement" method="post">
<div class="mb-3 col-xl-6 form-group">
<input type="password" class="form-control" id="password" name="password" placeholder="newpassword" value="" required>
</div>
<div class="mb-3 col-xl-6 form-group">
<input type="password" class="form-control" id="confirmpassword" name="confirmpassword" placeholder="confirmpassword" value="" required>
</div>
<div class="mb-3 form-group" align="right">
<button class="btn btn-primary" id="btnSavePasswd" type="button">Save</button>
</div>
</form>
Jquery部分主要是确认在提交按钮输入后,确认两个密码一致,如果不一致,则提示密码不一致
<script>
$('#btnSavePasswd').on('click',function(){
var form_id = $(this).closest('form').attr("id");
var values={};
$.each($('#'+form_id).serializeArray(), function(i, field) {
values[field.name] = field.value;
});
var password= values["password"];
var confirmpassword=values["confirmpassword"];
if (password != confirmpassword) {
alert("New Password Not Match");
return false;
} else {
$(this).closest('form').submit();
return true;
}
});
</script>
添加角色
我们会首先在html页面展示一个输入角色名字的输入框,然后展示所有可供这个角色使用的菜单。
Html的form
<form class="card p-3 mb-5" action="{{ url_for('login.role_add')}}" method="post" id="roleAdd" name="roleAdd">
<div class="mb-3">
<div>
<label for="rolename" class="form-inline">Role Name: <input type="text" class="form-control" id="rolename" name="rolename" required></label>
</div>
</div>
{% for menu in menus %}
<div class="mb-3">
{%if not menu.parentmenu%}
<input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
{%set parent_menu_id=menu.menuid%}
{% for menu in menus %}
{%if menu.parentmenu|int==parent_menu_id%}
 <input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
{%endif%}
{%endfor%}
{%endif%}
</div>
{%endfor%}
<div align="right">
<button class="btn btn-primary btn-save" type="submit" id="btnSaveRole">Save</button>
</div>
</form>
而form中的menus的来源于
def add_new_role():
if request.method == 'GET':
menus = MENUS.query.order_by(MENUS.menuorder).all()
return render_template('/login/role_add.html',menus=menus)
当用户输入了角色名字选择了menu以后,提交给python脚本处理,脚本会将角色和角色菜单映射添加到对应的表中去。
def role_add():
if request.method == 'POST':
rolename = request.form['rolename']
rolemenuidlist = request.form.getlist('rolemenu')
rolemenuidlist = [int(i) for i in rolemenuidlist]
newrole=ROLES(rolename=rolename)
dbusers.session.add(newrole)
dbusers.session.commit()
newroleid=ROLES.query.filter_by(rolename=rolename).first().role_id
for rolemenuid in rolemenuidlist:
new_role_mapping = ROLEMAPPINGS(roleid=newroleid, menuid=rolemenuid)
dbusers.session.add(new_role_mapping)
dbusers.session.commit()
return redirect(url_for('login.rolemanagement'))
修改角色
有时候可能觉得角色不需要了,或者觉得给这个角色的菜单需要改动,就调用修改角色的功能,我们需要先列出所有菜单,然后标记该角色对应的所有菜单,从而方便进行修改。
所以渲染html前,我们需要查询所有菜单,角色信息,以及角色和菜单映射
def edit_role():
if request.method == 'GET':
menus = MENUS.query.order_by(MENUS.menuorder).all()
roleid=request.args['roleid']
roleinfo=ROLES.query.filter_by(roleid=roleid).first()
mappings=ROLEMAPPINGS.query.filter_by(roleid=roleid).all()
return render_template('/login/role_edit.html',menus=menus,roleinfo=roleinfo,mappings=mappings)
接下来用这些数据渲染html
<form class="card p-3 mb-5" action="{{ url_for('login.role_edit')}}" method="post" id="roleEdit"
name="roleAdd">
<div class="mb-3">
<div>
<label class="form-inline">Role Name: {{roleinfo.rolename}} </label>
<input type="hidden" name="roleid" value="{{roleinfo.roleid}}">
</div>
</div>
{% for menu in menus %}
<div class="mb-3">
{%if not menu.parentmenu%}
{%set mybool = [False]%}
{%for mapping in mappings%}
{%if menu.menuid==mapping.menuid %}
{%set _ = mybool.append(not mybool.pop())%}
{%endif%}
{%endfor%}
{%if mybool[0] %}
<input type="checkbox" name="rolemenu" value="{{menu.menuid}}" checked>{{menu.menuname}}<br>
{%else%}
<input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
{%endif%}
{%set parent_menu_id=menu.menuid%}
{% for menu in menus %}
{%if menu.parentmenu|int==parent_menu_id%}
{%set mybool = [False]%}
{%for mapping in mappings%}
{%if menu.menuid==mapping.menuid %}
{%set _ = mybool.append(not mybool.pop())%}
{%endif%}
{%endfor%}
{%if mybool[0] %}
 <input type="checkbox" name="rolemenu" value="{{menu.menuid}}" checked>{{menu.menuname}}<br>
{%else%}
 <input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
{%endif%}
{%endif%}
{%endfor%}
{%endif%}
</div>
{%endfor%}
<div align="right">
<button class="btn btn-primary btn-save" type="submit" id="btnSaveRole">Save</button>
</div>
</form>
这段数据被提交后,python将会清空角色菜单映射表中和该角色相关的所有数据,然后重新插入新的菜单数据
def role_edit():
if request.method == 'POST':
roleid = request.form['roleid']
rolemenuidlist = request.form.getlist('rolemenu')
rolemenuidlist = [int(i) for i in rolemenuidlist]
#Remove the existing menus
existingmenus = ROLEMAPPINGS.query.filter_by(roleid=roleid).all()
for existingmenu in existingmenus:
dbusers.session.delete(existingmenu)
dbusers.session.commit()
#Add new menus
for rolemenuid in rolemenuidlist:
new_role_mapping = ROLEMAPPINGS(roleid=roleid, menuid=rolemenuid)
dbusers.session.add(new_role_mapping)
dbusers.session.commit()
return redirect(url_for('login.rolemanagement'))
添加用户
添加用户也比较简单,主要是让输入用户名和密码,并选择角色。
<form class="card p-3 mb-5" action="{{ url_for('login.add_new_user')}}" id="add_new_user"
method="post">
<div class="row form-group">
<div class="mb-3 col-xl-3 form-group">
<input type="text" class="form-control" id="username" name="username" placeholder="username"
value=""
required>
</div>
<div class="mb-3 col-xl-3 form-group">
<input type="password" class="form-control" id="password" name="password" placeholder="password"
value=""
required>
</div>
<div class="mb-3 col-xl-3 form-group">
<select id="userrole" name="userrole">
{%for userrole in userroles%}
<option value="{{userrole.roleid}}">{{userrole.rolename}}
</option>
{%endfor%}
</select>
</div>
<div class="mb-3 form-group" align="right">
<button class="btn btn-primary" id="btnSaveIssue" type="submit">Save</button>
</div>
</div>
</form>
其中userroles的数据是在login脚本中提供,可参加其他类似的功能。
Python收到添加用户的请求后,会把对应的数据加入数据库,当然,此前会检查这个用户是不是存在,同时也会将密码加密。
def add_new_user():
if request.method == 'GET':
userroles = ROLES.query.all()
return render_template('/login/user_add.html',userroles=userroles)
elif request.method == 'POST':
username = request.form['username'].lower()
existing_user=USERS.query.filter_by(username=username).first()
if existing_user:
flash ("User already exist")
userroles = ROLES.query.all()
return render_template('/login/user_add.html', userroles=userroles)
else:
password = hashlib.sha224(request.form['password'].encode()).hexdigest()
userrole = request.form['userrole']
new_user=USERS(username=username,password=password,userrole=userrole)
dbusers.session.add(new_user)
dbusers.session.commit()
return redirect(url_for('login.usermanagement'))
修改用户角色
列出所有的用户和所有的用户角色,并让管理员进行修改, html被渲染过的模板
{% for user in users %}
<form action="{{ url_for('login.usermanagement')}}" method="post" id="form{{user.userid}}">
<table class="table">
<tr>
<input type="hidden" name="userid" value="{{user.userid}}"/>
<td width="10%">{{user.username}}</td>
<td width="10%">
<select disabled="disabled" id="userrole" name="userrole">
{%for userrole in userroles%}
<option value="{{userrole.roleid}}" {%if user.userrole==userrole.roleid%} selected {%endif%}>{{userrole.rolename}}
</option>
{%endfor%}
</select>
</td>
<td width="20%">
<button class="btn btn-primary btn-edit" type="button" id="btnEditUser">Edit</button>
<a class="btn btn-primary" id="btnDelUser"
href="{{ url_for('login.delete_user',userid=user.userid) }}"
style="color: white"
onclick="{if(confirm('Are you sure?')){this.document.formname.submit();return true;}return false;}">Delete</a>
<button class="btn btn-primary d-none btn-save" type="button" id="btnSaveUser">Save
</button>
</td>
<td> </td>
</tr>
</table>
</form>
{% endfor %}
脚本收到数据后,则直接更新user表
def usermanagement():
if request.method=="POST":
userid=request.form['userid']
userrole=request.form['userrole']
userinfo=USERS.query.filter_by(userid=userid)
userinfo[0].userrole=userrole
dbusers.session.commit()
users=USERS.query.all()
userroles=ROLES.query.all()
return render_template('login/user_management.html',users=users,userroles=userroles)
至此,本功能全部介绍完毕,欢迎批评和指正。