forked from mrlan/EnglishPal
				
			feat: admin can manage articles and users without interfering with each other
							parent
							
								
									3e35679a91
								
							
						
					
					
						commit
						2cf65123e9
					
				|  | @ -1,9 +1,10 @@ | |||
| # System Library | ||||
| from flask import * | ||||
| from model import * | ||||
| from pony.orm import * | ||||
| 
 | ||||
| # Personal library | ||||
| from Yaml import yml | ||||
| from Login import md5 | ||||
| from datetime import datetime | ||||
| from model.user import * | ||||
| from model.article import * | ||||
| 
 | ||||
| ADMIN_NAME = "lanhui"  # unique admin name | ||||
| _cur_page = 1  # current article page | ||||
|  | @ -11,33 +12,54 @@ _page_size = 5  # article sizes per page | |||
| adminService = Blueprint("admin_service", __name__) | ||||
| 
 | ||||
| 
 | ||||
| @adminService.route("/admin", methods=["GET", "POST"]) | ||||
| def admin(): | ||||
|     global _cur_page, _page_size | ||||
| def check_is_admin(): | ||||
|     # 未登录,跳转到未登录界面 | ||||
|     if not session.get("logged_in"): | ||||
|         return render_template("not_login.html") | ||||
| 
 | ||||
|     # 获取session里的用户名 | ||||
|     username = session.get("username") | ||||
|     if username != ADMIN_NAME: | ||||
|     # 用户名不是admin_name | ||||
|     if session.get("username") != ADMIN_NAME: | ||||
|         return "You are not admin!" | ||||
| 
 | ||||
|     return "pass" | ||||
| 
 | ||||
| 
 | ||||
| @adminService.route("/admin", methods=["GET"]) | ||||
| def admin(): | ||||
|     is_admin = check_is_admin() | ||||
|     if is_admin != "pass": | ||||
|         return is_admin | ||||
| 
 | ||||
|     return render_template( | ||||
|         "admin_index.html", yml=yml, username=session.get("username") | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @adminService.route("/admin/article", methods=["GET", "POST"]) | ||||
| def article(): | ||||
|     global _cur_page, _page_size | ||||
| 
 | ||||
|     is_admin = check_is_admin() | ||||
|     if is_admin != "pass": | ||||
|         return is_admin | ||||
| 
 | ||||
|     article_number = get_number_of_articles() | ||||
|     try: | ||||
|         _page_size = min(max(1, int(request.args.get("size", 5))), article_number)  # 最小的size是1 | ||||
|         _cur_page = min(max(1, int(request.args.get("page", 1))), article_number // _page_size + 1) # 最小的page是1 | ||||
|         _page_size = min( | ||||
|             max(1, int(request.args.get("size", 5))), article_number | ||||
|         )  # 最小的size是1 | ||||
|         _cur_page = min( | ||||
|             max(1, int(request.args.get("page", 1))), article_number // _page_size + 1 | ||||
|         )  # 最小的page是1 | ||||
|     except ValueError: | ||||
|         return "page parmas must be int!" | ||||
| 
 | ||||
|     context = { | ||||
|         "article_number": article_number, | ||||
|         "text_list": get_page_articles(_cur_page, _page_size), | ||||
|         "page_size": _page_size, | ||||
|         "cur_page": _cur_page, | ||||
|         "text_list": get_page_articles(_cur_page, _page_size), | ||||
|         "user_list": get_users(), | ||||
|         "username": username, | ||||
|         "yml": yml, | ||||
|         "username": session.get("username"), | ||||
|     } | ||||
| 
 | ||||
|     def _update_context(): | ||||
|  | @ -46,71 +68,50 @@ def admin(): | |||
|         context["text_list"] = get_page_articles(_cur_page, _page_size) | ||||
| 
 | ||||
|     if request.method == "GET": | ||||
|         delete_id = int(request.args.get("delete_id", 0)) | ||||
|         try: | ||||
|             delete_id = int(request.args.get("delete_id", 0)) | ||||
|         except: | ||||
|             return "Delete article ID must be int!" | ||||
|         if delete_id:  # delete article | ||||
|             delete_article(delete_id) | ||||
|             delete_article_by_id(delete_id) | ||||
|             _update_context() | ||||
|     else: | ||||
|     elif request.method == "POST": | ||||
|         data = request.form | ||||
|         content = data.get("content", "") | ||||
|         source = data.get("source", "") | ||||
|         question = data.get("question", "") | ||||
|         username = data.get("username", "") | ||||
|         level = data.get("level", "5") | ||||
|         if content: | ||||
|             try:    # check level | ||||
|             try:  # check level | ||||
|                 if level not in [str(x + 1) for x in range(5)]: | ||||
|                     raise ValueError | ||||
|             except ValueError: | ||||
|                 return "level must be between 1 and 5" | ||||
|                 return "Level must be between 1 and 5" | ||||
|             add_article(content, source, level, question) | ||||
|             _update_context() | ||||
| 
 | ||||
|     return render_template("admin_manage_article.html", **context) | ||||
| 
 | ||||
| 
 | ||||
| @adminService.route("/admin/user", methods=["GET", "POST"]) | ||||
| def user(): | ||||
|     is_admin = check_is_admin() | ||||
|     if is_admin != "pass": | ||||
|         return is_admin | ||||
|      | ||||
|     context = { | ||||
|         "user_list": get_users(), | ||||
|         "username": session.get("username"), | ||||
|     } | ||||
|     if request.method == "POST": | ||||
|         data = request.form | ||||
|         username = data.get("username","") | ||||
|         new_password = data.get("new_password", "") | ||||
|         expiry_time = data.get("expiry_time", "") | ||||
|         if username: | ||||
|             update_user_password(username) | ||||
| 
 | ||||
|     return render_template("admin_index.html", **context) | ||||
| 
 | ||||
| 
 | ||||
| def add_article(content, source="manual_input", level="5", question="No question"): | ||||
|     with db_session: | ||||
|         # add one article to sqlite | ||||
|         Article( | ||||
|             text=content, | ||||
|             source=source, | ||||
|             date=datetime.now().strftime("%-d %b %Y"),  # format style of `5 Oct 2022` | ||||
|             level=level, | ||||
|             question=question, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| def delete_article(article_id): | ||||
|     article_id &= 0xFFFFFFFF  # max 32 bits | ||||
|     with db_session: | ||||
|         article = Article.select(article_id=article_id) | ||||
|         if article: | ||||
|             article.first().delete() | ||||
| 
 | ||||
| 
 | ||||
| def get_number_of_articles(): | ||||
|     with db_session: | ||||
|         return len(Article.select()[:]) | ||||
| 
 | ||||
| 
 | ||||
| def get_page_articles(num, size): | ||||
|     with db_session: | ||||
|         return [ | ||||
|             x | ||||
|             for x in Article.select().order_by(desc(Article.article_id)).page(num, size) | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| def get_users(): | ||||
|     with db_session: | ||||
|         return User.select().order_by(User.name)[:] | ||||
| 
 | ||||
| 
 | ||||
| def update_user_password(username, password="123456"): | ||||
|     with db_session: | ||||
|         user = User.select(name=username) | ||||
|         if user: | ||||
|             user.first().password = md5(username + password) | ||||
|             if new_password: | ||||
|                 update_password_by_username(username, new_password) | ||||
|             if expiry_time: | ||||
|                 update_expiry_time_by_username(username, "".join(expiry_time.split("-"))) | ||||
|      | ||||
|     return render_template("admin_manage_user.html", **context) | ||||
|  | @ -42,82 +42,15 @@ | |||
|     </nav> | ||||
| 
 | ||||
|     <div class="card" style="margin-top:24px;"> | ||||
|         <h5 style="margin-top: 10px;padding-left: 10px;">重置选中用户密码</h5> | ||||
|         <form action="" method="post" class="container mb-3"> | ||||
|             <div class="mb-3"> | ||||
|                 <label  class="form-label">用户</label> | ||||
|                 <select id="username" name="username" class="form-select" aria-label="Default select example"> | ||||
|                     <option selected>选择用户</option> | ||||
|                     {% for user in user_list %} | ||||
|                     <option value="{{ user.name }}">{{ user.name }}</option> | ||||
|                     {% endfor %} | ||||
|                 </select> | ||||
|             </div> | ||||
|             <input type="submit" value="重置密码为:123456" class="btn btn-outline-primary"> | ||||
|         </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="card" style="margin-top:24px;"> | ||||
|         {% if tips %} | ||||
|         <div class="alert alert-success" role="alert"> | ||||
|             {{ tips }} | ||||
|         <div class="card-header"> | ||||
|             请选择您需要的操作 | ||||
|         </div> | ||||
|         {% endif %} | ||||
|         <div class="card-content"> | ||||
|             <h5 style="margin-top: 10px;padding-left: 10px;">录入文章</h5> | ||||
|             <form action="" method="post" class="container mb-3"> | ||||
|                 <div class="mb-3"> | ||||
|                     <label class="form-label">文章内容</label> | ||||
|                     <textarea id="content" name="content" class="form-control" placeholder="请输入文章内容"></textarea> | ||||
|                     <label class="form-label">文章来源</label> | ||||
|                     <textarea id="source" name="source" class="form-control" placeholder="请输入来源"></textarea> | ||||
|                     <label class="form-label">文章等级</label> | ||||
|                     <select id="level" class="form-select" name="level"> | ||||
|                         <option value="1">1</option> | ||||
|                         <option value="2">2</option> | ||||
|                         <option value="3">3</option> | ||||
|                         <option value="4">4</option> | ||||
|                         <option selected value="5">5</option> | ||||
|                     </select> | ||||
|                     <label class="form-label">文章问题</label> | ||||
|                     <textarea id="question" name="question" class="form-control" placeholder="请输入问题"></textarea> | ||||
|         <ul class="list-group list-group-flush"> | ||||
|             <li class="list-group-item"> | ||||
|                 <div class="d-grid gap-2"> | ||||
|                     <a href="/admin/article" class="btn btn-outline-primary" type="button">管理文章</a> | ||||
|                     <a href="/admin/user" class="btn btn-outline-primary" type="button">管理用户</a> | ||||
|                 </div> | ||||
|                 <input type="submit" value="保存" class="btn btn-outline-primary"> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="card" style="margin-top:24px;"> | ||||
|         <h5 style="margin-top: 10px;padding-left: 10px;">文章列表</h5> | ||||
|         <div class="list-group"> | ||||
|             {% for text in text_list %} | ||||
|             <div class="list-group-item list-group-item-action" aria-current="true"> | ||||
|                 <div class="d-flex w-100 justify-content-between"> | ||||
|                     <h5 class="mb-1">{{ text.source }}</h5> | ||||
|                     <small>Date:{{ text.date }} Level:{{ text.level }}</small> | ||||
|                 </div> | ||||
|                 <div style="text-align: right; padding-bottom: 5px;"><a href="/admin?delete_id={{text.article_id}}" | ||||
|                         class="btn btn-outline-danger btn-sm"> | ||||
|                         删除文章 | ||||
|                     </a></div> | ||||
|                 <p class="mb-1">{{ text.text }}</p> | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|     </div> | ||||
|     <div style="margin:20px 0;"> | ||||
|         <ul class="pagination pagination-sm justify-content-center"> | ||||
|             <li class="page-item"><a class="page-link" href="/admin?page={{ cur_page - 1 }}&size={{ page_size }}">Previous</a> | ||||
|             </li> | ||||
|             {% for i in range(1, article_number // page_size + 2) %} | ||||
|             {% if cur_page == i %} | ||||
|             <li class="page-item active"><a class="page-link" href="/admin?page={{ i }}&size={{ page_size }}">{{ i }}</a> | ||||
|             </li> | ||||
|             {% else %} | ||||
|             <li class="page-item"><a class="page-link" href="/admin?page={{ i }}&size={{ page_size }}">{{ i }}</a></li> | ||||
|             {% endif %} | ||||
|             {% endfor %} | ||||
|             <li class="page-item"><a class="page-link" href="/admin?page={{ cur_page + 1 }}&size={{ page_size }}">Next</a> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -0,0 +1,99 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| 
 | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" | ||||
|         content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" /> | ||||
|     <meta name="format-detection" content="telephone=no" /> | ||||
|     <link href="../static/css/bootstrap.css" rel="stylesheet"> | ||||
| </head> | ||||
| 
 | ||||
| <body class="container" style="width: 800px; margin: auto; margin-top:24px;"> | ||||
|     <nav class="navbar navbar-expand-lg bg-light"> | ||||
|         <div class="container-fluid"> | ||||
|             <a class="navbar-brand" href="#">管理员 {{ username }} 您好!</a> | ||||
|             <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" | ||||
|                 aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|                 <span class="navbar-toggler-icon"></span> | ||||
|             </button> | ||||
|             <div class="collapse navbar-collapse" id="navbarNav"> | ||||
|                 <ul class="navbar-nav"> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/{{ username }}">返回主页</a> | ||||
|                     </li> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/logout">退出登录</a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div> | ||||
|     </nav> | ||||
| 
 | ||||
|     <div class="card" style="margin-top:24px;"> | ||||
|         {% if tips %} | ||||
|         <div class="alert alert-success" role="alert"> | ||||
|             {{ tips }} | ||||
|         </div> | ||||
|         {% endif %} | ||||
|         <div class="card-content"> | ||||
|             <h5 style="margin-top: 10px;padding-left: 10px;">录入文章</h5> | ||||
|             <form action="" method="post" class="container mb-3"> | ||||
|                 <div class="mb-3"> | ||||
|                     <label class="form-label">文章内容</label> | ||||
|                     <textarea id="content" name="content" class="form-control" placeholder="请输入文章内容"></textarea> | ||||
|                     <label class="form-label">文章来源</label> | ||||
|                     <textarea id="source" name="source" class="form-control" placeholder="请输入来源"></textarea> | ||||
|                     <label class="form-label">文章等级</label> | ||||
|                     <select id="level" class="form-select" name="level"> | ||||
|                         <option value="1">1</option> | ||||
|                         <option value="2">2</option> | ||||
|                         <option value="3">3</option> | ||||
|                         <option value="4">4</option> | ||||
|                         <option selected value="5">5</option> | ||||
|                     </select> | ||||
|                     <label class="form-label">文章问题</label> | ||||
|                     <textarea id="question" name="question" class="form-control" placeholder="请输入问题"></textarea> | ||||
|                 </div> | ||||
|                 <input type="submit" value="保存" class="btn btn-outline-primary"> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="card" style="margin-top:24px;"> | ||||
|         <h5 style="margin-top: 10px;padding-left: 10px;">文章列表</h5> | ||||
|         <div class="list-group"> | ||||
|             {% for text in text_list %} | ||||
|             <div class="list-group-item list-group-item-action" aria-current="true"> | ||||
|                 <div class="d-flex w-100 justify-content-between"> | ||||
|                     <h5 class="mb-1">{{ text.source }}</h5> | ||||
|                     <small>Date:{{ text.date }} Level:{{ text.level }}</small> | ||||
|                 </div> | ||||
|                 <div style="text-align: right; padding-bottom: 5px;"><a href="/admin/article?delete_id={{text.article_id}}" | ||||
|                         class="btn btn-outline-danger btn-sm"> | ||||
|                         删除文章 | ||||
|                     </a></div> | ||||
|                 <p class="mb-1">{{ text.text }}</p> | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|     </div> | ||||
|     <div style="margin:20px 0;"> | ||||
|         <ul class="pagination pagination-sm justify-content-center"> | ||||
|             <li class="page-item"><a class="page-link" href="/admin/article?page={{ cur_page - 1 }}&size={{ page_size }}">Previous</a> | ||||
|             </li> | ||||
|             {% for i in range(1, article_number // page_size + 2) %} | ||||
|             {% if cur_page == i %} | ||||
|             <li class="page-item active"><a class="page-link" href="/admin/article?page={{ i }}&size={{ page_size }}">{{ i }}</a> | ||||
|             </li> | ||||
|             {% else %} | ||||
|             <li class="page-item"><a class="page-link" href="/admin/article?page={{ i }}&size={{ page_size }}">{{ i }}</a></li> | ||||
|             {% endif %} | ||||
|             {% endfor %} | ||||
|             <li class="page-item"><a class="page-link" href="/admin/article?page={{ cur_page + 1 }}&size={{ page_size }}">Next</a> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
|  | @ -0,0 +1,83 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| 
 | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" | ||||
|         content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" /> | ||||
|     <meta name="format-detection" content="telephone=no" /> | ||||
|     <link href="../static/css/bootstrap.css" rel="stylesheet"> | ||||
|     <script src="../static/js/jquery.js"></script> | ||||
| </head> | ||||
| 
 | ||||
| <body class="container" style="width: 800px; margin: auto; margin-top:24px;"> | ||||
|     <nav class="navbar navbar-expand-lg bg-light"> | ||||
|         <div class="container-fluid"> | ||||
|             <a class="navbar-brand" href="#">管理员 {{ username }} 您好!</a> | ||||
|             <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" | ||||
|                 aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|                 <span class="navbar-toggler-icon"></span> | ||||
|             </button> | ||||
|             <div class="collapse navbar-collapse" id="navbarNav"> | ||||
|                 <ul class="navbar-nav"> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/{{ username }}">返回主页</a> | ||||
|                     </li> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/logout">退出登录</a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div> | ||||
|     </nav> | ||||
| 
 | ||||
|     <div class="card" style="margin-top:24px;"> | ||||
|         <h5 style="margin-top: 10px;padding-left: 10px;">重置选中用户的信息</h5> | ||||
|         <form id="user_form" action="" method="post" class="container mb-3"> | ||||
|             <div> | ||||
|                 <label class="form-label" style="padding-top: 10px;">用户</label> | ||||
|                 <select id="username" name="username" class="form-select" aria-label="Default select example"> | ||||
|                     <option selected>选择用户</option> | ||||
|                     {% for user in user_list %} | ||||
|                     <option value="{{ user.name }}">{{ user.name }}</option> | ||||
|                     {% endfor %} | ||||
|                 </select> | ||||
| 
 | ||||
|                 <label class="form-label" style="padding-top: 10px;">修改密码</label> | ||||
|                 <div> | ||||
|                     <button type="button" id="reset_pwd_btn" class="btn btn-outline-success">获取12位随机密码</button> | ||||
|                     <input style="margin-left: 20px;border: 0; font-size: 20px;" name="new_password" | ||||
|                         id="new_password"></input> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <label class="form-label" style="padding-top: 10px;">过期时间</label> | ||||
|                 <div> | ||||
|                     <input type="date" name="expiry_time" placeholder="YYYY-MM-DD" pattern="yyyyMMdd"> | ||||
|                 </div> | ||||
| 
 | ||||
|             </div> | ||||
| 
 | ||||
|             <button style="margin-top: 50px;" type="submit" class="btn btn-primary">更新用户信息</button> | ||||
|         </form> | ||||
|     </div> | ||||
| </body> | ||||
| 
 | ||||
| 
 | ||||
| <script> | ||||
|     // 密码生成器 | ||||
|     function generatePassword(length) { | ||||
|         var charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~`|}{[]\:;?><,./-="; | ||||
|         var password = ""; | ||||
|         for (var i = 0; i < length; i++) { | ||||
|             password += charset.charAt(Math.floor(Math.random() * charset.length)); | ||||
|         } | ||||
|         return password; | ||||
|     } | ||||
|     document.getElementById("reset_pwd_btn").addEventListener("click", () => { | ||||
|         // 生成12位随机密码 | ||||
|         let pwd = generatePassword(12) | ||||
|         document.getElementById("new_password").value = pwd | ||||
|     }) | ||||
| </script> | ||||
| 
 | ||||
| </html> | ||||
		Loading…
	
		Reference in New Issue