From 8e8de286e0f56f1aac1d2e5ca2a6613392efbe65 Mon Sep 17 00:00:00 2001 From: SUNJIAWEN <1556400771@qq.com> Date: Thu, 4 Jul 2024 10:06:44 +0800 Subject: [PATCH] Fix bug 566 --- .gitignore | 12 +- Dockerfile | 9 +- Jenkinsfile | 4 +- README.md | 30 +- app/Article.py | 34 +-- app/Login.py | 1 - app/UseSqlite.py | 87 ------ app/account_service.py | 1 + app/admin_service.py | 46 ++-- app/db/README.txt | 1 + app/difficulty.py | 11 +- app/main.py | 11 +- app/model/__init__.py | 2 +- app/model/article.py | 16 +- app/static/config.yml | 3 +- app/static/css/highlighted.css | 5 + app/static/js/checkboxes.js | 5 + app/static/js/fillword.js | 22 +- app/static/js/highlight.js | 79 +++--- app/static/js/read.js | 4 +- app/static/js/word_operation.js | 44 ++- app/static/pic/laba.png | Bin 0 -> 6591 bytes app/static/wordfreqapp.db | Bin 0 -> 20480 bytes app/templates/login.html | 9 +- app/templates/mainpage_get.html | 22 +- app/templates/mainpage_post.html | 2 +- app/templates/reset.html | 4 + app/templates/signup.html | 6 +- app/templates/userpage_get.html | 182 +++++-------- app/templates/userpage_post.html | 83 +++--- app/test/conftest.py | 28 +- app/test/helper.py | 33 +++ app/test/test_add_word.py | 75 +---- app/test/test_bug528_tangjiao.py | 95 +++++++ app/test/test_bug544_tangxinyuan.py | 55 ++++ app/test/test_bug545_HuangHuiLing.py | 44 +++ app/test/test_bug546_lixiaofeng.py | 39 +++ app/test/test_bug551_DingZeYu.py | 37 +++ app/test/test_bug553_LinShan.py | 58 ++++ app/test/test_bug561_LiangZiyue.py | 27 ++ app/test/test_stress.py | 43 +++ app/user_service.py | 13 +- app/wordfreqCMD.py | 6 +- build.sh | 7 +- da | 391 +++++++++++++++++++++++++++ img.png | Bin 0 -> 4446 bytes 46 files changed, 1235 insertions(+), 451 deletions(-) delete mode 100644 app/UseSqlite.py create mode 100644 app/db/README.txt create mode 100644 app/static/css/highlighted.css create mode 100644 app/static/js/checkboxes.js create mode 100644 app/static/pic/laba.png create mode 100644 app/static/wordfreqapp.db create mode 100644 app/test/helper.py create mode 100644 app/test/test_bug528_tangjiao.py create mode 100644 app/test/test_bug544_tangxinyuan.py create mode 100644 app/test/test_bug545_HuangHuiLing.py create mode 100644 app/test/test_bug546_lixiaofeng.py create mode 100644 app/test/test_bug551_DingZeYu.py create mode 100644 app/test/test_bug553_LinShan.py create mode 100644 app/test/test_bug561_LiangZiyue.py create mode 100755 app/test/test_stress.py create mode 100644 da create mode 100644 img.png diff --git a/.gitignore b/.gitignore index 3d901ba..33f789d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,20 @@ venv/ app/__init__.py app/__pycache__/ +.DS_Store +app/.DS_Store app/sqlite_commands.py app/static/usr/*.jpg app/static/img/ app/static/frequency/frequency_*.pickle app/static/frequency/frequency.p -app/static/wordfreqapp.db +app/wordfreqapp.db +app/db/wordfreqapp.db app/static/donate-the-author.jpg app/static/donate-the-author-hidden.jpg -app/model/__pycache__/ \ No newline at end of file +app/model/__pycache__/ +app/test/__pycache__/ +app/test/.pytest_cache/ +app/test/pytest_report.html +app/test/assets +app/log.txt diff --git a/Dockerfile b/Dockerfile index 284195a..55e5946 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM tiangolo/uwsgi-nginx-flask:python3.6 -COPY requirements.txt /app -RUN pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ -COPY ./app /app +FROM tiangolo/uwsgi-nginx-flask:python3.8-alpine +COPY requirements.txt /tmp +COPY ./app/ /app/ +RUN pip3 install -U pip -i https://mirrors.aliyun.com/pypi/simple/ +RUN pip3 install -r /tmp/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ diff --git a/Jenkinsfile b/Jenkinsfile index 2633859..c3772cc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -10,8 +10,8 @@ pipeline { stages { stage('MakeDatabasefile') { steps { - sh 'touch ./app/static/wordfreqapp.db && rm -f ./app/static/wordfreqapp.db' - sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/static/wordfreqapp.db' + sh 'touch ./app/wordfreqapp.db && rm -f ./app/wordfreqapp.db' + sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/wordfreqapp.db' } } stage('BuildIt') { diff --git a/README.md b/README.md index 14cc9aa..15fc966 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,15 @@ My steps for deploying English on a Ubuntu server. All articles are stored in the `article` table in a SQLite file called -`app/static/wordfreqapp.db`. +`app/db/wordfreqapp.db`. ### Adding new articles -To add articles, open and edit `app/static/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org). +To add articles, open and edit `app/db/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`. +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/db/wordfreqapp.db`. Simply update field `expiry_date`. ### Exporting the database @@ -95,7 +95,7 @@ sqlite3 wordfreqapp.db`. Delete wordfreqapp.db first if it exists. ### Uploading wordfreqapp.db to the server -`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/static` +`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/db/` @@ -129,6 +129,28 @@ We welcome feedback on EnglishPal. Feedback examples: EnglishPal's bugs and improvement suggestions are recorded in [Bugzilla](http://118.25.96.118/bugzilla/buglist.cgi?bug_status=__all__&list_id=1302&order=Importance&product=EnglishPal&query_format=specific). Send (lanhui at zjnu.edu.cn) an email message for opening a Bugzilla account or reporting a bug. +## End-to-end testing + +We use the Selenium test framework to test our app. + +In order to run the test, first we need to download a webdriver executable. + +Microsoft Edge's webdriver can be downloaded from [microsoft-edge-tools-webdriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/). Make sure the version we download matches the version of the web browser installed on our laptop. + +After extracting the downloaded zip file (e.g., edgedriver_win64.zip), rename msedgedriver.exe to MicrosoftWebDriver.exe. + +Add MicrosoftWebDriver.exe's path to system's PATH variable. + +Install the following dependencies too: + +- pip install -U selenium==3.141.0 +- pip install -U urllib3==1.26.2 + +Run English Pal first, then run the test using pytest as follows: pytest --html=pytest_report.html test_add_word.py + +The above command will generate a HTML report file pytest_report.html after finishing executing test_add_word.py. Note: you need to install pytest-html package first: pip install pytest-html. + +You may also want to use [webdriver-manager](https://pypi.org/project/webdriver-manager/) from PyPI, so that you can avoid tediously installing a web driver executable manually. However, my experience shows that webdriver-manager is too slow. For example, it took me 16 minutes to run 9 tests, while with the pre-installed web driver executable, it took less than 2 minutes. ## TODO diff --git a/app/Article.py b/app/Article.py index df9ac3a..566ceb6 100644 --- a/app/Article.py +++ b/app/Article.py @@ -1,6 +1,5 @@ from WordFreq import WordFreq from wordfreqCMD import youdao_link, sort_in_descending_order -from UseSqlite import InsertQuery, RecordQuery import pickle_idea, pickle_idea2 import os import random, glob @@ -8,18 +7,15 @@ import hashlib from datetime import datetime from flask import Flask, request, redirect, render_template, url_for, session, abort, flash, get_flashed_messages from difficulty import get_difficulty_level_for_user, text_difficulty_level, user_difficulty_level +from model.article import get_all_articles, get_article_by_id, get_number_of_articles +import logging - -path_prefix = '/var/www/wordfreq/wordfreq/' -path_prefix = './' # comment this line in deployment +path_prefix = './' +db_path_prefix = './db/' # comment this line in deployment def total_number_of_essays(): - rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') - rq.instructions("SELECT * FROM article") - rq.do() - result = rq.get_results() - return len(result) + return get_number_of_articles() def get_article_title(s): @@ -33,32 +29,36 @@ def get_article_body(s): def get_today_article(user_word_list, visited_articles): - rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') if visited_articles is None: visited_articles = { "index" : 0, # 为 article_ids 的索引 "article_ids": [] # 之前显示文章的id列表,越后越新 } if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章,因此查找所有的文章 - rq.instructions("SELECT * FROM article") + result = get_all_articles() else: # 生成阅读过的文章,因此查询指定 article_id 的文章 if visited_articles["article_ids"][visited_articles["index"]] == 'null': # 可能因为直接刷新页面导致直接去查询了'null',因此当刷新的页面的时候,需要直接进行“上一篇”操作 visited_articles["index"] -= 1 visited_articles["article_ids"].pop() - rq.instructions('SELECT * FROM article WHERE article_id=%d' % (visited_articles["article_ids"][visited_articles["index"]])) - rq.do() - result = rq.get_results() + article_id = visited_articles["article_ids"][visited_articles["index"]] + result = get_article_by_id(article_id) random.shuffle(result) # Choose article according to reader's level - d1 = load_freq_history(path_prefix + 'static/frequency/frequency.p') + logging.debug('* get_today_article(): start d1 = ... ') + d1 = load_freq_history(user_word_list) d2 = load_freq_history(path_prefix + 'static/words_and_tests.p') + logging.debug(' ... get_today_article(): get_difficulty_level_for_user() start') d3 = get_difficulty_level_for_user(d1, d2) + logging.debug(' ... get_today_article(): done') d = None result_of_generate_article = "not found" + d_user = load_freq_history(user_word_list) + logging.debug('* get_today_article(): user_difficulty_level() start') user_level = user_difficulty_level(d_user, d3) # more consideration as user's behaviour is dynamic. Time factor should be considered. + logging.debug('* get_today_article(): done') text_level = 0 if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章 amount_of_visited_articles = len(visited_articles["article_ids"]) @@ -87,8 +87,8 @@ def get_today_article(user_word_list, visited_articles): today_article = None if d: today_article = { - "user_level": '%4.2f' % user_level, - "text_level": '%4.2f' % text_level, + "user_level": '%4.1f' % user_level, + "text_level": '%4.1f' % text_level, "date": d['date'], "article_title": get_article_title(d['text']), "article_body": get_article_body(d['text']), diff --git a/app/Login.py b/app/Login.py index cd750d1..17d92fa 100644 --- a/app/Login.py +++ b/app/Login.py @@ -1,7 +1,6 @@ import hashlib import string from datetime import datetime, timedelta -from UseSqlite import InsertQuery, RecordQuery def md5(s): ''' diff --git a/app/UseSqlite.py b/app/UseSqlite.py deleted file mode 100644 index ea4baeb..0000000 --- a/app/UseSqlite.py +++ /dev/null @@ -1,87 +0,0 @@ -########################################################################### -# Copyright 2019 (C) Hui Lan -# Written permission must be obtained from the author for commercial uses. -########################################################################### - - -# Reference: Dusty Phillips. Python 3 Objected-oriented Programming Second Edition. Pages 326-328. -# Copyright (C) 2019 Hui Lan - -import sqlite3 - -class Sqlite3Template: - def __init__(self, db_fname): - self.db_fname = db_fname - - def connect(self, db_fname): - self.conn = sqlite3.connect(self.db_fname) - - def instructions(self, query_statement): - raise NotImplementedError() - - def operate(self): - self.conn.row_factory = sqlite3.Row - self.results = self.conn.execute(self.query) # self.query is to be given in the child classes - self.conn.commit() - - def format_results(self): - raise NotImplementedError() - - def do(self): - self.connect(self.db_fname) - self.instructions(self.query) - self.operate() - - def instructions_with_parameters(self, query_statement, parameters): - self.query = query_statement - self.parameters = parameters - - def do_with_parameters(self): - self.connect(self.db_fname) - self.instructions_with_parameters(self.query, self.parameters) - self.operate_with_parameters() - - def operate_with_parameters(self): - self.conn.row_factory = sqlite3.Row - self.results = self.conn.execute(self.query, self.parameters) # self.query is to be given in the child classes - self.conn.commit() - - -class InsertQuery(Sqlite3Template): - def instructions(self, query): - self.query = query - - -class RecordQuery(Sqlite3Template): - def instructions(self, query): - self.query = query - - def format_results(self): - output = [] - for row_dict in self.results.fetchall(): - lst = [] - for k in dict(row_dict): - lst.append( row_dict[k] ) - output.append(', '.join(lst)) - return '\n\n'.join(output) - - def get_results(self): - result = [] - for row_dict in self.results.fetchall(): - result.append( dict(row_dict) ) - return result - - - -if __name__ == '__main__': - - #iq = InsertQuery('RiskDB.db') - #iq.instructions("INSERT INTO inspection Values ('FoodSupplies', 'RI2019051301', '2019-05-13', '{}')") - #iq.do() - #iq.instructions("INSERT INTO inspection Values ('CarSupplies', 'RI2019051302', '2019-05-13', '{[{\"risk_name\":\"elevator\"}]}')") - #iq.do() - - rq = RecordQuery('wordfreqapp.db') - rq.instructions("SELECT * FROM article WHERE level=3") - rq.do() - #print(rq.format_results()) diff --git a/app/account_service.py b/app/account_service.py index a7ed0c4..fd5f7f6 100644 --- a/app/account_service.py +++ b/app/account_service.py @@ -1,4 +1,5 @@ from flask import * +from markupsafe import escape from Login import check_username_availability, verify_user, add_user, get_expiry_date, change_password, WarningMessage diff --git a/app/admin_service.py b/app/admin_service.py index a604b5e..c461af9 100644 --- a/app/admin_service.py +++ b/app/admin_service.py @@ -1,5 +1,6 @@ # System Library from flask import * +from markupsafe import escape # Personal library from Yaml import yml @@ -37,6 +38,22 @@ def admin(): @adminService.route("/admin/article", methods=["GET", "POST"]) def article(): + + def _make_title_and_content(article_lst): + for article in article_lst: + text = escape(article.text) # Fix XSS vulnerability, contributed by Xu Xuan + article.title = text.split("\n")[0] + article.content = '
'.join(text.split("\n")[1:]) + + + 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) + _make_title_and_content(_articles) + context["text_list"] = _articles + global _cur_page, _page_size is_admin = check_is_admin() @@ -44,20 +61,15 @@ def article(): 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 + _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!" - + return "page parameters must be integer!" + _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:]) + _make_title_and_content(_articles) context = { "article_number": _article_number, @@ -67,23 +79,16 @@ def article(): "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!" + return "Delete article ID must be integer!" if delete_id: # delete article delete_article_by_id(delete_id) _update_context() + elif request.method == "POST": data = request.form content = data.get("content", "") @@ -97,6 +102,7 @@ def article(): _update_context() title = content.split('\n')[0] flash(f'Article added. Title: {title}') + return render_template("admin_manage_article.html", **context) diff --git a/app/db/README.txt b/app/db/README.txt new file mode 100644 index 0000000..bb826a6 --- /dev/null +++ b/app/db/README.txt @@ -0,0 +1 @@ +Put wordfreqapp.db here diff --git a/app/difficulty.py b/app/difficulty.py index cb93768..1bd8d68 100644 --- a/app/difficulty.py +++ b/app/difficulty.py @@ -18,6 +18,7 @@ def load_record(pickle_fname): return d +ENGLISH_WORD_DIFFICULTY_DICT = {} def convert_test_type_to_difficulty_level(d): """ 对原本的单词库中的单词进行难度评级 @@ -39,8 +40,10 @@ def convert_test_type_to_difficulty_level(d): elif 'BBC' in d[k]: result[k] = 8 - return result # {'apple': 4, ...} + global ENGLISH_WORD_DIFFICULTY_DICT + ENGLISH_WORD_DIFFICULTY_DICT = result + return result # {'apple': 4, ...} def get_difficulty_level_for_user(d1, d2): """ @@ -49,7 +52,11 @@ def get_difficulty_level_for_user(d1, d2): 在d2的后面添加单词,没有新建一个新的字典 """ # TODO: convert_test_type_to_difficulty_level() should not be called every time. Each word's difficulty level should be pre-computed. - d2 = convert_test_type_to_difficulty_level(d2) # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...} + if ENGLISH_WORD_DIFFICULTY_DICT == {}: + d2 = convert_test_type_to_difficulty_level(d2) # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...} + else: + d2 = ENGLISH_WORD_DIFFICULTY_DICT + stemmer = snowballstemmer.stemmer('english') for k in d1: # 用户的词 diff --git a/app/main.py b/app/main.py index 4e3f829..19bd889 100644 --- a/app/main.py +++ b/app/main.py @@ -1,19 +1,19 @@ -#! /usr/bin/python3 -# -*- coding: utf-8 -*- - ########################################################################### # Copyright 2019 (C) Hui Lan # Written permission must be obtained from the author for commercial uses. ########################################################################### -from flask import escape +from flask import abort +from markupsafe 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 +import os + app = Flask(__name__) -app.secret_key = 'lunch.time!' +app.secret_key = os.urandom(32) # 将蓝图注册到Lab app app.register_blueprint(userService) @@ -54,7 +54,6 @@ def appears_in_test(word, d): else: return ','.join(d[word]) - @app.route("/mark", methods=['GET', 'POST']) def mark_word(): ''' diff --git a/app/model/__init__.py b/app/model/__init__.py index 9526313..f5256a2 100644 --- a/app/model/__init__.py +++ b/app/model/__init__.py @@ -1,7 +1,7 @@ from pony.orm import * db = Database() -db.bind("sqlite", "../static/wordfreqapp.db", create_db=True) # bind sqlite file +db.bind("sqlite", "../db/wordfreqapp.db", create_db=True) # bind sqlite file class User(db.Entity): diff --git a/app/model/article.py b/app/model/article.py index a3b4bf7..bf19ded 100644 --- a/app/model/article.py +++ b/app/model/article.py @@ -7,7 +7,7 @@ def add_article(content, source="manual_input", level="5", question="No question Article( text=content, source=source, - date=datetime.now().strftime("%-d %b %Y"), # format style of `5 Oct 2022` + date=datetime.now().strftime("%d %b %Y"), # format style of `5 Oct 2022` level=level, question=question, ) @@ -32,3 +32,17 @@ def get_page_articles(num, size): x for x in Article.select().order_by(desc(Article.article_id)).page(num, size) ] + + +def get_all_articles(): + articles = [] + with db_session: + for article in Article.select(): + articles.append(article.to_dict()) + return articles + + +def get_article_by_id(article_id): + with db_session: + article = Article.get(article_id=article_id) + return [article.to_dict()] diff --git a/app/static/config.yml b/app/static/config.yml index 285f31f..7e681fe 100644 --- a/app/static/config.yml +++ b/app/static/config.yml @@ -2,13 +2,14 @@ css: item: - ../static/css/bootstrap.css - + - ../static/css/highlighted.css # 全局引入的js文件地址 js: head: # 在页面加载之前加载 - ../static/js/jquery.js - ../static/js/read.js - ../static/js/word_operation.js + - ../static/js/checkboxes.js bottom: # 在页面加载完之后加载 - ../static/js/fillword.js - ../static/js/highlight.js diff --git a/app/static/css/highlighted.css b/app/static/css/highlighted.css new file mode 100644 index 0000000..167f595 --- /dev/null +++ b/app/static/css/highlighted.css @@ -0,0 +1,5 @@ + +.highlighted { + color: red; + font-weight: normal; +} \ No newline at end of file diff --git a/app/static/js/checkboxes.js b/app/static/js/checkboxes.js new file mode 100644 index 0000000..297df55 --- /dev/null +++ b/app/static/js/checkboxes.js @@ -0,0 +1,5 @@ +function toggleCheckboxSelection(checkStatus) { + // used in userpage_post.html + const checkBoxes = document.getElementsByName('marked'); + checkBoxes.forEach((checkbox) => { checkbox.checked = checkStatus;} ); +} diff --git a/app/static/js/fillword.js b/app/static/js/fillword.js index b967633..111027a 100644 --- a/app/static/js/fillword.js +++ b/app/static/js/fillword.js @@ -1,5 +1,5 @@ -let isRead = true; -let isChoose = true; +let isRead = localStorage.getItem('readChecked') !== 'false'; // default to true +let isChoose = localStorage.getItem('chooseChecked') !== 'false'; function getWord() { return window.getSelection ? window.getSelection() : document.selection.createRange().text; @@ -8,9 +8,17 @@ function getWord() { function fillInWord() { let word = getWord(); if (isRead) Reader.read(word, inputSlider.value); - if (!isChoose) return; + if (!isChoose) { + if(isHighlight){ + const element = document.getElementById("selected-words3"); + element.value = element.value + " " + word; + } + return; + } const element = document.getElementById("selected-words"); + localStorage.setItem('nowWords', element.value); element.value = element.value + " " + word; + localStorage.setItem('selectedWords', element.value); } document.getElementById("text-content").addEventListener("click", fillInWord, false); @@ -24,8 +32,16 @@ inputSlider.oninput = () => { function onReadClick() { isRead = !isRead; + localStorage.setItem('readChecked', isRead); } function onChooseClick() { isChoose = !isChoose; + localStorage.setItem('chooseChecked', isChoose); } + +// 如果网页刷新,停止播放声音 +if (performance.getEntriesByType("navigation")[0].type == "reload") { + Reader.stopRead(); +} + diff --git a/app/static/js/highlight.js b/app/static/js/highlight.js index 0cea31a..9646ff3 100644 --- a/app/static/js/highlight.js +++ b/app/static/js/highlight.js @@ -1,4 +1,4 @@ -let isHighlight = true; +let isHighlight = localStorage.getItem('highlightChecked') !== 'false'; // default to true function cancelBtnHandler() { cancelHighlighting(); @@ -22,62 +22,48 @@ function getWord() { function highLight() { if (!isHighlight) return; - let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml,使用innerText会把原文章中所包含的
标签去除,导致处理后的文章内容失去了原来的格式 + let word = (getWord() + "").trim(); + let articleContent = document.getElementById("article").innerHTML; // innerHTML保留HTML标签来保持部分格式,且适配不同的浏览器 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 = ""; //初始化allWords的值,避免进入判断后编译器认为allWords未初始化的问题 - if(dictionaryWords != null){//增加一个判断,检查生词本里面是否为空,如果为空,allWords只添加选中的单词 - allWords = pickedWords.value + " " + dictionaryWords.value; + let allWords = dictionaryWords === null ? pickedWords.value + " " : pickedWords.value + " " + dictionaryWords.value; + highlightWords = document.getElementById("selected-words3"); + allWords = highlightWords == null ? allWords : allWords + " " + highlightWords.value; + const list = allWords.split(" "); // 将所有的生词放入一个list中 + if(word !== null && word !== "" && word !== " "){ + let articleContent_fb2 = articleContent; + if(localStorage.getItem("nowWords").indexOf(word) !== -1 || localStorage.getItem("nowWords").indexOf(word.toLowerCase()) !== -1){ + articleContent = articleContent.replace(new RegExp('' + word + '', "gi"), word); + pickedWords.value = localStorage.getItem("nowWords").replace(word,""); + document.getElementById("article").innerHTML = articleContent; + return; + } } - else{ - allWords = pickedWords.value + " "; - } - const list = allWords.split(" ");//将所有的生词放入一个list中,用于后续处理 + let totalSet = new Set(); for (let i = 0; i < list.length; ++i) { - list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); //消除单词两边的空字符 + list[i] = list[i].replace(/(^\W*)|(\W*$)/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"代表单词边界匹配。 - - //修改代码 - let articleContent_fb = articleContent; //文章副本 - while(articleContent_fb.toLowerCase().indexOf(list[i].toLowerCase()) !== -1 && list[i]!=""){ - //找到副本中和list[i]匹配的第一个单词(第一种匹配情况),并赋值给list[i]。 - const index = articleContent_fb.toLowerCase().indexOf(list[i].toLowerCase()); - list[i] = articleContent_fb.substring(index, index + list[i].length); - - articleContent_fb = articleContent_fb.substring(index + list[i].length); // 使用副本中list[i]之后的子串替换掉副本 - articleContent = articleContent.replace(new RegExp("\\b"+list[i]+"\\b","g"),"" + list[i] + ""); - } + if (list[i] != "" && !totalSet.has(list[i])) { + // 返回所有匹配单词的集合, 正则表达式RegExp()中, "\b"匹配一个单词的边界, g 表示全局匹配, i 表示对大小写不敏感。 + let matches = new Set(articleContent.match(new RegExp("\\b" + list[i] + "\\b", "gi"))); + totalSet = new Set([...totalSet, ...matches]); } } + // 删除所有的""标签,防止标签发生嵌套 + articleContent = articleContent.replace(new RegExp('',"gi"), "") + articleContent = articleContent.replace(new RegExp("","gi"), ""); + // 将文章中所有出现该单词word的地方改为:"" + word + ""。 + for (let word of totalSet) { + articleContent = articleContent.replace(new RegExp("\\b" + word + "\\b", "g"), "" + word + ""); + } document.getElementById("article").innerHTML = articleContent; } function cancelHighlighting() { - 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] !== "") { //原来判断的代码中,替换的内容为“list[i]”这个字符串,这明显是错误的,我们需要替换的是list[i]里的内容 - articleContent = articleContent.replace(new RegExp(""+list[i]+"", "g"), list[i]); - } - } - } - if (dictionaryWords != null) { - let list2 = pickedWords.value.split(" "); - for (let i = 0; i < list2.length; ++i) { - list2 = dictionaryWords.value.split(" "); - list2[i] = list2[i].replace(/(^\s*)|(\s*$)/g, ""); - if (list2[i] !== "") { //原来代码中,替换的内容为“list[i]”这个字符串,这明显是错误的,我们需要替换的是list[i]里的内容 - articleContent = articleContent.replace(new RegExp(""+list2[i]+"", "g"), list2[i]); - } - } - } + let articleContent = document.getElementById("article").innerHTML; + articleContent = articleContent.replace(new RegExp('',"gi"), "") + articleContent = articleContent.replace(new RegExp("","gi"), ""); document.getElementById("article").innerHTML = articleContent; } @@ -97,6 +83,7 @@ function toggleHighlighting() { isHighlight = true; highLight(); } + localStorage.setItem('highlightChecked', isHighlight); } -showBtnHandler(); +showBtnHandler(); \ No newline at end of file diff --git a/app/static/js/read.js b/app/static/js/read.js index 814f627..c28fd26 100644 --- a/app/static/js/read.js +++ b/app/static/js/read.js @@ -9,7 +9,7 @@ var Reader = (function() { msg.rate = rate; msg.lang = "en-US"; msg.onboundary = ev => { - if (ev.name == "word") { + if (ev.name === "word") { current_position = ev.charIndex; } } @@ -32,4 +32,4 @@ var Reader = (function() { read: read, stopRead: stopRead }; -})(); +}) (); diff --git a/app/static/js/word_operation.js b/app/static/js/word_operation.js index f043cce..944f0e8 100644 --- a/app/static/js/word_operation.js +++ b/app/static/js/word_operation.js @@ -5,15 +5,14 @@ function familiar(theWord) { $.ajax({ type:"GET", url:"/" + username + "/" + word + "/familiar", - success:function(response){ + success:function(response) { let new_freq = freq - 1; const allow_move = document.getElementById("move_dynamiclly").checked; if (allow_move) { - if (new_freq <= 0) { removeWord(theWord); } else { - renderWord({ word: theWord, freq: new_freq }); + renderWord({word: theWord, freq: new_freq}); } } else { if(new_freq <1) { @@ -33,11 +32,11 @@ function unfamiliar(theWord) { $.ajax({ type:"GET", url:"/" + username + "/" + word + "/unfamiliar", - success:function(response){ + success:function(response) { let new_freq = parseInt(freq) + 1; const allow_move = document.getElementById("move_dynamiclly").checked; if (allow_move) { - renderWord({ word: theWord, freq: new_freq }); + renderWord({word: theWord, freq: new_freq}); } else { $("#freq_" + theWord).text(new_freq); } @@ -51,7 +50,7 @@ function delete_word(theWord) { $.ajax({ type:"GET", url:"/" + username + "/" + word + "/del", - success:function(response){ + success:function(response) { const allow_move = document.getElementById("move_dynamiclly").checked; if (allow_move) { removeWord(theWord); @@ -96,13 +95,12 @@ function parseWord(element) { function wordTemplate(word) { // 这个模板应当与 templates/userpage_get.html 中的

...

保持一致 return `

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

`; } @@ -114,7 +112,7 @@ function removeWord(word) { // 根据词频信息删除元素 word = word.replace('&', '&'); const element_to_remove = document.getElementById(`p_${word}`); - if (element_to_remove != null) { + if (element_to_remove !== null) { element_to_remove.remove(); } } @@ -129,7 +127,7 @@ function renderWord(word) { for (const current of container.children) { const cur_word = parseWord(current); // 找到第一个词频比它小的元素,插入到这个元素前面 - if (compareWord(cur_word, word) == -1) { + if (compareWord(cur_word, word) === -1) { container.insertBefore(new_element, current); inserted = true; break; @@ -165,17 +163,11 @@ function elementFromString(string) { * 当first大于second时返回1 */ function compareWord(first, second) { - if (first.freq < second.freq) { - return -1; + if (first.freq !== second.freq) { + return first.freq < second.freq ? -1 : 1; } - if (first.freq > second.freq) { - return 1; - } - if (first.word < second.word) { - return -1; - } - if (first.word > second.word) { - return 1; + if (first.word !== second.word) { + return first.word < second.word ? -1 : 1; } return 0; -} +} \ No newline at end of file diff --git a/app/static/pic/laba.png b/app/static/pic/laba.png new file mode 100644 index 0000000000000000000000000000000000000000..6628bb3e60502c6d7e6b23d6372c01fcf14e4f4a GIT binary patch literal 6591 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 literal 0 HcmV?d00001 diff --git a/app/templates/login.html b/app/templates/login.html index 2507f75..b0806b6 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -1,7 +1,7 @@ {% block body %} {% if session['logged_in'] %} -你已登录 {{ session['username'] }}。 登出点击这里。 +你已登录 {{ session['username'] }}。 登出点击这里。 {% else %} @@ -15,6 +15,10 @@ alert('输入不能为空!'); return false; } + if (password.includes(' ')) { + alert('输入不能包含空格!'); + return false; + } $.post( "/login", {'username': username, 'password': password}, function (response) { @@ -32,14 +36,13 @@
-
{% endif %} diff --git a/app/templates/mainpage_get.html b/app/templates/mainpage_get.html index 344943d..3ba0ece 100644 --- a/app/templates/mainpage_get.html +++ b/app/templates/mainpage_get.html @@ -34,9 +34,9 @@

粘贴1篇文章 (English only)

-
+
- +
{% if d_len > 0 %}

最常见的词

@@ -44,6 +44,7 @@ {{x[0]}} {{x[1]}} {% endfor %} {% endif %} +

Version: 20230810

{{ yml['footer'] | safe }} @@ -52,5 +53,22 @@ {% endfor %} {% endif %} + diff --git a/app/templates/mainpage_post.html b/app/templates/mainpage_post.html index 7357457..5df7dd5 100644 --- a/app/templates/mainpage_post.html +++ b/app/templates/mainpage_post.html @@ -2,7 +2,7 @@ - Title + 单词词频 {{ yml['header'] | safe }} {% if yml['css']['item'] %} diff --git a/app/templates/reset.html b/app/templates/reset.html index 3425c97..408e001 100644 --- a/app/templates/reset.html +++ b/app/templates/reset.html @@ -12,6 +12,10 @@ alert('输入不能为空!'); return false; } + if (old_password.includes(' ') || new_password.includes(' ')) { + alert('输入不能包含空格!'); + return false; + } if (new_password !== re_new_password) { alert('新密码不匹配,请重新输入'); return false; diff --git a/app/templates/signup.html b/app/templates/signup.html index 9030d41..6b5db6e 100644 --- a/app/templates/signup.html +++ b/app/templates/signup.html @@ -16,6 +16,10 @@ You're logged in already! Logout. alert('输入不能为空!'); return false; } + if (password.includes(' ') || password2.includes(' ')) { + alert('输入不能包含空格!'); + return false; + } if (password !== password2) { alert('确认密码与输入密码不一致!'); return false; @@ -53,7 +57,7 @@ You're logged in already! Logout.

diff --git a/app/templates/userpage_get.html b/app/templates/userpage_get.html index 0488aea..6fab7a5 100644 --- a/app/templates/userpage_get.html +++ b/app/templates/userpage_get.html @@ -23,44 +23,34 @@ EnglishPal Study Room for {{ username }} @@ -69,83 +59,72 @@

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

{% for message in get_flashed_messages() %} - + {% endfor %} - - + +

阅读文章并回答问题

- -

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


+ +

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



-

{{ today_article["article_title"] }}


-

{{ today_article["article_body"] }}


-
-

{{ today_article['source'] }}


-
+

{{ today_article["article_title"] }}


+

{{ today_article["article_body"] }}


+
+

{{ today_article['source'] }}


+
-

{{ today_article['question'] }}


+

{{ today_article['question'] }}


- -
+
- 生词高亮 - 大声朗读 - 划词入库 + 生词高亮 + 大声朗读 + 划词入库
- +

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


- +
{% if session.get['thisWord'] %} - {% endfor %} - {% endif %} + {{ 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 %} - EnglishPal Study Room for {{username}} - - -

取消勾选认识的单词

-
- - {% for x in lst %} - {% set word = x[0]%} -

- {{loop.index}} - : - {{word}} - ({{x[1]}}) - -

+ EnglishPal Study Room for {{username}} + + +
+

+ + +

+ + + {% for x in lst %} + {% set word = x[0]%} +

+ {{loop.index}} + : + {{word}} + ({{x[1]}}) + +

- {% endfor %} - - {{ yml['footer'] | safe }} - {% if yml['js']['bottom'] %} - {% for js in yml['js']['bottom'] %} - - {% endfor %} - {% endif %} - + {% endfor %} + + {{ yml['footer'] | safe }} + {% if yml['js']['bottom'] %} + {% for js in yml['js']['bottom'] %} + + {% endfor %} + {% endif %} +
+ diff --git a/app/test/conftest.py b/app/test/conftest.py index 29f6431..ed4186c 100644 --- a/app/test/conftest.py +++ b/app/test/conftest.py @@ -1,6 +1,9 @@ import pytest +import sqlite3 +import time from selenium import webdriver -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + +from pathlib import Path @pytest.fixture def URL(): @@ -9,5 +12,24 @@ def URL(): @pytest.fixture def driver(): - my_driver = webdriver.Edge() # uncomment this line if you wish to run the test on your laptop - return my_driver + return webdriver.Edge() # follow the "End-to-end testing" section in README.md to install the web driver executable + + +@pytest.fixture +def restore_sqlite_database(): + ''' + Automatically restore SQLite database file app/db/wordfreqapp.db + using SQL statements from app/static/wordfreqapp.sql + ''' + con = sqlite3.connect('../db/wordfreqapp.db') + with con: + con.executescript('DROP TABLE IF EXISTS user;') + con.executescript('DROP TABLE IF EXISTS article;') + con.executescript(open('../static/wordfreqapp.sql', encoding='utf8').read()) + con.close() + + +@pytest.fixture(autouse=True) +def restart_englishpal(restore_sqlite_database): + (Path(__file__).parent / '../main.py').touch() + time.sleep(1) diff --git a/app/test/helper.py b/app/test/helper.py new file mode 100644 index 0000000..2b0deec --- /dev/null +++ b/app/test/helper.py @@ -0,0 +1,33 @@ +import uuid +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import UnexpectedAlertPresentException, NoAlertPresentException + +def signup(URL, driver): + username = 'TestUser' + str(uuid.uuid1()).split('-')[0].title() + password = '[Abc+123]' + + driver.get(URL) + + elem = driver.find_element_by_link_text('注册') + elem.click() + + elem = driver.find_element_by_id('username') + elem.send_keys(username) + + elem = driver.find_element_by_id('password') + elem.send_keys(password) + + elem = driver.find_element_by_id('password2') + elem.send_keys(password) + + elem = driver.find_element_by_class_name('btn') # 找到"注册"按钮 + elem.click() + + try: + WebDriverWait(driver, 1).until(EC.alert_is_present()) + driver.switch_to.alert.accept() + except (UnexpectedAlertPresentException, NoAlertPresentException): + pass + + return username, password diff --git a/app/test/test_add_word.py b/app/test/test_add_word.py index a08c376..6526bf0 100644 --- a/app/test/test_add_word.py +++ b/app/test/test_add_word.py @@ -1,76 +1,31 @@ -# -*- coding: utf-8 -*- -# Run the docker image using the following command: -# docker run -d -p 4444:4444 selenium/standalone-chrome -from selenium import webdriver -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities - -import random, time -import string - -driver = webdriver.Remote('http://localhost:4444/wd/hub', DesiredCapabilities.FIREFOX) -driver.implicitly_wait(10) - -HOME_PAGE = 'http://121.4.94.30:91/' +import time +from helper import signup -def has_punctuation(s): - return [c for c in s if c in string.punctuation] != [] - -def test_add_word(): +def test_add_word(URL, driver): try: - driver.get(HOME_PAGE) - assert 'English Pal -' in driver.page_source - - # login - elem = driver.find_element_by_link_text('登录') - elem.click() - - uname = 'lanhui' - password = 'l0ve1t' - elem = driver.find_element_by_name('username') - elem.send_keys(uname) - - elem = driver.find_element_by_name('password') - elem.send_keys(password) - - elem = driver.find_element_by_xpath('//form[1]/p[3]/input[1]') # 找到登录按钮 - elem.click() - - assert 'EnglishPal Study Room for ' + uname in driver.title - - # get essay content - elem = driver.find_element_by_id('text-content') - essay_content = elem.text - - elem = driver.find_element_by_id('selected-words') - word = random.choice(essay_content.split()) - while 'font>' in word or 'br>' in word or 'p>' in word or len(word) < 6 or has_punctuation(word): - word = random.choice(essay_content.split()) + username, password = signup(URL, driver) # sign up a new account and automatically log in + time.sleep(1) + # enter the word in the text area + elem = driver.find_element_by_id('selected-words') + word = 'devour' elem.send_keys(word) - elem = driver.find_element_by_xpath('//form[1]//input[1]') # 找到get所有词频按钮 - elem.click() - - elems = driver.find_elements_by_xpath("//input[@type='checkbox']") - for elem in elems: - if elem.get_attribute('name') == 'marked': - elem.click() - - elem = driver.find_element_by_name('add-btn') # 找到加入我的生词簿按钮 + elem = driver.find_element_by_xpath('//form[1]//button[1]') # 找到"把生词加入我的生词库"按钮 + elem.click() + + elem = driver.find_element_by_name('add-btn') # 找到"加入我的生词簿"按钮 elem.click() - driver.refresh() - driver.refresh() - driver.refresh() elems = driver.find_elements_by_xpath("//p[@class='new-word']/a") - + found = 0 for elem in elems: if word in elem.text: found = 1 break - + assert found == 1 - finally: + finally: driver.quit() diff --git a/app/test/test_bug528_tangjiao.py b/app/test/test_bug528_tangjiao.py new file mode 100644 index 0000000..802423c --- /dev/null +++ b/app/test/test_bug528_tangjiao.py @@ -0,0 +1,95 @@ +import pytest +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import NoSuchElementException, TimeoutException + + +# 测试登录页面输入密码包含空格的情况 +def test_login_password_with_space(driver, URL): + try: + driver.get(URL+"/login") + + # 输入用户名 + username_elem = driver.find_element_by_id('username') + username_elem.send_keys("test_user") + + # 输入包含空格的密码 + password_elem = driver.find_element_by_id('password') + password_elem.send_keys("password with space") + + # 提交登录表单 + elem = driver.find_element_by_class_name('btn') # 找到提交按钮 + elem.click() + + # 显式等待直到警告框出现 + WebDriverWait(driver, 10).until(EC.alert_is_present()) + + # 检查是否弹出警告框 + alert = driver.switch_to.alert + assert "输入不能包含空格!" in alert.text + except (NoSuchElementException, TimeoutException) as e: + pytest.fail("页面元素未找到或超时: {}".format(e)) + + +# 测试注册页面输入密码包含空格的情况 + +def test_signup_password_with_space(driver, URL): + try: + driver.get(URL+"/signup") + + # 输入用户名 + username_elem = driver.find_element_by_id('username') + username_elem.send_keys("new_user") + + # 输入包含空格的密码 + password_elem = driver.find_element_by_id('password') + password_elem.send_keys("password with space") + + # 再次输入密码 + password2_elem = driver.find_element_by_id('password2') + password2_elem.send_keys("password with space") + + # 提交注册表单 + elem = driver.find_element_by_class_name('btn') # 找到提交按钮 + elem.click() + + # 显式等待直到警告框出现 + WebDriverWait(driver, 10).until(EC.alert_is_present()) + + # 检查是否弹出警告框 + alert = driver.switch_to.alert + assert "输入不能包含空格!" in alert.text + except (NoSuchElementException, TimeoutException) as e: + pytest.fail("页面元素未找到或超时: {}".format(e)) + + + +# 测试重设密码页面输入新密码包含空格的情况 + +def test_reset_password_with_space(driver, URL): + try: + driver.get(URL+"/reset") + + # 输入用户名 + username_elem = driver.find_element_by_id('username') + username_elem.send_keys("test_user") + + # 输入包含空格的密码 + password_elem = driver.find_element_by_id('password') + password_elem.send_keys("password with space") + + # 提交重设密码表单 + elem = driver.find_element_by_class_name('btn') # 找到提交按钮 + elem.click() + + # 显式等待直到警告框出现 + WebDriverWait(driver, 10).until(EC.alert_is_present()) + + # 检查是否弹出警告框 + alert = driver.switch_to.alert + assert "输入不能包含空格!" in alert.text + except (NoSuchElementException, TimeoutException) as e: + pytest.fail("页面元素未找到或超时: {}".format(e)) diff --git a/app/test/test_bug544_tangxinyuan.py b/app/test/test_bug544_tangxinyuan.py new file mode 100644 index 0000000..2cffdd4 --- /dev/null +++ b/app/test/test_bug544_tangxinyuan.py @@ -0,0 +1,55 @@ +import random +import string +import time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from helper import signup + + +def has_punctuation(s): + return any(c in string.punctuation for c in s) + + +def login(driver, home, uname, password): + driver.get(home) + WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, '登录'))).click() + driver.find_element(By.ID, 'username').send_keys(uname) + driver.find_element(By.ID, 'password').send_keys(password) + driver.find_element(By.XPATH, '//button[text()="登录"]').click() + WebDriverWait(driver, 10).until(EC.title_is(f"EnglishPal Study Room for {uname}")) + + +def select_valid_word(driver): + elem = driver.find_element(By.ID, 'text-content') + essay_content = elem.text + valid_word = random.choice([word for word in essay_content.split() if len(word) >= 6 and not has_punctuation( + word) and 'font>' not in word and 'br>' not in word and 'p>' not in word]) + driver.find_element(By.ID, 'selected-words').send_keys(valid_word) + return valid_word + + +def test_save_selected_word(driver, URL): + try: + username, password = signup(URL, driver) + word = select_valid_word(driver) + stored_words = driver.execute_script('return localStorage.getItem("selectedWords");') + assert word == stored_words, "Selected word not saved to localStorage correctly" + # 退出并重新登录以检查存储的单词 + driver.find_element(By.LINK_TEXT, '退出').click() + driver.execute_script("window.open('');window.close();") + + # 等待一会儿,让浏览器有足够的时间关闭标签页 + WebDriverWait(driver, 2) + + # 重新打开一个新的标签页 + driver.execute_script("window.open('');") + driver.switch_to.window(driver.window_handles[-1]) # 切换到新打开的标签页 + + login(driver, URL, username, password) + textarea_content = driver.find_element(By.ID, 'selected-words').get_attribute('value') + assert word == textarea_content, "Selected word not preserved after re-login" + finally: + driver.quit() diff --git a/app/test/test_bug545_HuangHuiLing.py b/app/test/test_bug545_HuangHuiLing.py new file mode 100644 index 0000000..21ddeea --- /dev/null +++ b/app/test/test_bug545_HuangHuiLing.py @@ -0,0 +1,44 @@ +import random +import string +import time +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.action_chains import ActionChains + +from helper import signup + +def has_punctuation(s): + return any(c in string.punctuation for c in s) + +def select_one(driver): + elem = driver.find_element(By.ID, 'article') + essay_content = elem.text + valid_word = random.choice([word for word in essay_content.split() if len(word) >= 6 and not has_punctuation( + word) and 'font>' not in word and 'br>' not in word and 'p>' not in word]) + driver.find_element(By.ID, 'selected-words').send_keys(valid_word) + driver.find_element(By.ID, 'article').click() + return valid_word + +def select_two(driver): + word = driver.find_element(By.CLASS_NAME, 'highlighted') + + # 创建ActionChains对象 + actions = ActionChains(driver) + actions.move_to_element(word) + + # 模拟鼠标按下并拖动以选择文本 + actions.double_click() + actions.perform() + + +def test_selected_second_word(driver, URL): + try: + signup(URL, driver) + selected_words = select_one(driver); + assert selected_words.strip() != "", "选中的单词被放置框中" + select_two(driver) + selected_second_words = driver.find_element(By.ID, 'selected-words').get_attribute('value') + assert selected_second_words.strip() == "", "选中的单词被删除" + finally: + driver.quit() diff --git a/app/test/test_bug546_lixiaofeng.py b/app/test/test_bug546_lixiaofeng.py new file mode 100644 index 0000000..671094b --- /dev/null +++ b/app/test/test_bug546_lixiaofeng.py @@ -0,0 +1,39 @@ +from selenium.webdriver.common.action_chains import ActionChains +from helper import signup + + +def test_highlight(driver, URL): + try: + # 打开网页 + driver.get(URL) + driver.maximize_window() + + # 注册 + signup(URL, driver) + + # 取消勾选“划词入库按钮” + highlight_checkbox = driver.find_element_by_id("chooseCheckbox") + driver.execute_script("arguments[0].click();", highlight_checkbox) + + article = driver.find_element_by_id("article") + + # 创建 ActionChains 对象 + actions = ActionChains(driver) + + # 移动鼠标到起点位置 + actions.move_to_element(article) + # actions.move_to_element_with_offset(article, 50, 100) + # 按下鼠标左键 + actions.click_and_hold() + # 拖动鼠标到结束位置 + actions.move_by_offset(400,50) + # 释放鼠标左键 + actions.release() + # 执行操作链 + actions.perform() + # time.sleep(10) + + assert driver.find_elements_by_class_name("highlighted") is not None + finally: + # 测试结束后关闭浏览器 + driver.quit() \ No newline at end of file diff --git a/app/test/test_bug551_DingZeYu.py b/app/test/test_bug551_DingZeYu.py new file mode 100644 index 0000000..015fb5a --- /dev/null +++ b/app/test/test_bug551_DingZeYu.py @@ -0,0 +1,37 @@ +import time +import pytest +from selenium import webdriver +from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.common.alert import Alert +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait +from helper import signup + +def test_bug551(driver, URL): + driver.maximize_window() + driver.get(URL) + + username, password = signup(URL, driver) + + article = driver.find_element(By.ID, 'article') + actions = ActionChains(driver) + + actions.move_to_element(article) + actions.click_and_hold() + actions.move_by_offset(450, 200) + actions.release() + actions.perform() + + # 获取选中高亮部分的单词的元素 + highlighted_words = driver.find_elements(By.CLASS_NAME, 'highlighted') + + # 验证选中部分的单词是否同时应用了需求样式 + expected_font_weight = "400" + + for word in highlighted_words: + font_weight = word.value_of_css_property("font-weight") + assert font_weight == expected_font_weight, f"选中部分的单词的字体样式错误" + + time.sleep(5) + driver.quit() diff --git a/app/test/test_bug553_LinShan.py b/app/test/test_bug553_LinShan.py new file mode 100644 index 0000000..388f5da --- /dev/null +++ b/app/test/test_bug553_LinShan.py @@ -0,0 +1,58 @@ +from selenium import webdriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.support import expected_conditions as EC +from selenium import webdriver +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +import logging +import time +import pytest + +@pytest.mark.parametrize("test_input,expected", + [("‘test1’", "test1"), + ("'test2'", "test2"), + ("“test3”", "test3"), + ("it's", "it's"), + ("hello,I'm linshan", ["hello","i'm","linshan"]), + ("Happy New Year!?", ["happy","new","year"]), + ("My favorite book is 《Harry Potter》。", ["potter","harry","my","favorite","book","is"])]) +def test_bug553_LinShan(test_input,expected, driver, URL): + try: + # 打开对应地址的网页 + driver.get(URL) + + # 浏览器最大窗口化 + driver.maximize_window() + + # 判断网页源代码中是否有English Pal -文字 + assert 'English Pal -' in driver.page_source + + # 将测试的数据输入到主页的textarea里 + driver.find_element_by_xpath("//textarea[@name='content']").send_keys(Keys.CONTROL, "a") + driver.find_element_by_xpath("//textarea[@name='content']").send_keys(test_input) + time.sleep(1) + + # 点击按钮获取单词 + driver.find_element_by_xpath("//input[@value='get文章中的词频']").click() + time.sleep(1) + + # 获取筛选后的单词 + words = driver.find_elements_by_xpath("//p/a") + + # 遍历获取到的单词,并判断单词与预期的相同 + for word in words: + # 判断单词是否在预期结果中 + assert word.text in expected + + # 返回上一页网页 + driver.find_element_by_xpath("//input[@value='确定并返回']").click() + time.sleep(0.1) + + except Exception as e: + # 输出异常信息 + logging.error(e) + # 关闭浏览器 + driver.quit() + finally: + driver.quit() diff --git a/app/test/test_bug561_LiangZiyue.py b/app/test/test_bug561_LiangZiyue.py new file mode 100644 index 0000000..368bc0b --- /dev/null +++ b/app/test/test_bug561_LiangZiyue.py @@ -0,0 +1,27 @@ +import random +import string +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +def test_bug561_LiangZiyue(driver, URL): + try: + driver.get(home) + WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, '登录'))).click() + driver.find_element(By.ID, 'username').send_keys("wrr") + driver.find_element(By.ID, 'password').send_keys("1234") + driver.find_element(By.XPATH, '//button[text()="登录"]').click() + ele = driver.find_element(By.XPATH,'//font[@id="article"]') + driver.execute_script('arguments[0].scrollIntoView();',ele) + action = ActionChains(driver) + action.click_and_hold(ele) + action.move_by_offset(0,500) + action.perform() + next_ele = driver.find_element(By.ID,'//button[@id="load_next_article"]') + driver.execute_script('arguments[0].scrollIntoView();',next_ele) + next_ele.click() + driver.execute_script('arguments[0].scrollIntoView();',ele) + ele.click() + finally: + driver.quit() \ No newline at end of file diff --git a/app/test/test_stress.py b/app/test/test_stress.py new file mode 100755 index 0000000..b437b86 --- /dev/null +++ b/app/test/test_stress.py @@ -0,0 +1,43 @@ +''' Contributed by Lin Junhong et al. 2023-06.''' + +import requests +import multiprocessing +import time + +def stress(username): + try: + data = { + 'username': username, + 'password': '123123' + } + headers = { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36 Edg/114.0.1823.51' + } + session = requests.session() + response = session.post(url='http://127.0.0.1:5000/signup', data=data, headers=headers) + print('Sign up ', response.status_code) + time.sleep(0.5) + response = session.post(url='http://127.0.0.1:5000/login', data=data, headers=headers) + print('Sign in ', response.status_code) + time.sleep(0.5) + response = session.get(url=f'http://127.0.0.1:5000/{username}/userpage', headers=headers) + print('User page', response.status_code) + time.sleep(0.5) + print(session.cookies) + for i in range(5): + response = session.get(url=f'http://127.0.0.1:5000/get_next_article/{username}', headers=headers, cookies=session.cookies) + time.sleep(0.5) + print(f'Next page ({i}) [{username}]') + print(response.status_code) + print(response.json()['today_article']['article_title']) + except Exception as e: + print(e) + + +if __name__ == '__main__': + username = 'Learner' + pool = multiprocessing.Pool(processes=10) + for i in range(10): + pool.apply_async(stress, (f'{username}{i}',)) + pool.close() + pool.join() diff --git a/app/user_service.py b/app/user_service.py index 2e5feed..27323b8 100644 --- a/app/user_service.py +++ b/app/user_service.py @@ -15,6 +15,9 @@ from wordfreqCMD import sort_in_descending_order import pickle_idea import pickle_idea2 +import logging +logging.basicConfig(filename='log.txt', format='%(asctime)s %(message)s', level=logging.DEBUG) + # 初始化蓝图 userService = Blueprint("user_bp", __name__) @@ -32,7 +35,9 @@ def get_next_article(username): else: # 当前不为“null”,直接 index+=1 visited_articles["index"] += 1 session["visited_articles"] = visited_articles + logging.debug('/get_next_article: start calling get_today_arcile()') visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles')) + logging.debug('/get_next_arcile: done.') data = { 'visited_articles': visited_articles, 'today_article': today_article, @@ -129,7 +134,7 @@ def userpage(username): user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username) if request.method == 'POST': # when we submit a form - content = escape(request.form['content']) + content = request.form['content'] f = WordFreq(content) lst = f.get_freq() return render_template('userpage_post.html',username=username,lst = lst, yml=Yaml.yml) @@ -176,7 +181,11 @@ def user_mark_word(username): for word in request.form.getlist('marked'): lst.append((word, [get_time()])) d = pickle_idea2.merge_frequency(lst, lst_history) - pickle_idea2.save_frequency_to_pickle(d, user_freq_record) + if len(lst_history) > 999: + flash('You have way too many words in your difficult-words book. Delete some first.') + else: + pickle_idea2.save_frequency_to_pickle(d, user_freq_record) + flash('Added %s.' % (', '.join(request.form.getlist('marked')))) return redirect(url_for('user_bp.userpage', username=username)) else: return 'Under construction' diff --git a/app/wordfreqCMD.py b/app/wordfreqCMD.py index dcee74e..feeafbd 100644 --- a/app/wordfreqCMD.py +++ b/app/wordfreqCMD.py @@ -4,6 +4,7 @@ ########################################################################### import collections +import html import string import operator import os, sys # 引入模块sys,因为我要用里面的sys.argv列表中的信息来读取命令行参数。 @@ -39,7 +40,8 @@ def file2str(fname):#文件转字符 def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。 - special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' # 把里面的字符都去掉 + special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|,。?!¥……()、《》:;·' # 把里面的字符都去掉 + s = html.unescape(s) # 将HTML实体转换为对应的字符,比如<会被识别为小于号 for c in special_characters: s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况 s = s.replace('--', ' ') @@ -104,7 +106,7 @@ if __name__ == '__main__': print('%s\t%d\t%s' % (x[0], x[1], youdao_link(x[0])))#函数导出 # 把频率的结果放result.html中 - make_html_page(sort_in_descending_order(L), 'result.html') + make_html_page(sort_in_descending_order(L), 'result.html') print('\nHistory:\n') if os.path.exists('frequency.p'): diff --git a/build.sh b/build.sh index e313fce..158a86f 100755 --- a/build.sh +++ b/build.sh @@ -2,10 +2,7 @@ DEPLOYMENT_DIR=/home/lanhui/englishpal2/EnglishPal cd $DEPLOYMENT_DIR - -# Install dependencies - -pip3 install -r requirements.txt +pwd # Stop service sudo docker stop EnglishPal @@ -15,7 +12,7 @@ sudo docker rm EnglishPal sudo docker build -t englishpal . # Run the application -sudo docker run --restart=always -d --name EnglishPal -p 90:80 -v ${DEPLOYMENT_DIR}/app/static/frequency:/app/static/frequency -v ${DEPLOYMENT_DIR}/app/static/:/app/static/ -t englishpal # for permanently saving data +sudo docker run --restart=always -d --name EnglishPal -p 90:80 -v ${DEPLOYMENT_DIR}/app/static/frequency:/app/static/frequency --mount type=volume,src=englishpal-db,target=/app/db -t englishpal # for permanently saving data # Save space. Run it after sudo docker run sudo docker system prune -a -f diff --git a/da b/da new file mode 100644 index 0000000..0ff8001 --- /dev/null +++ b/da @@ -0,0 +1,391 @@ +commit 8cbc7c9a0ce543db48f80a743c4168ca847ca500 (HEAD -> Alpha-snapshot20240618, origin/master, origin/HEAD, master) +Author: lisinan <1299311192@qq.com> +Date: Fri May 24 22:00:08 2024 +0800 + + 修复快速点击下一页按钮点击频率过快时页面跳转到未知名页面 + +commit ff6286cf01bccdda793ec626c66594e43d81cb16 (origin/547, origin/542) +Author: 丁晟晔 <759539128@qq.com> +Date: Mon May 6 11:42:32 2024 +0800 + + 删除 app/test/test_bug551_DingZeYu.py + +commit 1d7e61d7519342396bec9c64cdd8090c1515cec7 +Author: 丁晟晔 <759539128@qq.com> +Date: Mon May 6 11:36:36 2024 +0800 + + 上传文件至 app/test + +commit 708a6a282102c2b60e3abfade633389bc1f03ddb (origin/Bosh) +Merge: 43c719b 688a198 +Author: 顾涵 <1554137355@qq.com> +Date: Sun Jun 4 12:39:34 2023 +0800 + + Merge pull request 'WIP:Bug529-GuHan' (#88) from Bug529-GuHan into master + + Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/88 + +commit 688a19876823e818f3f312f865f4921f28f28c53 (origin/Bug529-GuHan) +Merge: 030b897 1543b30 +Author: ghaa0920 <1554137355@qq.com> +Date: Sun May 28 16:31:12 2023 +0800 + + 已经与Alpha-snapshot20230525 分支同步,重新提交 + +commit 1543b3095dde2a295293af1ea7e495d6ba005f8d (origin/Refactor-XunYucan, origin/Alpha-snapshot20230525) +Merge: c6bf323 b41e104 +Author: Xunflash +Date: Thu May 25 22:30:06 2023 +0800 + + Merge remote-tracking branch 'origin/Alpha-snapshot20230519' into Refactor-XunYucan + +commit c6bf323c6084255abcde0c47ae1a0504bd372475 +Author: Xunflash +Date: Thu May 25 21:23:25 2023 +0800 + + 修改格式 + +commit 03ccb3527a94a3c463b4bffe24ba0f579f286d5f +Author: Xunflash +Date: Thu May 25 17:35:31 2023 +0800 + + 重构前端阅读js,新增阅读器全局对象,新增生词朗读按钮 + +commit b41e1044bce3e4d9169b7743ce9aad74c0fec7d7 (origin/Alpha-snapshot20230519) +Author: Hui Lan +Date: Wed May 24 10:12:44 2023 +0800 + + difficulty.py: add some stop words, hoping that getting the next article can be faster. + +commit 67e921ba60df76640005fa2838d80808b311bc2d +Author: Hui Lan +Date: Tue May 23 22:25:40 2023 +0800 + + difficulty.py: todo. + +commit a5c3564f15cea5ddb5f79ba15bf888a332698440 +Author: Hui Lan +Date: Tue May 23 22:22:57 2023 +0800 + + difficulty.py: do not stem a word twice. + +commit 1295616d5be485475b06e456458a3b8b7a092a50 +Merge: 3494881 c151a0e +Author: Hui Lan +Date: Tue May 23 19:50:30 2023 +0800 + + Merge branch 'Bug476-YuHuangtao' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230519 + +commit c151a0efaa109aa806c40105cb86d1b3950458ea (origin/Bug476-YuHuangtao) +Author: PlutoCtx +Date: Tue May 23 19:40:33 2023 +0800 + + 去掉了get_difficulty_level_for_user的多出的break + +commit 030b89706e51a35d95eb6eafa7ae2b2fefac821b +Author: ghaa0920 <1554137355@qq.com> +Date: Sat May 20 15:29:12 2023 +0800 + + special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' 用于过滤字符,我将其中的“-”删去,使连字符没有被过滤,实现录入例如fifty-six等组合词的功能。另外对于删除过滤是否会引发字符bug,答案是肯定的,但是这段代码中的过滤字符虽然多,但是并没有完全过滤掉所有字符,(过滤的只是键盘上能打出的字符,不包括输入法中能打出的特殊字符),所以字符bug本身就一直存在,我认为减少一个“-”字符对程序的过滤过程不会造成问题。 + +commit 349488167b3c1cbdf6116adff5798cf356d94af2 +Author: Hui Lan +Date: Fri May 19 09:03:20 2023 +0800 + + requirements.txt: install snowballstemmer for better computing a word's difficulty level. + +commit 39d96014d90892faf22b0830ad8d78aeaaa66d96 +Author: PlutoCtx +Date: Thu May 18 23:29:38 2023 +0800 + + pull最新的snapshot-20230511,后更新了difficulty.py和Article.py的部分代码,提交了新的pickle文件 + +commit acd8db6e3e1c498650f2e8cc3021828e6132e823 +Author: ghaa0920 <1554137355@qq.com> +Date: Mon May 15 19:24:43 2023 +0800 + + special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' 用于过滤字符,我将其中的“-”删去,使连字符没有被过滤,实现录入例如fifty-six等组合词的功能。另外对于删除过滤是否会引发字符bug,答案是肯定的,但是这段代码中的过滤字符虽然多,但是并没有完全过滤掉所有字符,(过滤的只是键盘上能打出的字符,不包括输入法中能打出的特殊字符),所以字符bug本身就一直存在,我认为减少一个对“1-”字符的过滤不会造成问题。 + +commit 9f3f5b43e1bc79e884091593b9b8f3d6a2164ea5 +Author: ghaa0920 <1554137355@qq.com> +Date: Mon May 15 19:15:30 2023 +0800 + + special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' 用于过滤字符,我将其中的“-”删去,使连字符没有被过滤,实现录入例如fifty-six等组合词的功能。另外对于删除过滤是否会引发字符bug,答案是肯定的,但是这段代码中的过滤字符虽然多,但是并没有完全过滤掉所有字符,(过滤的只是键盘上能打出的字符,不包括输入法中能打出的特殊字符),所以字符bug本身就一直存在,我认为减少一个对“-”字符的过滤不会造成问题。 + +commit d9f6df7fbe585395a19b9a08c411d841b6b89fd4 (origin/Bug532-HuangDan) +Author: huangdan <2741654266@qq.com> +Date: Thu May 11 15:51:10 2023 +0800 + + AJAX载入文章数据 + +commit 5039f5710e4575ecbd9c5bfcc2e349dbe6025bfa (origin/Alpha-snapshot20230507) +Author: huangdan <2741654266@qq.com> +Date: Mon May 8 14:33:48 2023 +0800 + + AJAX载入文章数据 + +commit becef7e3436c93561ba581cff9b9ca9da118c888 (origin/Alpha-snapshot20230506) +Merge: a4cc4fd 01ecc83 +Author: Hui Lan +Date: Sun May 7 15:59:35 2023 +0800 + + Merge branch 'Bug502-YuGaoXiang' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230506 + +commit 01ecc8376893404cb026e34e7f8fecbefcd082cf (origin/Bug502-YuGaoXiang) +Author: Awoodwhale +Date: Sat May 6 17:42:04 2023 +0800 + + refactor: refactor the way to check article level + +commit f64d06fbbf00255fc21f141d852d8995e588785e +Author: Awoodwhale +Date: Sat May 6 17:24:51 2023 +0800 + + fix: fix Bug 531 and use ES6 grammar + +commit a4cc4fd011317f3daec2de456b95abca16841663 +Merge: 779dafe 18ca48b +Author: Hui Lan +Date: Sat May 6 17:16:08 2023 +0800 + + Merge branch 'Bug522-HuangZirui' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230506 + +commit 18ca48b42214720f411937b01c20550a5f96580b +Merge: a80b062 ce2e1f2 +Author: ZhuZhihao <1287365321@qq.com> +Date: Fri May 5 17:21:49 2023 +0800 + + Merge branch 'Bug522-HuangZirui' of http://121.4.94.30:3000/mrlan/EnglishPal into Bug522-HuangZirui + +commit a80b062b8707fb6a73554b8500b8c5ae17775ed3 +Author: ZhuZhihao <1287365321@qq.com> +Date: Fri May 5 17:20:58 2023 +0800 + + refactor: remove variable 'count' + +commit 779dafefe8a3ae5c40b3376d2b0309c0ddcd3273 (origin/Alpha-snapshot20230427) +Merge: e118d92 d30a434 +Author: Hui Lan +Date: Thu Apr 27 07:21:15 2023 +0800 + + Merge branch 'Bug509-XieQiuHan-WangZiming' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230427 + +commit e118d92659911db4f3bd047a67865522c8b00c66 +Merge: ce2e1f2 5654fbf +Author: Hui Lan +Date: Thu Apr 27 07:20:21 2023 +0800 + + Merge branch 'Alpha-snapshot20230425' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230427 + +commit 5654fbf9bc3ae85eec621a4e6e2ecd1f6c6c3b30 (origin/Alpha-snapshot20230425) +Author: 一问三不知 <178428409@qq.com> +Date: Wed Apr 26 18:49:59 2023 +0800 + + 修改:使用新的//userpage路由 + +commit d30a434b2a0dce5247bb005c5c245529d5a542d4 (origin/Bug509-XieQiuHan-WangZiming) +Author: 一问三不知 <178428409@qq.com> +Date: Tue Apr 25 17:47:51 2023 +0800 + + 修改变量名had_read_articles->visited_articles + +commit b88bc8f36b395f3979d4a9b37993b88b8a9eb658 +Merge: ef78679 6be035f +Author: Hui Lan +Date: Tue Apr 25 11:40:42 2023 +0800 + + Merge branch 'Bug509-XieQiuHan-WangZiming' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230425 + +commit 6be035f2828286b5cd59f15ec04336b86c3cb0fa +Author: 一问三不知 <178428409@qq.com> +Date: Tue Apr 25 11:38:01 2023 +0800 + + 修复当没有找到文章或者文章读完时,直接刷新页面或者session不关闭重新进入页面,导致的错误; + +commit ef786795e27287502b5388e2e80aa83eac596c8d +Merge: 21a77ef fc3e274 +Author: Hui Lan +Date: Tue Apr 25 08:47:22 2023 +0800 + + Resolve merge conflict + +commit 21a77ef2df5111ca4fb0103624ee0b68a15b2a06 +Merge: 58d7349 f3d609c +Author: Hui Lan +Date: Tue Apr 25 08:42:18 2023 +0800 + + Merge branch 'Alpha' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha + +commit 58d7349afe69c5934754cb7a5145b33382326c37 +Author: Hui Lan +Date: Tue Apr 25 08:40:26 2023 +0800 + + Change from bug359-zhangkeli + +commit fc3e27488b3b55c1013a970e6abb95b88caff9aa +Author: 一问三不知 <178428409@qq.com> +Date: Fri Apr 21 05:33:26 2023 +0800 + + 给标签添加id,方便测试 + +commit 03145b57d98c29ae04948281e8e4e67512e75829 +Author: 一问三不知 <178428409@qq.com> +Date: Fri Apr 21 02:36:51 2023 +0800 + + 修复边界值问题(当刚开始就没有找到文章或者就根本被没有文章的时候,会出现上一篇按钮) + +commit 70917df47b91f7fbb16870b15f2a2ecdbdad6135 +Author: 一问三不知 <178428409@qq.com> +Date: Thu Apr 20 23:15:12 2023 +0800 + + 删除测试代码 + +commit 8f132ed87bfc00e321214dd4a4d6ed2a3ff4a8cd +Author: 一问三不知 <178428409@qq.com> +Date: Thu Apr 20 22:53:30 2023 +0800 + + 添加了阅读完所有文章的提示 + +commit da13e5bbd5da2a1ceb838c5a4836a0cabd8bdf69 +Author: 一问三不知 <178428409@qq.com> +Date: Thu Apr 20 21:28:29 2023 +0800 + + 修复Bug(没找到文章后立即上一篇会回到上上篇文章) & 标签添加id方便测试 + +commit 84affaeb69fdecb155cdf4daa4f57c4b29dbea55 +Author: 一问三不知 <178428409@qq.com> +Date: Thu Apr 20 20:30:14 2023 +0800 + + 修改 / 路由存在的问题(每次调用别的路由他都会被调用),新路由为 //userpage;同时因为修改了路由导致访问userpage_get的时候会导致静态文件路径生成错误,这里修改了\static\config.yml中的静态资源路径,修改后也都可以正常访问到的 + +commit 16de0a7fd99a26d331e23eac7c1fd22594b7c50f +Author: 一问三不知 <178428409@qq.com> +Date: Thu Apr 20 15:40:11 2023 +0800 + + 修改变量命名:existing_articles → had_read_articles + +commit ce2e1f2978e68aac46a03e0d7bc5e27b78a1106a (origin/Alpha-snapshot20230426) +Merge: 11ae093 cc8ca47 +Author: zzhaofisher <10839192+zzhaofisher@user.noreply.gitee.com> +Date: Tue Apr 18 21:52:28 2023 +0800 + + Merge branch 'DevLocal' into Bug522-HuangZirui + +commit 11ae093fd7b714b37bd26f42ad9f5b042ae1dfc1 +Merge: 3bce450 f3d609c +Author: zzhaofisher <10839192+zzhaofisher@user.noreply.gitee.com> +Date: Tue Apr 18 21:52:01 2023 +0800 + + Merge branch 'Alpha' into Bug522-HuangZirui + +commit cc8ca47f8c96eedaf2ce7ec61f5480123663953f +Author: zzhaofisher <10839192+zzhaofisher@user.noreply.gitee.com> +Date: Tue Apr 18 21:50:54 2023 +0800 + + refactor: remove sql sentences + +commit 5d20e92061d3c43dab01e0ca9483779fc9b9a9a2 +Merge: f3d609c 3bce450 +Author: zzhaofisher <10839192+zzhaofisher@user.noreply.gitee.com> +Date: Tue Apr 18 21:50:18 2023 +0800 + + Merge branch 'Bug522-HuangZirui' of http://121.4.94.30:3000/mrlan/EnglishPal into DevLocal + +commit f3d609c92bfd27ed4ef70cab838a7d1362a5fb6b (origin/Alpha) +Merge: 688ed72 15bb925 +Author: Lan Hui <1348141770@qq.com> +Date: Fri Apr 7 06:41:49 2023 +0800 + + Merge Wang Ziming's work and Wu Yuhan's work. + +commit 15bb9250248aa6feb249be72c53d3fa6d7005d52 +Author: 一问三不知 <178428409@qq.com> +Date: Tue Apr 4 22:31:53 2023 +0800 + + 将记录阅读过文章的数据结果改为字典,以及修改了flag的问题 + +commit 688ed724734fddb4a07e9dd0dc5b6b2ace16e645 +Author: Lan Hui <1348141770@qq.com> +Date: Sat Apr 1 16:07:59 2023 +0800 + + Correct grammar。 + +commit 1f150fc847d0fc70947261ff45843ccc116117c2 +Author: Awoodwhale +Date: Fri Mar 31 13:39:28 2023 +0800 + + refactor: use ajax to get expiry_date + +commit 7107f634c2ec9fa12dd6bcbed9f4ee5c339928c7 +Merge: 6f1dd13 4417cf7 +Author: 一问三不知 <178428409@qq.com> +Date: Fri Mar 31 04:58:21 2023 +0800 + + Merge branch 'test' into dev-fixBug509-reconstruction + +commit 6f1dd134193cb3f9fa1d3502b286d3be1f22c1ed +Author: 一问三不知 <178428409@qq.com> +Date: Fri Mar 31 04:50:41 2023 +0800 + + 测试的print忘删了 + +commit 4417cf7017d50f20d99e1a274b48ad5d3b32e8e6 +Author: Hui Lan +Date: Thu Mar 30 16:10:22 2023 +0800 + + Article.py: remove debug statement. + +commit 0c16a4dc6f21849915cdad92c7189e37ccb9a289 +Author: 一问三不知 <178428409@qq.com> +Date: Wed Mar 29 20:53:38 2023 +0800 + + 判断文章是否已经出现的语句写错位置了,改正下 + +commit 5b2f5199a82be7cdb7bb3ac57f7b54b5ea0f4034 +Author: 一问三不知 <178428409@qq.com> +Date: Mon Mar 27 14:28:54 2023 +0800 + + 1. 取消userpage_get.html中提示删除单词信息的代码 和 取消user_service.userpage中render_template的flashed_messages参数。因为删除单词操作已经是异步了,而提示信息的出现是同步执行,所以就注释了代码且没有产生太大影响。 + 2. 修改取消user_service.deleteword中对注释flash代码的注释,根据上一步进行了重新解释。 + +commit 0ce1c6eb6e5707ccfb47113b2c58c9961f2eae48 +Author: Lan Hui <1348141770@qq.com> +Date: Sun Mar 26 21:14:29 2023 +0800 + + 文章管理页面:每篇文章中保留换行,方便查看。 + +commit d4ac7093859d400f77f24c7cf5b6d39dd67b2e9a +Author: Lan Hui <1348141770@qq.com> +Date: Sun Mar 26 21:05:05 2023 +0800 + + 将删除按钮移到第一行,避免因为文章的标题过长跨行导致删除按钮形状改变。 + +commit 9eb5210d3f11211abe756597518d09b1cfd8f3e4 +Author: Lan Hui <1348141770@qq.com> +Date: Sun Mar 26 20:58:37 2023 +0800 + + Level与Date的冒号后面加个空格,使得后面的信息更加看得清楚。 + +commit 0e257373816ee365d41e78b0b13ba807e0230aac +Author: Lan Hui <1348141770@qq.com> +Date: Sun Mar 26 20:56:08 2023 +0800 + + 管理文章页面的文章列表中,每篇文章不再在内容部分重新显示标题。 + +commit b3b154a24f9bb267146712e6020903a6e574c976 +Author: Lan Hui <1348141770@qq.com> +Date: Sun Mar 26 19:06:04 2023 +0800 + + 简化管理文章与管理用户页面信息。 + +commit 4d99405bfaf2f87abdf4bfa1291083419c6df502 +Author: Lan Hui <1348141770@qq.com> +Date: Sun Mar 26 18:59:15 2023 +0800 + + 简化管理员页面信息。删除退出登录按钮,可以返回到前一页后再退出,不影响使用体验。删除'管理员您好'欢迎词,没啥意义。 + +commit 8d8b9197b614b595bbba1ecf95c189ea1389780c +Author: Lan Hui <1348141770@qq.com> +Date: Sun Mar 26 09:59:06 2023 +0800 + + 手动输入的文字最高难度等级是4 diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..216ea36b1cc1f126f4c032eedd709ce0bca7bc6b GIT binary patch literal 4446 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 abn#%@oL0tEQ<3s+0x-R5VN`A468AqUim=lF literal 0 HcmV?d00001