diff --git a/.gitignore b/.gitignore index 413c71c..3d901ba 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ app/static/frequency/frequency.p app/static/wordfreqapp.db app/static/donate-the-author.jpg app/static/donate-the-author-hidden.jpg +app/model/__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index ec92598..14cc9aa 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,14 @@ Hui Lan EnglishPal allows the user to build his list of new English words -picked from articles selected for him according his vocabulary level. +picked from articles selected for him to read according his vocabulary level. EnglishPal will determine a user's vocabulary level based on his picked words. After that, it will recommend articles for him to read, in order to booster his English vocabulary furthermore. -## Run it on a local machine - +## Run on your own laptop `python3 main.py` -Make sure you have the SQLite database file in `app/static` (see below). +Make sure you have put the SQLite database file in the path `app/static` (see below). ## Run it as a Docker container @@ -29,32 +28,32 @@ Assuming that docker has been installed and that you are a sudo user (i.e., sudo `sudo ./build.sh` -Open your favourite Internet browser and enter this URL address: `http://ip-address:90`. +Open your favourite Internet browser and enter this URL address: `http://ip-address:90`. Note: you must update the variable `DEPLOYMENT_DIR` in `build.sh`. ### Explanation on the commands in build.sh -My steps for deploying English on the server. +My steps for deploying English on a Ubuntu server. - ssh to ubuntu@118.*.*.118 -- cd to /home/lanhui/englishpal2/EnglishPal +- cd to `/home/lanhui/englishpal2/EnglishPal` - Stop all docker service: `sudo service docker restart`. If you know the docker container ID, then the above command is an overkill. Use the following command instead: `sudo docker stop ContainerID`. You could get all container IDs with the following command: `sudo docker ps` -- Rebuild container. Run the following command to rebuild a docker image after the code gets updated: `sudo docker build -t englishpal .` +- Rebuild container. Run the following command to rebuild a docker image each time after the source code gets updated: `sudo docker build -t englishpal .` -- Run the application: `sudo docker run -d -p 90:80 -v /home/lanhui/englishpal2/EnglishPal/app/static/frequency:/app/static/frequency -t englishpal`. If you use `sudo docker run -d -p 90:80 -t englishpal`, data will be lost after terminating the program. +- Run the application: `sudo docker run -d -p 90:80 -v /home/lanhui/englishpal2/EnglishPal/app/static/frequency:/app/static/frequency -t englishpal`. If you use `sudo docker run -d -p 90:80 -t englishpal`, data will be lost after terminating the program. If you want to automatically restart the docker image after each system reboot, add the option `--restart=always` after `docker run`. -- Save space: `sudo docker system prune -a -f` +- Save disk space: `sudo docker system prune -a -f` + +`build.sh` contains all the above commands. Run "sudo ./build.sh" to rebuild and start the web application. -### Other useful docker commands +#### Other useful docker commands - `sudo docker ps -a` -- `sudo docker logs image_name`, where image_name could be obtained from `sudo docker ps`. - -`build.sh` contains all the above commands. Run "sudo ./build.sh" to rebuild and run the web application. +- `sudo docker logs image_name`, where `image_name` could be obtained from `sudo docker ps`. @@ -68,6 +67,10 @@ All articles are stored in the `article` table in a SQLite file called To add articles, open and edit `app/static/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org). +### Extending an account's expiry date + +By default, an account's expiry is 30 days after first sign-up. To extend account's expiry date, open and edit `user` table in `app/static/wordfreqapp.db`. Simply update field `expiry_date`. + ### Exporting the database Export wordfreqapp.db to wordfreqapp.sql using the following commands: @@ -92,33 +95,31 @@ sqlite3 wordfreqapp.db`. Delete wordfreqapp.db first if it exists. ### Uploading wordfreqapp.db to the server -`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal/app/static` +`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/static` ## Feedback -We welcome feedback on EnglishPal. +We welcome feedback on EnglishPal. Feedback examples: -### Respondent 1 +### Feedback 1 + +- "Need a phone app. I use phone a lot. You cannot ask students to use computers." -"Need a phone app. I use phone a lot. You cannot ask students to use computers." - -Can take a picture for text. Automatic translation. - -### Respondent 2 +### Feedback 2 -“成为会员”改成“注册” +- “成为会员”改成“注册” -“登出”改成“退出” +- “登出”改成“退出” -“收集生词吧”改成“生词收集栏” +- “收集生词吧”改成“生词收集栏” -“不要自动显示下一篇” +- 不要自动显示下一篇 -需要有“上一篇”、“下一篇” +- 需要有“上一篇”、“下一篇”按钮。 @@ -137,7 +138,7 @@ EnglishPal's bugs and improvement suggestions are recorded in [Bugzilla](http:// - Usability testing -## Improvements made by contributors +## Improvements made by contributors (incomplete list) ### 朱文绮 @@ -159,7 +160,6 @@ too many words that they already know, on the other hand, it can reduce unnecessary memory occupied by the database, in addition, it can also improve the simplicity of the page. -More information at: http://118.25.96.118/kanboard/?controller=TaskViewController&action=readonly&task_id=736&token=81a561da57ff7a172da17a480f0d421ff3bc69efbd29437daef90b1b8959 ### 占健豪 @@ -181,4 +181,16 @@ Demo video link: https://b23.tv/QuB77m Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=215 -*Last modified on 2021-10-17* \ No newline at end of file + + +### 丁锐 + +修复了以下漏洞 + +漏洞:新用户在创建账号时,不需要输入确定密码也可以注册成功,并且新账户可以正常使用。 + +Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=489 + + +*Last modified on 2023-01-30* + diff --git a/app/Article.py b/app/Article.py index 04a32ea..e0f006a 100644 --- a/app/Article.py +++ b/app/Article.py @@ -32,12 +32,17 @@ def get_article_body(s): return '\n'.join(lst) -def get_today_article(user_word_list, articleID): +def get_today_article(user_word_list, existing_articles): rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') - if articleID == None: + if existing_articles is None: + existing_articles = { + "index" : 0, # 为 article_ids 的索引 + "article_ids": [] # 之前显示文章的id列表,越后越新 + } + if existing_articles["index"] > len(existing_articles["article_ids"])-1: rq.instructions("SELECT * FROM article") else: - rq.instructions('SELECT * FROM article WHERE article_id=%d' % (articleID)) + rq.instructions('SELECT * FROM article WHERE article_id=%d' % (existing_articles["article_ids"][existing_articles["index"]])) rq.do() result = rq.get_results() random.shuffle(result) @@ -47,36 +52,41 @@ def get_today_article(user_word_list, articleID): d2 = load_freq_history(path_prefix + 'static/words_and_tests.p') d3 = get_difficulty_level(d1, d2) - d = {} + d = None d_user = load_freq_history(user_word_list) user_level = user_difficulty_level(d_user, d3) # more consideration as user's behaviour is dynamic. Time factor should be considered. - random.shuffle(result) # shuffle list - d = random.choice(result) - text_level = text_difficulty_level(d['text'], d3) - if articleID == None: + text_level = 0 + if existing_articles["index"] > len(existing_articles["article_ids"])-1: # 下一篇 + flag_get_article = False for reading in result: text_level = text_difficulty_level(reading['text'], d3) factor = random.gauss(0.8, 0.1) # a number drawn from Gaussian distribution with a mean of 0.8 and a stand deviation of 1 - if within_range(text_level, user_level, (8.0 - user_level) * factor): + if reading['article_id'] not in existing_articles["article_ids"] and within_range(text_level, user_level, (8.0 - user_level) * factor): # 新的文章之前没有出现过且符合一定范围的水平 d = reading + existing_articles["article_ids"].append(d['article_id']) # 列表添加新的文章id;下面进行 + flag_get_article = True break + if not flag_get_article: + existing_articles["index"] -= 1 + else: # 上一篇 + d = random.choice(result) + text_level = text_difficulty_level(d['text'], d3) - s = '' % ( - user_level, text_level) - s += '

Article added on: %s

' % (d['date']) - s += '
' - article_title = get_article_title(d['text']) - article_body = get_article_body(d['text']) - s += '

%s

' % (article_title) - s += '

%s

' % (article_body) - s += '

%s

' % (d['source']) - s += '

%s

' % (get_question_part(d['question'])) - s = s.replace('\n', '
') - s += '%s' % (get_answer_part(d['question'])) - s += '
' - session['articleID'] = d['article_id'] - return s + today_article = None + if d: + today_article = { + "user_level": '%4.2f' % user_level, + "text_level": '%4.2f' % text_level, + "date": d['date'], + "article_title": get_article_title(d['text']), + "article_body": get_article_body(d['text']), + "source": d["source"], + "question": get_question_part(d['question']), + "answer": get_answer_part(d['question']) + } + + return existing_articles, today_article def load_freq_history(path): @@ -116,21 +126,4 @@ def get_answer_part(s): flag = 1 elif flag == 1: result.append(line) - # https://css-tricks.com/snippets/javascript/showhide-element/ - js = ''' - - ''' - html_code = js - html_code += '\n' - html_code += '\n' - html_code += '\n' % ('\n'.join(result)) - return html_code \ No newline at end of file + return '\n'.join(result) diff --git a/app/Login.py b/app/Login.py index 1ada0af..cd750d1 100644 --- a/app/Login.py +++ b/app/Login.py @@ -1,8 +1,20 @@ import hashlib import string -from datetime import datetime +from datetime import datetime, timedelta from UseSqlite import InsertQuery, RecordQuery +def md5(s): + ''' + MD5摘要 + :param str: 字符串 + :return: 经MD5以后的字符串 + ''' + h = hashlib.md5(s.encode(encoding='utf-8')) + return h.hexdigest() + +# import model.user after the defination of md5(s) to avoid circular import +from model.user import get_user_by_username, insert_user, update_password_by_username + path_prefix = '/var/www/wordfreq/wordfreq/' path_prefix = './' # comment this line in deployment @@ -12,33 +24,22 @@ def verify_pass(newpass,oldpass): def verify_user(username, password): - rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') - password = md5(username + password) - rq.instructions_with_parameters("SELECT * FROM user WHERE name=:username AND password=:password", dict( - username=username, password=password)) # the named style https://docs.python.org/3/library/sqlite3.html - rq.do_with_parameters() - result = rq.get_results() - return result != [] + user = get_user_by_username(username) + encoded_password = md5(username + password) + return user is not None and user.password == encoded_password def add_user(username, password): start_date = datetime.now().strftime('%Y%m%d') - expiry_date = '20221230' + expiry_date = (datetime.now() + timedelta(days=30)).strftime('%Y%m%d') # will expire after 30 days # 将用户名和密码一起加密,以免暴露不同用户的相同密码 password = md5(username + password) - rq = InsertQuery(path_prefix + 'static/wordfreqapp.db') - rq.instructions_with_parameters("INSERT INTO user VALUES (:username, :password, :start_date, :expiry_date)", dict( - username=username, password=password, start_date=start_date, expiry_date=expiry_date)) - rq.do_with_parameters() + insert_user(username=username, password=password, start_date=start_date, expiry_date=expiry_date) def check_username_availability(username): - rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') - rq.instructions_with_parameters( - "SELECT * FROM user WHERE name=:username", dict(username=username)) - rq.do_with_parameters() - result = rq.get_results() - return result == [] + existed_user = get_user_by_username(username) + return existed_user is None def change_password(username, old_password, new_password): @@ -54,35 +55,16 @@ def change_password(username, old_password, new_password): # 将用户名和密码一起加密,以免暴露不同用户的相同密码 if verify_pass(new_password,old_password): #新旧密码一致 return False - password = md5(username + new_password) - rq = InsertQuery(path_prefix + 'static/wordfreqapp.db') - rq.instructions_with_parameters("UPDATE user SET password=:password WHERE name=:username", dict( - password=password, username=username)) - rq.do_with_parameters() + update_password_by_username(username, new_password) return True def get_expiry_date(username): - rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') - rq.instructions_with_parameters( - "SELECT expiry_date FROM user WHERE name=:username", dict(username=username)) - rq.do_with_parameters() - result = rq.get_results() - if len(result) > 0: - return result[0]['expiry_date'] - else: + user = get_user_by_username(username) + if user is None: return '20191024' - - -def md5(s): - ''' - MD5摘要 - :param str: 字符串 - :return: 经MD5以后的字符串 - ''' - h = hashlib.md5(s.encode(encoding='utf-8')) - return h.hexdigest() - + else: + return user.expiry_date class UserName: def __init__(self, username): @@ -96,9 +78,9 @@ class UserName: if ' ' in self.username: # a user name must not include a whitespace return 'Whitespace is not allowed in the user name.' for c in self.username: # a user name must not include special characters, except non-leading periods or underscores - if c in string.punctuation and c is not '.' and c is not '_': + if c in string.punctuation and c != '.' and c != '_': return f'{c} is not allowed in the user name.' - if self.username in ['signup', 'login', 'logout', 'reset', 'mark', 'back', 'unfamiliar', 'familiar', 'del']: + if self.username in ['signup', 'login', 'logout', 'reset', 'mark', 'back', 'unfamiliar', 'familiar', 'del', 'admin']: return 'You used a restricted word as your user name. Please come up with a better one.' return 'OK' diff --git a/app/account_service.py b/app/account_service.py index dc854a3..c1bd64c 100644 --- a/app/account_service.py +++ b/app/account_service.py @@ -5,7 +5,6 @@ from Login import check_username_availability, verify_user, add_user, get_expiry # 初始化蓝图 accountService = Blueprint("accountService", __name__) - ### Sign-up, login, logout ### @accountService.route("/signup", methods=['GET', 'POST']) def signup(): @@ -24,14 +23,11 @@ def signup(): #! 添加如下代码为了过滤注册时的非法字符 warn = WarningMessage(username) if str(warn) != 'OK': - return str(warn) + return jsonify({'status': '3', 'warn': str(warn)}) available = check_username_availability(username) if not available: # 用户名不可用 - flash('用户名 %s 已经被注册。' % (username)) - return render_template('signup.html') - elif len(password.strip()) < 4: # 密码过短 - return '密码过于简单。' + return jsonify({'status': '0'}) else: # 添加账户信息 add_user(username, password) verified = verify_user(username, password) @@ -41,11 +37,11 @@ def signup(): session[username] = username session['username'] = username session['expiry_date'] = get_expiry_date(username) - session['articleID'] = None - return '

恭喜,你已成功注册, 你的用户名是 %s

\ -

开始使用 返回首页

' % (username, username, username) + session['existing_articles'] = None + return jsonify({'status': '2'}) else: - return '用户名密码验证失败。' + return jsonify({'status': '1'}) + @accountService.route("/login", methods=['GET', 'POST']) @@ -56,13 +52,7 @@ def login(): ''' if request.method == 'GET': # GET请求 - if not session.get('logged_in'): - # 未登录,返回登录页面 - return render_template('login.html') - else: - # 已登录,提示信息并显示登出按钮 - return '你已登录 %s。 登出点击这里。' % ( - session['username'], session['username']) + return render_template('login.html') elif request.method == 'POST': # POST方法用于判断登录是否成功 # check database and verify user @@ -76,10 +66,10 @@ def login(): session['username'] = username user_expiry_date = get_expiry_date(username) session['expiry_date'] = user_expiry_date - session['articleID'] = None - return redirect(url_for('user_bp.userpage', username=username)) + session['existing_articles'] = None + return jsonify({'status': '1'}) else: - return '无法通过验证。' + return jsonify({'status': '0'}) @accountService.route("/logout", methods=['GET', 'POST']) @@ -112,31 +102,9 @@ def reset(): # POST请求用于提交修改后信息 old_password = escape(request.form['old-password']) new_password = escape(request.form['new-password']) - - re_new_password = escape(request.form['re-new-password']) # 确认新密码 - if re_new_password != new_password: #验证新密码两次输入是否相同 - return '新密码不匹配,请重新输入' - if len(new_password) < 4: #验证新密码长度,原则参照注册模块 - return '密码过于简单。(密码长度至少4位)' - flag = change_password(username, old_password, new_password) # flag表示是否修改成功 if flag: session['logged_in'] = False - return \ -''' - - -''' - + return jsonify({'status':'1'}) # 修改成功 else: - return \ -''' - - -''' + return jsonify({'status':'2'}) # 修改失败 diff --git a/app/admin_service.py b/app/admin_service.py new file mode 100644 index 0000000..1d1ba6e --- /dev/null +++ b/app/admin_service.py @@ -0,0 +1,145 @@ +# System Library +from flask import * + +# Personal library +from Yaml import yml +from model.user import * +from model.article import * + +ADMIN_NAME = "lanhui" # unique admin name +_cur_page = 1 # current article page +_page_size = 5 # article sizes per page +adminService = Blueprint("admin_service", __name__) + + +def check_is_admin(): + # 未登录,跳转到未登录界面 + if not session.get("logged_in"): + return render_template("not_login.html") + + # 用户名不是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 + (_article_number % _page_size > 0) + ) # 最小的page是1 + except ValueError: + return "page parmas must be int!" + + _articles = get_page_articles(_cur_page, _page_size) + for article in _articles: # 获取每篇文章的title + article.title = article.text.split("\n")[0] + article.content = '
'.join(article.text.split("\n")[1:]) + + context = { + "article_number": _article_number, + "text_list": _articles, + "page_size": _page_size, + "cur_page": _cur_page, + "username": session.get("username"), + } + + def _update_context(): + article_len = get_number_of_articles() + context["article_number"] = article_len + context["text_list"] = get_page_articles(_cur_page, _page_size) + _articles = get_page_articles(_cur_page, _page_size) + for article in _articles: # 获取每篇文章的title + article.title = article.text.split("\n")[0] + context["text_list"] = _articles + + if request.method == "GET": + 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_by_id(delete_id) + _update_context() + elif request.method == "POST": + data = request.form + content = data.get("content", "") + source = data.get("source", "") + question = data.get("question", "") + level = data.get("level", "4") + if content: + try: # check level + if level not in ['1', '2', '3', '4']: + raise ValueError + except ValueError: + return "Level must be between 1 and 4." + add_article(content, source, level, question) + _update_context() + title = content.split('\n')[0] + flash(f'Article added. Title: {title}') + 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: + if new_password: + update_password_by_username(username, new_password) + flash(f'Password updated to {new_password}') + if expiry_time: + update_expiry_time_by_username(username, "".join(expiry_time.split("-"))) + flash(f'Expiry date updated to {expiry_time}.') + return render_template("admin_manage_user.html", **context) + + +@adminService.route("/admin/expiry", methods=["GET"]) +def user_expiry_time(): + is_admin = check_is_admin() + if is_admin != "pass": + return is_admin + + username = request.args.get("username", "") + if not username: + return "Username can't be empty." + + user = get_user_by_username(username) + if not user: + return "User does not exist." + + return user.expiry_date diff --git a/app/main.py b/app/main.py index e311bb0..4e3f829 100644 --- a/app/main.py +++ b/app/main.py @@ -5,24 +5,24 @@ # Copyright 2019 (C) Hui Lan # Written permission must be obtained from the author for commercial uses. ########################################################################### - from flask import escape from Login import * from Article import * import Yaml from user_service import userService from account_service import accountService +from admin_service import adminService, ADMIN_NAME app = Flask(__name__) app.secret_key = 'lunch.time!' # 将蓝图注册到Lab app app.register_blueprint(userService) app.register_blueprint(accountService) +app.register_blueprint(adminService) path_prefix = '/var/www/wordfreq/wordfreq/' path_prefix = './' # comment this line in deployment - def get_random_image(path): ''' 返回随机图 @@ -39,8 +39,7 @@ def get_random_ads(): 返回随机广告 :return: 一个广告(包含HTML标签) ''' - ads = random.choice(['个性化分析精准提升', '你的专有单词本', '智能捕捉阅读弱点,针对性提高你的阅读水平']) - return ads + '。 试试吧!' + return random.choice(['个性化分析精准提升', '你的专有单词本', '智能捕捉阅读弱点,针对性提高你的阅读水平']) def appears_in_test(word, d): @@ -98,9 +97,13 @@ def mainpage(): d = load_freq_history(path_prefix + 'static/frequency/frequency.p') d_len = len(d) lst = sort_in_descending_order(pickle_idea.dict2lst(d)) - return render_template('mainpage_get.html', random_ads=random_ads, number_of_essays=number_of_essays, - d_len=d_len, lst=lst, yml=Yaml.yml) - + return render_template('mainpage_get.html', + admin_name=ADMIN_NAME, + random_ads=random_ads, + d_len=d_len, + lst=lst, + yml=Yaml.yml, + number_of_essays=number_of_essays) if __name__ == '__main__': diff --git a/app/model/__init__.py b/app/model/__init__.py new file mode 100644 index 0000000..9526313 --- /dev/null +++ b/app/model/__init__.py @@ -0,0 +1,30 @@ +from pony.orm import * + +db = Database() +db.bind("sqlite", "../static/wordfreqapp.db", create_db=True) # bind sqlite file + + +class User(db.Entity): + _table_ = "user" # table name + name = PrimaryKey(str) + password = Optional(str) + start_date = Optional(str) + expiry_date = Optional(str) + + +class Article(db.Entity): + _table_ = "article" # table name + article_id = PrimaryKey(int, auto=True) + text = Optional(str) + source = Optional(str) + date = Optional(str) + level = Optional(str) + question = Optional(str) + + +db.generate_mapping(create_tables=True) # must mapping after class declaration + + +if __name__ == "__main__": + with db_session: + print(Article[2].text) # test get article which id=2 text content diff --git a/app/model/article.py b/app/model/article.py new file mode 100644 index 0000000..a3b4bf7 --- /dev/null +++ b/app/model/article.py @@ -0,0 +1,34 @@ +from model import * +from datetime import datetime + +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_by_id(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) + ] diff --git a/app/model/user.py b/app/model/user.py new file mode 100644 index 0000000..d684332 --- /dev/null +++ b/app/model/user.py @@ -0,0 +1,30 @@ +from model import * +from Login import md5 +from pony import orm + +def get_users(): + with db_session: + return User.select().order_by(User.name)[:] + +def get_user_by_username(username): + with db_session: + user = User.select(name=username) + if user: + return user.first() + +def insert_user(username, password, start_date, expiry_date): + with db_session: + user = User(name=username, password=password, start_date=start_date, expiry_date=expiry_date) + orm.commit() + +def update_password_by_username(username, password="123456"): + with db_session: + user = User.select(name=username) + if user: + user.first().password = md5(username + password) + +def update_expiry_time_by_username(username, expiry_time="20230323"): + with db_session: + user = User.select(name=username) + if user: + user.first().expiry_date = expiry_time diff --git a/app/static/frequency/README.txt b/app/static/frequency/README.txt new file mode 100644 index 0000000..3a9c1f9 --- /dev/null +++ b/app/static/frequency/README.txt @@ -0,0 +1,5 @@ +This folder holds users' vocabulary files. +Each file ends with .pickle. +For example, mrlan.pickle is the vocabulary file for user mrlan. + + diff --git a/app/static/js/fillword.js b/app/static/js/fillword.js index 505808e..ba249dd 100644 --- a/app/static/js/fillword.js +++ b/app/static/js/fillword.js @@ -62,3 +62,7 @@ function onReadClick() { function onChooseClick() { isChoose = !isChoose; } + +function stopRead() { + reader.cancel(); +} \ No newline at end of file diff --git a/app/static/js/highlight.js b/app/static/js/highlight.js index 555f76b..0cea31a 100644 --- a/app/static/js/highlight.js +++ b/app/static/js/highlight.js @@ -22,13 +22,21 @@ function getWord() { function highLight() { if (!isHighlight) return; - let articleContent = document.getElementById("article").innerText; + let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml,使用innerText会把原文章中所包含的
标签去除,导致处理后的文章内容失去了原来的格式 let pickedWords = document.getElementById("selected-words"); // words picked to the text area let dictionaryWords = document.getElementById("selected-words2"); // words appearing in the user's new words list - let allWords = pickedWords.value + " " + dictionaryWords.value; - const list = allWords.split(" "); + let allWords = ""; //初始化allWords的值,避免进入判断后编译器认为allWords未初始化的问题 + if(dictionaryWords != null){//增加一个判断,检查生词本里面是否为空,如果为空,allWords只添加选中的单词 + allWords = pickedWords.value + " " + dictionaryWords.value; + } + else{ + allWords = pickedWords.value + " "; + } + const list = allWords.split(" ");//将所有的生词放入一个list中,用于后续处理 for (let i = 0; i < list.length; ++i) { list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); //消除单词两边的空字符 + list[i] = list[i].replace('|', ""); + list[i] = list[i].replace('?', ""); if (list[i] !== "" && "".indexOf(list[i]) === -1 && "".indexOf(list[i]) === -1) { //将文章中所有出现该单词word的地方改为:"" + word + ""。 正则表达式RegExp()中,"\\b"代表单词边界匹配。 @@ -48,15 +56,15 @@ function highLight() { } function cancelHighlighting() { - let articleContent = document.getElementById("article").innerText; + let articleContent = document.getElementById("article").innerText;//将原来的.innerText改为.innerHtml,原因同上 let pickedWords = document.getElementById("selected-words"); const dictionaryWords = document.getElementById("selected-words2"); const list = pickedWords.value.split(" "); if (pickedWords != null) { for (let i = 0; i < list.length; ++i) { list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); - if (list[i] !== "") { - articleContent = articleContent.replace("" + list[i] + "", "list[i]"); + if (list[i] !== "") { //原来判断的代码中,替换的内容为“list[i]”这个字符串,这明显是错误的,我们需要替换的是list[i]里的内容 + articleContent = articleContent.replace(new RegExp(""+list[i]+"", "g"), list[i]); } } } @@ -65,8 +73,8 @@ function cancelHighlighting() { for (let i = 0; i < list2.length; ++i) { list2 = dictionaryWords.value.split(" "); list2[i] = list2[i].replace(/(^\s*)|(\s*$)/g, ""); - if (list2[i] !== "") { - articleContent = articleContent.replace("" + list[i] + "", "list[i]"); + if (list2[i] !== "") { //原来代码中,替换的内容为“list[i]”这个字符串,这明显是错误的,我们需要替换的是list[i]里的内容 + articleContent = articleContent.replace(new RegExp(""+list2[i]+"", "g"), list2[i]); } } } diff --git a/app/static/js/word_operation.js b/app/static/js/word_operation.js index a55fb6e..ea6a6e8 100644 --- a/app/static/js/word_operation.js +++ b/app/static/js/word_operation.js @@ -7,10 +7,20 @@ function familiar(theWord) { url:"/" + username + "/" + word + "/familiar", success:function(response){ let new_freq = freq - 1; - if(new_freq <1) { - $("#p_" + theWord).remove(); + const allow_move = document.getElementById("move_dynamiclly").checked; + if (allow_move) { + + if (new_freq <= 0) { + removeWord(theWord); + } else { + renderWord({ word: theWord, freq: new_freq }); + } } else { - $("#freq_" + theWord).text(new_freq); + if(new_freq <1) { + $("#p_" + theWord).remove(); + } else { + $("#freq_" + theWord).text(new_freq); + } } } }); @@ -25,19 +35,139 @@ function unfamiliar(theWord) { url:"/" + username + "/" + word + "/unfamiliar", success:function(response){ let new_freq = parseInt(freq) + 1; - $("#freq_" + theWord).text(new_freq); + const allow_move = document.getElementById("move_dynamiclly").checked; + if (allow_move) { + renderWord({ word: theWord, freq: new_freq }); + } else { + $("#freq_" + theWord).text(new_freq); + } } }); } function delete_word(theWord) { let username = $("#username").text(); - let word = $("#word_" + theWord).text(); + let word = theWord.replace('&', '&'); $.ajax({ type:"GET", url:"/" + username + "/" + word + "/del", success:function(response){ - $("#p_" + theWord).remove(); + const allow_move = document.getElementById("move_dynamiclly").checked; + if (allow_move) { + removeWord(theWord); + } else { + $("#p_" + theWord).remove(); + } } }); } + +/* + * interface Word { + * word: string, + * freq: number + * } +* */ + +/** + * 传入一个词频HTML元素,将其解析为Word类型的对象 + */ +function parseWord(element) { + const word = element + .querySelector("a.btn.btn-light[role=button]") // 获取当前词频元素的词汇元素 + .innerText // 获取词汇值; + const freq = Number.parseInt(element.querySelector(`#freq_${word}`).innerText); // 获取词汇的数量 + return { + word, + freq + }; +} + +/** + * 使用模板将传入的单词转换为相应的HTML字符串 +*/ +function wordTemplate(word) { + // 这个模板应当与 templates/userpage_get.html 中的

...

保持一致 + return `

+ ${word.word} + ( ${word.freq} ) + 熟悉 + 不熟悉 + 删除 +

`; +} + +/** + * 删除某一词频元素 + * 此处word为词频元素对应的单词 + */ +function removeWord(word) { + // 根据词频信息删除元素 + word = word.replace('&', '&'); + const element_to_remove = document.getElementById(`p_${word}`); + if (element_to_remove != null) { + element_to_remove.remove(); + } +} + +function renderWord(word) { + const container = document.querySelector(".word-container"); + // 删除原有元素 + removeWord(word.word); + // 插入新元素 + let inserted = false; + const new_element = elementFromString(wordTemplate(word)); + for (const current of container.children) { + const cur_word = parseWord(current); + // 找到第一个词频比它小的元素,插入到这个元素前面 + if (compareWord(cur_word, word) == -1) { + container.insertBefore(new_element, current); + inserted = true; + break; + } + } + // 当word就是词频最小的词时,把他补回去 + if (!inserted) { + container.appendChild(new_element); + } + // 让发生变化的元素抖动 + new_element.classList.add("shaking"); + // 移动到该元素 + new_element.scrollIntoView({behavior: "smooth", block: "center", inline: "nearest"}); + // 抖动完毕后删除抖动类 + setTimeout(() => { + new_element.classList.remove("shaking"); + }, 1600); +} + +/** + * 从string中创建一个HTML元素并返回 + */ +function elementFromString(string) { + const d = document.createElement('div'); + d.innerHTML = string; + return d.children.item(0); +} + +/** + * 对比两个单词: + * 当first小于second时返回-1 + * 当first等于second时返回0 + * 当first大于second时返回1 + */ +function compareWord(first, second) { + if (first.freq < second.freq) { + return -1; + } + if (first.freq > second.freq) { + return 1; + } + if (first.word < second.word) { + return -1; + } + if (first.word > second.word) { + return 1; + } + return 0; +} diff --git a/app/templates/admin_index.html b/app/templates/admin_index.html new file mode 100644 index 0000000..68ee68f --- /dev/null +++ b/app/templates/admin_index.html @@ -0,0 +1,55 @@ + + + + + + + + {{ yml['header'] | safe }} + {% if yml['css']['item'] %} + {% for css in yml['css']['item'] %} + + {% endfor %} + {% endif %} + {% if yml['js']['head'] %} + {% for js in yml['js']['head'] %} + + {% endfor %} + {% endif %} + + + + + + +
+
+ 请选择您需要的操作 +
+ +
+ + + diff --git a/app/templates/admin_manage_article.html b/app/templates/admin_manage_article.html new file mode 100644 index 0000000..272b54e --- /dev/null +++ b/app/templates/admin_manage_article.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + {% for message in get_flashed_messages() %} + + {% endfor %} + +
+ {% if tips %} + + {% endif %} +
+
录入文章
+
+
+ + + + + + + + +
+ +
+
+
+ +
+
文章列表
+
+ {% for text in text_list %} +
+
+ 删除 +
+
+
{{ text.title }}
+
+
{{ text.source }}
+
+ Level: {{text.level }} + Date: {{ text.date }} +
+ {{ text.content | safe }} +
+ {% endfor %} +
+
+
+ +
+ + + diff --git a/app/templates/admin_manage_user.html b/app/templates/admin_manage_user.html new file mode 100644 index 0000000..a3f0ca0 --- /dev/null +++ b/app/templates/admin_manage_user.html @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + {% for message in get_flashed_messages() %} + + {% endfor %} + +
+
重置选中用户的信息
+
+
+ + + + +
+ + +
+ + +
+ +
+ +
+ + +
+
+ + + + + + diff --git a/app/templates/expiry.html b/app/templates/expiry.html index 797a109..9464325 100644 --- a/app/templates/expiry.html +++ b/app/templates/expiry.html @@ -5,7 +5,7 @@ 账号过期 -

您的账号{{ username }}过期。

+

您的账号过期(过期日 {{expiry_date}})。

为了提高服务质量,English Pal 收取会员费用, 每天1元。

请决定你要试用的时间长度,扫描下面支付宝二维码支付。 支付时请注明English Pal Membership Fee。 我们会于12小时内激活账号。

支付宝二维码

diff --git a/app/templates/login.html b/app/templates/login.html index a347e22..ccf6f34 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -1,28 +1,47 @@ {% block body %} {% if session['logged_in'] %} -You're logged in already! +你已登录 {{ session['username'] }}。 登出点击这里。 {% else %} - + +
-
- - - -
+ + + +
-注册 - {% endif %} {% endblock %} diff --git a/app/templates/mainpage_get.html b/app/templates/mainpage_get.html index cbb51a6..3594571 100644 --- a/app/templates/mainpage_get.html +++ b/app/templates/mainpage_get.html @@ -23,12 +23,15 @@

English Pal - Learn English smartly!

{% if session['logged_in'] %} - {{session['username']}}

+ {{ session['username'] }} + {% if session['username'] == admin_name %} + 管理

+ {% endif %} {% else %}

登录 注册 使用说明

-

{{random_ads|safe}}

+

{{ random_ads }}。 试试吧!

{% endif %} - +

粘贴1篇文章 (English only)


diff --git a/app/templates/reset.html b/app/templates/reset.html index 902d046..d29855b 100644 --- a/app/templates/reset.html +++ b/app/templates/reset.html @@ -2,6 +2,38 @@ + +
@@ -9,14 +41,11 @@

Reset Password

- - - - - - - + + + + +
{% endblock %} \ No newline at end of file diff --git a/app/templates/signup.html b/app/templates/signup.html index f7bd69f..c70e4ba 100644 --- a/app/templates/signup.html +++ b/app/templates/signup.html @@ -6,6 +6,47 @@ You're logged in already! Logout. {% else %} + +

{{ get_flashed_messages()[0] | safe }}

@@ -15,12 +56,10 @@ You're logged in already! Logout.

Sign Up

-
-

-

-

- -
+

+

+

+
diff --git a/app/templates/userpage_get.html b/app/templates/userpage_get.html index 9e3891b..b5e16aa 100644 --- a/app/templates/userpage_get.html +++ b/app/templates/userpage_get.html @@ -19,24 +19,67 @@ {% endif %} EnglishPal Study Room for {{ username }} + +

English Pal for {{ username }} - 退出 - 重设密码 + {% if username == admin_name %} + 管理 + {% endif %} + 退出 + 重设密码

- {{ flashed_messages|safe }} +{# {% for message in flashed_messages %}#} {# 根据user_service.userpage,取消了参数flashed_messages,因此注释了这段代码 #} +{# #} +{# {% endfor %}#} - 下一篇 Next Article - {% if session.get('articleID') != session.get('old_articleID') %} - {% if session.get('old_articleID') != None %} - 上一篇 Previous Article - {% endif%} + 下一篇 Next Article + {% if session.get('existing_articles') != None and session.get('existing_articles')["index"] !=0 %} + 上一篇 Previous Article {% endif %}

阅读文章并回答问题

-
{{ today_article|safe }}
+
+ {% if today_article %} + +

Article added on: {{ today_article["date"] }}


+

+

{{ today_article["article_title"] }}


+

{{ today_article["article_body"] }}


+

{{ today_article['source'] }}


+

{{ today_article['question'] }}


+ + +
+
+ {% else %} + + {% endif %} +
生词高亮 大声朗读 @@ -46,9 +89,9 @@
- +
- +

收集生词吧 (可以在正文中划词,也可以复制黏贴)


@@ -67,22 +110,30 @@ {% endif %} {% if d_len > 0 %} -

我的生词簿

- {% for x in lst3 %} - {% set word = x[0] %} - {% set freq = x[1] %} - {% if session.get('thisWord') == x[0] and session.get('time') == 1 %} - - {% endif %} -

- {{ word }} - ( {{ freq }} ) - 熟悉 - 不熟悉 - 删除 -

- {% endfor %} +

+ 我的生词簿 + +

+ +
+ {% for x in lst3 %} + {% set word = x[0] %} + {% set freq = x[1] %} + {% if session.get('thisWord') == x[0] and session.get('time') == 1 %} + {% endif %} +

+ {{ word }} + ( {{ freq }} ) + 熟悉 + 不熟悉 + 删除 +

+ {% endfor %} +
{% endif %} @@ -95,9 +146,9 @@ diff --git a/app/templates/userpage_post.html b/app/templates/userpage_post.html index 725ee09..1163787 100644 --- a/app/templates/userpage_post.html +++ b/app/templates/userpage_post.html @@ -20,7 +20,7 @@ EnglishPal Study Room for {{username}} -

勾选不认识的单词

+

取消勾选认识的单词

{% for x in lst %} @@ -30,7 +30,7 @@ : {{word}} ({{x[1]}}) - +

{% endfor %} diff --git a/app/user_service.py b/app/user_service.py index 79c7888..c69c9e2 100644 --- a/app/user_service.py +++ b/app/user_service.py @@ -1,5 +1,5 @@ from datetime import datetime - +from admin_service import ADMIN_NAME from flask import * # from app import Yaml @@ -29,9 +29,10 @@ def user_reset(username): :param username: 用户名 :return: 返回页面内容 ''' - session['old_articleID'] = session.get('articleID') if request.method == 'GET': - session['articleID'] = None + existing_articles = session.get("existing_articles") + existing_articles["index"] += 1 + session["existing_articles"] = existing_articles return redirect(url_for('user_bp.userpage', username=username)) else: return 'Under construction' @@ -44,7 +45,9 @@ def user_back(username): :return: 返回页面内容 ''' if request.method == 'GET': - session['articleID'] = session.get('old_articleID') + existing_articles = session.get("existing_articles") + existing_articles["index"] -= 1 + session["existing_articles"] = existing_articles return redirect(url_for('user_bp.userpage', username=username)) @@ -89,7 +92,8 @@ def deleteword(username, word): ''' user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username) pickle_idea2.deleteRecord(user_freq_record, word) - flash(f'{word} is no longer in your word list.') + # 模板userpage_get.html中删除单词是异步执行,而flash的信息后续是同步执行的,所以注释这段代码;同时如果这里使用flash但不提取信息,则会影响 signup.html的显示。bug复现:删除单词后,点击退出,点击注册,注册页面就会出现提示信息 + # flash(f'{word} is no longer in your word list.') return "success" @@ -107,7 +111,7 @@ def userpage(username): # 用户过期 user_expiry_date = session.get('expiry_date') if datetime.now().strftime('%Y%m%d') > user_expiry_date: - return render_template('expiry.html') + return render_template('expiry.html', expiry_date=user_expiry_date) # 获取session里的用户名 username = session.get('username') @@ -130,11 +134,15 @@ def userpage(username): words = '' for x in lst3: words += x[0] + ' ' + existing_articles, today_article = get_today_article(user_freq_record, session.get('existing_articles')) + session['existing_articles'] = existing_articles + # 通过 today_article,加载前端的显示页面 return render_template('userpage_get.html', + admin_name=ADMIN_NAME, username=username, session=session, - flashed_messages=get_flashed_messages_if_any(), - today_article=get_today_article(user_freq_record, session['articleID']), + # flashed_messages=get_flashed_messages(), 仅有删除单词的时候使用到flash,而删除单词是异步执行,这里的信息提示是同步执行,所以就没有存在的必要了 + today_article=today_article, d_len=len(d), lst3=lst3, yml=Yaml.yml, @@ -173,15 +181,3 @@ def get_time(): ''' return datetime.now().strftime('%Y%m%d%H%M') # upper to minutes -def get_flashed_messages_if_any(): - ''' - 在用户界面显示黄色提示信息 - :return: 包含HTML标签的提示信息 - ''' - messages = get_flashed_messages() - s = '' - for message in messages: - s += '' - return s diff --git a/app/wordfreqCMD.py b/app/wordfreqCMD.py index 9ee7e56..e56ba0c 100644 --- a/app/wordfreqCMD.py +++ b/app/wordfreqCMD.py @@ -39,7 +39,7 @@ def file2str(fname):#文件转字符 def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。 - special_characters = '_©~=+[]*&$%^@.,?!:;#()"“”—‘’' # 把里面的字符都去掉 + special_characters = '\_©~<=>+-/[]*&$%^@.,?!:;#()"“”—‘’{}|' # 把里面的字符都去掉 for c in special_characters: s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况 s = s.replace('--', ' ') @@ -70,7 +70,7 @@ def sort_in_ascending_order(lst):# 单词按频率降序排列 return lst2 -def make_html_page(lst, fname): +def make_html_page(lst, fname): # 只是在wordfreqCMD.py中的main函数中调用,所以不做修改 ''' 功能:把lst的信息存到fname中,以html格式。 ''' diff --git a/build.sh b/build.sh index 4348b2f..e313fce 100755 --- a/build.sh +++ b/build.sh @@ -3,6 +3,10 @@ DEPLOYMENT_DIR=/home/lanhui/englishpal2/EnglishPal cd $DEPLOYMENT_DIR +# Install dependencies + +pip3 install -r requirements.txt + # Stop service sudo docker stop EnglishPal sudo docker rm EnglishPal diff --git a/requirements.txt b/requirements.txt index 2746a3b..e2d1e1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Flask==1.1.2 selenium==3.141.0 PyYAML~=6.0 +pony==0.7.16