1
0
Fork 0

Compare commits

..

2 Commits

Author SHA1 Message Date
于洋 930f07d2fa 上传文件至 /
修改后的vocabulary.py
2025-05-29 14:39:08 +08:00
李思楠 8cbc7c9a0c 修复快速点击下一页按钮点击频率过快时页面跳转到未知名页面 2024-05-24 22:00:08 +08:00
42 changed files with 618 additions and 759 deletions

12
.gitignore vendored
View File

@ -2,20 +2,12 @@
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/wordfreqapp.db
app/db/wordfreqapp.db
app/static/wordfreqapp.db
app/static/donate-the-author.jpg
app/static/donate-the-author-hidden.jpg
app/model/__pycache__/
app/test/__pycache__/
app/test/.pytest_cache/
app/test/pytest_report.html
app/test/assets
app/log.txt
app/model/__pycache__/

View File

@ -1,5 +1,4 @@
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/
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

4
Jenkinsfile vendored
View File

@ -10,8 +10,8 @@ pipeline {
stages {
stage('MakeDatabasefile') {
steps {
sh 'touch ./app/wordfreqapp.db && rm -f ./app/wordfreqapp.db'
sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/wordfreqapp.db'
sh 'touch ./app/static/wordfreqapp.db && rm -f ./app/static/wordfreqapp.db'
sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/static/wordfreqapp.db'
}
}
stage('BuildIt') {

View File

@ -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/db/wordfreqapp.db`.
`app/static/wordfreqapp.db`.
### Adding new articles
To add articles, open and edit `app/db/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org).
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/db/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/static/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/db/`
`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/static`
@ -129,28 +129,6 @@ 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

View File

@ -1,5 +1,6 @@
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
@ -7,15 +8,18 @@ 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 = './'
db_path_prefix = './db/' # comment this line in deployment
path_prefix = '/var/www/wordfreq/wordfreq/'
path_prefix = './' # comment this line in deployment
def total_number_of_essays():
return get_number_of_articles()
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
rq.instructions("SELECT * FROM article")
rq.do()
result = rq.get_results()
return len(result)
def get_article_title(s):
@ -29,36 +33,32 @@ 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: # 生成新的文章,因此查找所有的文章
result = get_all_articles()
rq.instructions("SELECT * FROM article")
else: # 生成阅读过的文章,因此查询指定 article_id 的文章
if visited_articles["article_ids"][visited_articles["index"]] == 'null': # 可能因为直接刷新页面导致直接去查询了'null',因此当刷新的页面的时候,需要直接进行“上一篇”操作
visited_articles["index"] -= 1
visited_articles["article_ids"].pop()
article_id = visited_articles["article_ids"][visited_articles["index"]]
result = get_article_by_id(article_id)
rq.instructions('SELECT * FROM article WHERE article_id=%d' % (visited_articles["article_ids"][visited_articles["index"]]))
rq.do()
result = rq.get_results()
random.shuffle(result)
# Choose article according to reader's level
logging.debug('* get_today_article(): start d1 = ... ')
d1 = load_freq_history(user_word_list)
d1 = load_freq_history(path_prefix + 'static/frequency/frequency.p')
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.1f' % user_level,
"text_level": '%4.1f' % text_level,
"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']),

View File

@ -1,6 +1,7 @@
import hashlib
import string
from datetime import datetime, timedelta
from UseSqlite import InsertQuery, RecordQuery
def md5(s):
'''

87
app/UseSqlite.py Normal file
View File

@ -0,0 +1,87 @@
###########################################################################
# Copyright 2019 (C) Hui Lan <hui.lan@cantab.net>
# 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())

View File

@ -1,5 +1,4 @@
from flask import *
from markupsafe import escape
from Login import check_username_availability, verify_user, add_user, get_expiry_date, change_password, WarningMessage

View File

@ -1,6 +1,5 @@
# System Library
from flask import *
from markupsafe import escape
# Personal library
from Yaml import yml
@ -38,22 +37,6 @@ 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 = '<br/>'.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()
@ -61,15 +44,20 @@ 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 parameters must be integer!"
return "page parmas must be int!"
_articles = get_page_articles(_cur_page, _page_size)
_make_title_and_content(_articles)
for article in _articles: # 获取每篇文章的title
article.title = article.text.split("\n")[0]
article.content = '<br/>'.join(article.text.split("\n")[1:])
context = {
"article_number": _article_number,
@ -79,16 +67,23 @@ 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 integer!"
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", "")
@ -102,7 +97,6 @@ def article():
_update_context()
title = content.split('\n')[0]
flash(f'Article added. Title: {title}')
return render_template("admin_manage_article.html", **context)

View File

@ -1 +0,0 @@
Put wordfreqapp.db here

View File

@ -18,7 +18,6 @@ def load_record(pickle_fname):
return d
ENGLISH_WORD_DIFFICULTY_DICT = {}
def convert_test_type_to_difficulty_level(d):
"""
对原本的单词库中的单词进行难度评级
@ -40,11 +39,9 @@ def convert_test_type_to_difficulty_level(d):
elif 'BBC' in d[k]:
result[k] = 8
global ENGLISH_WORD_DIFFICULTY_DICT
ENGLISH_WORD_DIFFICULTY_DICT = result
return result # {'apple': 4, ...}
def get_difficulty_level_for_user(d1, d2):
"""
d2 来自于词库的35511个已标记单词
@ -52,11 +49,7 @@ 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.
if ENGLISH_WORD_DIFFICULTY_DICT == {}:
d2 = convert_test_type_to_difficulty_level(d2) # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...}
else:
d2 = ENGLISH_WORD_DIFFICULTY_DICT
d2 = convert_test_type_to_difficulty_level(d2) # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...}
stemmer = snowballstemmer.stemmer('english')
for k in d1: # 用户的词

View File

@ -1,19 +1,19 @@
#! /usr/bin/python3
# -*- coding: utf-8 -*-
###########################################################################
# Copyright 2019 (C) Hui Lan <hui.lan@cantab.net>
# Written permission must be obtained from the author for commercial uses.
###########################################################################
from flask import abort
from markupsafe import escape
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
import os
app = Flask(__name__)
app.secret_key = os.urandom(32)
app.secret_key = 'lunch.time!'
# 将蓝图注册到Lab app
app.register_blueprint(userService)
@ -54,6 +54,7 @@ def appears_in_test(word, d):
else:
return ','.join(d[word])
@app.route("/mark", methods=['GET', 'POST'])
def mark_word():
'''

View File

@ -1,7 +1,7 @@
from pony.orm import *
db = Database()
db.bind("sqlite", "../db/wordfreqapp.db", create_db=True) # bind sqlite file
db.bind("sqlite", "../static/wordfreqapp.db", create_db=True) # bind sqlite file
class User(db.Entity):

View File

@ -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,17 +32,3 @@ 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()]

View File

@ -2,14 +2,13 @@
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

View File

@ -1,5 +0,0 @@
.highlighted {
color: red;
font-weight: normal;
}

View File

@ -1,5 +0,0 @@
function toggleCheckboxSelection(checkStatus) {
// used in userpage_post.html
const checkBoxes = document.getElementsByName('marked');
checkBoxes.forEach((checkbox) => { checkbox.checked = checkStatus;} );
}

View File

@ -1,5 +1,5 @@
let isRead = localStorage.getItem('readChecked') !== 'false'; // default to true
let isChoose = localStorage.getItem('chooseChecked') !== 'false';
let isRead = true;
let isChoose = true;
function getWord() {
return window.getSelection ? window.getSelection() : document.selection.createRange().text;
@ -10,9 +10,7 @@ function fillInWord() {
if (isRead) Reader.read(word, inputSlider.value);
if (!isChoose) 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);
@ -26,16 +24,8 @@ 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();
}

View File

@ -1,4 +1,4 @@
let isHighlight = localStorage.getItem('highlightChecked') !== 'false'; // default to true
let isHighlight = true;
function cancelBtnHandler() {
cancelHighlighting();
@ -22,46 +22,62 @@ function getWord() {
function highLight() {
if (!isHighlight) return;
let word = (getWord() + "").trim();
let articleContent = document.getElementById("article").innerHTML; // innerHTML保留HTML标签来保持部分格式且适配不同的浏览器
let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml使用innerText会把原文章中所包含的<br>标签去除,导致处理后的文章内容失去了原来的格式
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 = dictionaryWords === null ? pickedWords.value + " " : pickedWords.value + " " + dictionaryWords.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('<span class="highlighted">' + word + '</span>', "gi"), word);
pickedWords.value = localStorage.getItem("nowWords").replace(word,"");
document.getElementById("article").innerHTML = articleContent;
return;
}
let allWords = ""; //初始化allWords的值避免进入判断后编译器认为allWords未初始化的问题
if(dictionaryWords != null){//增加一个判断检查生词本里面是否为空如果为空allWords只添加选中的单词
allWords = pickedWords.value + " " + dictionaryWords.value;
}
let totalSet = new Set();
else{
allWords = pickedWords.value + " ";
}
const list = allWords.split(" ");//将所有的生词放入一个list中用于后续处理
for (let i = 0; i < list.length; ++i) {
list[i] = list[i].replace(/(^\W*)|(\W*$)/g, ""); // 消除单词两边的非单词字符
list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); //消除单词两边的空字符
list[i] = list[i].replace('|', "");
list[i] = list[i].replace('?', "");
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]);
if (list[i] !== "" && "<mark>".indexOf(list[i]) === -1 && "</mark>".indexOf(list[i]) === -1) {
//将文章中所有出现该单词word的地方改为"<mark>" + word + "<mark>"。 正则表达式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"),"<mark>" + list[i] + "</mark>");
}
}
}
// 删除所有的"<span class='highlighted'>"标签,防止标签发生嵌套
articleContent = articleContent.replace(new RegExp('<span class="highlighted">',"gi"), "")
articleContent = articleContent.replace(new RegExp("</span>","gi"), "");
// 将文章中所有出现该单词word的地方改为"<span class='highlighted'>" + word + "</span>"。
for (let word of totalSet) {
articleContent = articleContent.replace(new RegExp("\\b" + word + "\\b", "g"), "<span class='highlighted'>" + word + "</span>");
}
document.getElementById("article").innerHTML = articleContent;
}
function cancelHighlighting() {
let articleContent = document.getElementById("article").innerHTML;
articleContent = articleContent.replace(new RegExp('<span class="highlighted">',"gi"), "")
articleContent = articleContent.replace(new RegExp("</span>","gi"), "");
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("<mark>"+list[i]+"</mark>", "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("<mark>"+list2[i]+"</mark>", "g"), list2[i]);
}
}
}
document.getElementById("article").innerHTML = articleContent;
}
@ -81,7 +97,6 @@ function toggleHighlighting() {
isHighlight = true;
highLight();
}
localStorage.setItem('highlightChecked', isHighlight);
}
showBtnHandler();
showBtnHandler();

View File

@ -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
};
}) ();
})();

View File

@ -5,14 +5,15 @@ 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) {
@ -32,11 +33,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);
}
@ -50,7 +51,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);
@ -113,7 +114,7 @@ function removeWord(word) {
// 根据词频信息删除元素
word = word.replace('&amp;', '&');
const element_to_remove = document.getElementById(`p_${word}`);
if (element_to_remove !== null) {
if (element_to_remove != null) {
element_to_remove.remove();
}
}
@ -128,7 +129,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;
@ -164,11 +165,17 @@ function elementFromString(string) {
* 当first大于second时返回1
*/
function compareWord(first, second) {
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 first.word < second.word ? -1 : 1;
if (first.freq > second.freq) {
return 1;
}
if (first.word < second.word) {
return -1;
}
if (first.word > second.word) {
return 1;
}
return 0;
}
}

View File

@ -1,7 +1,7 @@
{% block body %}
{% if session['logged_in'] %}
你已登录 <a href="/{{ session['username'] }}/userpage">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>
你已登录 <a href="/{{ session['username'] }}">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>
{% else %}
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
@ -15,10 +15,6 @@
alert('输入不能为空!');
return false;
}
if (password.includes(' ')) {
alert('输入不能包含空格!');
return false;
}
$.post(
"/login", {'username': username, 'password': password},
function (response) {
@ -36,13 +32,14 @@
<div class="container">
<section class="signin-heading">
<h1>Sign in</h1>
<h1>Sign In</h1>
</section>
<input type="text" placeholder="用户名" class="username" id="username">
<input type="password" placeholder="密码" class="password" id="password">
<button type="button" class="btn" onclick="login()">登录</button>
<a class="signup" href="/signup">注册</a>
</div>
{% endif %}

View File

@ -34,9 +34,9 @@
<div class="alert alert-success" role="alert">共有文章 <span class="badge bg-success"> {{ number_of_essays }} </span></div>
<p>粘贴1篇文章 (English only)</p>
<form method="post" action="/">
<textarea name="content" id="article" rows="10" cols="120"></textarea><br/>
<textarea name="content" rows="10" cols="120"></textarea><br/>
<input type="submit" value="get文章中的词频"/>
<input type="reset" value="清除" onclick="clearArticle()"/>
<input type="reset" value="清除"/>
</form>
{% if d_len > 0 %}
<p><b>最常见的词</b></p>
@ -44,7 +44,6 @@
<a href="http://youdao.com/w/eng/{{x[0]}}/#keyfrom=dict2.index">{{x[0]}}</a> {{x[1]}}
{% endfor %}
{% endif %}
<p class="text-muted">Version: 20230810</p>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</div>
{{ yml['footer'] | safe }}
@ -53,22 +52,5 @@
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
<script type="text/javascript">
// IIFE, avoid polluting the global scope
(function() {
const articleInput = document.querySelector('#article');
articleInput.value = localStorage.getItem('article') || '';
articleInput.addEventListener('input', function() {
localStorage.setItem('article', articleInput.value);
});
window.clearArticle = function() {
localStorage.removeItem('article');
articleInput.value = '';
};
})();
</script>
</body>
</html>

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单词词频</title>
<title>Title</title>
{{ yml['header'] | safe }}
{% if yml['css']['item'] %}

View File

@ -12,10 +12,6 @@
alert('输入不能为空!');
return false;
}
if (old_password.includes(' ') || new_password.includes(' ')) {
alert('输入不能包含空格!');
return false;
}
if (new_password !== re_new_password) {
alert('新密码不匹配,请重新输入');
return false;

View File

@ -16,10 +16,6 @@ You're logged in already! <a href="/logout">Logout</a>.
alert('输入不能为空!');
return false;
}
if (password.includes(' ') || password2.includes(' ')) {
alert('输入不能包含空格!');
return false;
}
if (password !== password2) {
alert('确认密码与输入密码不一致!');
return false;
@ -57,7 +53,7 @@ You're logged in already! <a href="/logout">Logout</a>.
<div class="container">
<section class="signin-heading">
<h1>Sign up</h1>
<h1>Sign Up</h1>
</section>
<p><input type="username" id="username" placeholder="输入用户名" class="username"></p>

View File

@ -23,34 +23,44 @@
<title>EnglishPal Study Room for {{ username }}</title>
<style>
.shaking {
animation: shakes 1600ms ease-in-out;
}
.shaking {
animation: shakes 1600ms ease-in-out;
}
@keyframes shakes {
10%, 90% { transform: translate3d(-1px, 0, 0); }
20%, 50% { transform: translate3d(+2px, 0, 0); }
30%, 70% { transform: translate3d(-4px, 0, 0); }
40%, 60% { transform: translate3d(+4px, 0, 0); }
50% { transform: translate3d(-4px, 0, 0); }
}
@keyframes shakes {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 50% {
transform: translate3d(+2px, 0, 0);
}
30%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(+4px, 0, 0);
}
50% {
transform: translate3d(-4px, 0, 0);
}
}
.lead{
font-size: 22px;
font-family: Helvetica, sans-serif;
white-space: pre-wrap;
}
.lead {
font-size: 22px;
font-family: Helvetica, sans-serif;
white-space: pre-wrap;
}
.arrow {
padding: 0;
font-size: 20px;
line-height: 21px;
display: inline-block;
}
.arrow {
padding: 0;
font-size: 20px;
line-height: 21px;
display: inline-block;
}
.arrow:hover {
cursor: pointer;
}
.arrow:hover {
cursor: pointer;
}
</style>
</head>
@ -59,72 +69,83 @@
<p><b>English Pal for <font id="username" color="red">{{ username }}</font></b>
{% if username == admin_name %}
<a class="btn btn-secondary" href="/admin" role="button" onclick="stopRead()">管理</a>
<a class="btn btn-secondary" href="/admin" role="button" onclick="stopRead()">管理</a>
{% endif %}
<a id="quit" class="btn btn-secondary" href="/logout" role="button" onclick="stopRead()">退出</a>
<a class="btn btn-secondary" href="/reset" role="button" onclick="stopRead()">重设密码</a>
</p>
{% for message in get_flashed_messages() %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
<button class="arrow" id="load_next_article" onclick="load_next_article();Reader.stopRead()" title="下一篇 Next Article"></button>
<button class="arrow" id="load_pre_article" onclick="load_pre_article();Reader.stopRead()" style="display: none" title="上一篇 Previous Article"></button>
<button class="arrow" id="load_next_article" onclick="load_next_article();Reader.stopRead()"
title="下一篇 Next Article">⇨
</button>
<button class="arrow" id="load_pre_article" onclick="load_pre_article();Reader.stopRead()" style="display: none"
title="上一篇 Previous Article">⇦
</button>
<p><b>阅读文章并回答问题</b></p>
<div id="text-content">
<div id="found">
<div class="alert alert-success" role="alert">According to your word list, your level is <span class="text-decoration-underline" id="user_level">{{ today_article["user_level"] }}</span> and we have chosen an article with a difficulty level of <span class="text-decoration-underline" id="text_level">{{ today_article["text_level"] }}</span> for you.</div>
<p class="text-muted" id="date">Article added on: {{ today_article["date"] }}</p><br/>
<div class="p-3 mb-2 bg-light text-dark" style="margin: 0 0.5%;"><br/>
<p class="display-6" id="article_title">{{ today_article["article_title"] }}</p><br/>
<p class="lead"><font id="article">{{ today_article["article_body"] }}</font></p><br/>
<div>
<p><small class="text-muted" id="source">{{ today_article['source'] }}</small></p><br/>
<div class="alert alert-success" role="alert">According to your word list, your level is <span
class="text-decoration-underline" id="user_level">{{ today_article["user_level"] }}</span> and we
have chosen an article with a difficulty level of <span class="text-decoration-underline"
id="text_level">{{ today_article["text_level"] }}</span>
for you.
</div>
<p class="text-muted" id="date">Article added on: {{ today_article["date"] }}</p><br/>
<div class="p-3 mb-2 bg-light text-dark" style="margin: 0 0.5%;"><br/>
<p class="display-6" id="article_title">{{ today_article["article_title"] }}</p><br/>
<p class="lead"><font id="article">{{ today_article["article_body"] }}</font></p><br/>
<div>
<p><small class="text-muted" id="source">{{ today_article['source'] }}</small></p><br/>
</div>
<p><b id="question">{{ today_article['question'] }}</b></p><br/>
<p><b id="question">{{ today_article['question'] }}</b></p><br/>
<script type="text/javascript">
function toggle_visibility(id) { {# https://css-tricks.com/snippets/javascript/showhide-element/#}
const e = document.getElementById(id);
if(e.style.display === 'block')
if (e.style.display === 'block')
e.style.display = 'none';
else
e.style.display = 'block';
}
</script>
<button onclick="toggle_visibility('answer');">ANSWER</button>
<div id="answer" style="display:none;">{{ today_article['answer'] }}</div><br/>
<div id="answer" style="display:none;">{{ today_article['answer'] }}</div>
<br/>
</div>
</div>
<div class="alert alert-success" role="alert" id="not_found" style="display:none;">
<p class="text-muted"><span class="badge bg-success">Notes:</span><br>No article is currently available for you. You can try again a few times or mark new words in the passage to improve your level.</p>
<p class="text-muted"><span class="badge bg-success">Notes:</span><br>No article is currently available for
you. You can try again a few times or mark new words in the passage to improve your level.</p>
</div>
<div class="alert alert-success" role="alert" id="read_all" style="display:none;">
<p class="text-muted"><span class="badge bg-success">Notes:</span><br>You've read all the articles.</p>
</div>
</div>
<input type="checkbox" id="highlightCheckbox" onclick="toggleHighlighting()" />生词高亮
<input type="checkbox" id="readCheckbox" onclick="onReadClick()" />大声朗读
<input type="checkbox" id="chooseCheckbox" onclick="onChooseClick()" />划词入库
<input type="checkbox" id="highlightCheckbox" onclick="toggleHighlighting()"/>生词高亮
<input type="checkbox" id="readCheckbox" onclick="onReadClick()"/>大声朗读
<input type="checkbox" id="chooseCheckbox" onclick="onChooseClick()"/>划词入库
<div class="range">
<div class="field">
<div class="sliderValue">
<span id="rangeValue">1×</span>
</div>
<input type="range" id="rangeComponent" min="0.5" max="2" value="1" step="0.25" />
<input type="range" id="rangeComponent" min="0.5" max="2" value="1" step="0.25"/>
</div>
</div>
<p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p>
<form method="post" action="/{{ username }}/userpage">
<textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/>
<button class="btn btn-primary btn-lg" type="submit" onclick="Reader.stopRead()">把生词加入我的生词库</button>
<button class="btn btn-primary btn-lg" type="reset" onclick="clearSelectedWords()">清除</button>
<button class="btn btn-primary btn-lg" type="reset" onclick="clearSelectedWords()">清除</button>
</form>
{% if session.get['thisWord'] %}
<script type="text/javascript">
@ -152,9 +173,10 @@
{% set freq = x[1] %}
{% if session.get('thisWord') == x[0] and session.get('time') == 1 %}
{% endif %}
<p id='p_{{ word }}' class="new-word" >
<a id="word_{{ word }}" class="btn btn-light" href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index'
role="button">{{ word }}</a>
<p id='p_{{ word }}' class="new-word">
<a id="word_{{ word }}" class="btn btn-light"
href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index'
role="button">{{ word }}</a>
( <a id="freq_{{ word }}" title="{{ word }}">{{ freq }}</a> )
<a class="btn btn-success" onclick="familiar('{{ word }}')" role="button">熟悉</a>
<a class="btn btn-warning" onclick="unfamiliar('{{ word }}')" role="button">不熟悉</a>
@ -222,66 +244,78 @@
}
function load_next_article(){
function load_next_article() {
$("#load_next_article").prop("disabled", true)
$.ajax({
url: '/get_next_article/{{username}}',
dataType: 'json',
success: function(data) {
success: function (data) {
// 更新页面内容
if(data['today_article']){
if (data['today_article']) {
update(data['today_article']);
check_pre(data['visited_articles']);
check_next(data['result_of_generate_article']);
}
}, complete: function (xhr, status) {
$("#load_next_article").prop("disabled", false)
}
});
}
function load_pre_article(){
function load_pre_article() {
$.ajax({
url: '/get_pre_article/{{username}}',
dataType: 'json',
success: function(data) {
success: function (data) {
// 更新页面内容
if(data['today_article']){
if (data['today_article']) {
update(data['today_article']);
check_pre(data['visited_articles']);
}
}
});
}
function update(today_article){
function update(today_article) {
$('#user_level').html(today_article['user_level']);
$('#text_level').html(today_article["text_level"]);
$('#date').html('Article added on: '+today_article["date"]);
$('#date').html('Article added on: ' + today_article["date"]);
$('#article_title').html(today_article["article_title"]);
$('#article').html(today_article["article_body"]);
$('#source').html(today_article['source']);
$('#question').html(today_article["question"]);
$('#answer').html(today_article["answer"]);
document.querySelector('#text_level').classList.add('mark'); // highlight text difficult level for 2 seconds
setTimeout(() => {document.querySelector('#text_level').classList.remove('mark');}, 2000);
setTimeout(() => {
document.querySelector('#text_level').classList.remove('mark');
}, 2000);
document.querySelector('#user_level').classList.add('mark'); // do the same thing for user difficulty level
setTimeout(() => {document.querySelector('#user_level').classList.remove('mark');}, 2000);
setTimeout(() => {
document.querySelector('#user_level').classList.remove('mark');
}, 2000);
}
<!-- 检查是否存在上一篇或下一篇,不存在则对应按钮隐藏-->
function check_pre(visited_articles){
if((visited_articles=='')||(visited_articles['index']<=0)){
<!-- 检查是否存在上一篇或下一篇,不存在则对应按钮隐藏-->
function check_pre(visited_articles) {
if ((visited_articles == '') || (visited_articles['index'] <= 0)) {
$('#load_pre_article').hide();
sessionStorage.setItem('pre_page_button', 'display')
}else{
} else {
$('#load_pre_article').show();
sessionStorage.setItem('pre_page_button', 'show')
}
}
function check_next(result_of_generate_article){
if(result_of_generate_article == "found"){
$('#found').show();$('#not_found').hide();
function check_next(result_of_generate_article) {
if (result_of_generate_article == "found") {
$('#found').show();
$('#not_found').hide();
$('#read_all').hide();
}else if(result_of_generate_article == "not found"){
} else if (result_of_generate_article == "not found") {
$('#found').hide();
$('#not_found').show();
$('#read_all').hide();
}else{
} else {
$('#found').hide();
$('#not_found').hide();
$('#read_all').show();
@ -291,7 +325,7 @@
</body>
<style>
mark {
color: red;
color: #{{ yml['highlight']['color'] }};
background-color: rgba(0, 0, 0, 0);
}
</style>

View File

@ -1,50 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<meta name="format-detection" content="telephone=no" />
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<meta name="format-detection" content="telephone=no" />
{{ yml['header'] | safe }}
{% if yml['css']['item'] %}
{% for css in yml['css']['item'] %}
<link href="{{ css }}" rel="stylesheet">
{% endfor %}
{% endif %}
{% if yml['js']['head'] %}
{% for js in yml['js']['head'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
{{ yml['header'] | safe }}
{% if yml['css']['item'] %}
{% for css in yml['css']['item'] %}
<link href="{{ css }}" rel="stylesheet">
{% endfor %}
{% endif %}
{% if yml['js']['head'] %}
{% for js in yml['js']['head'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
<title>EnglishPal Study Room for {{username}}</title>
</head>
<body>
<div class="container-fluid">
<p class="mt-md-3">
<input type="button" id="btn-cancel-selection" value="取消勾选" onclick="toggleCheckboxSelection(false)" />
<input type="button" id="btn-selection" value="全部勾选" onclick="toggleCheckboxSelection(true)" />
</p>
<form method="post" action="/{{username}}/mark">
<button type="submit" name="add-btn" class="btn btn-outline-primary btn-lg" onclick="clearSelectedWords()">加入我的生词簿</button>
{% for x in lst %}
{% set word = x[0]%}
<p>
<font color="grey">{{loop.index}}</font>
:
<a href='http://youdao.com/w/eng/{{word}}/#keyfrom=dict2.index' title={{word}}>{{word}}</a>
({{x[1]}})
<input type="checkbox" name="marked" value="{{word}}" checked>
</p>
<title>EnglishPal Study Room for {{username}}</title>
</head>
<body>
<p>取消勾选认识的单词</p>
<form method="post" action="/{{username}}/mark">
<input type="submit" name="add-btn" value="加入我的生词簿"/>
{% for x in lst %}
{% set word = x[0]%}
<p>
<font color="grey">{{loop.index}}</font>
:
<a href='http://youdao.com/w/eng/{{word}}/#keyfrom=dict2.index' title={{word}}>{{word}}</a>
({{x[1]}})
<input type="checkbox" name="marked" value="{{word}}" checked>
</p>
{% endfor %}
</form>
{{ yml['footer'] | safe }}
{% if yml['js']['bottom'] %}
{% for js in yml['js']['bottom'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
</div>
</body>
{% endfor %}
</form>
{{ yml['footer'] | safe }}
{% if yml['js']['bottom'] %}
{% for js in yml['js']['bottom'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
</body>
</html>

View File

@ -1,9 +1,6 @@
import pytest
import sqlite3
import time
from selenium import webdriver
from pathlib import Path
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
@pytest.fixture
def URL():
@ -12,24 +9,5 @@ def URL():
@pytest.fixture
def 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)
my_driver = webdriver.Edge() # uncomment this line if you wish to run the test on your laptop
return my_driver

View File

@ -1,33 +0,0 @@
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

View File

@ -1,31 +1,76 @@
import time
from helper import signup
# -*- 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/'
def test_add_word(URL, driver):
def has_punctuation(s):
return [c for c in s if c in string.punctuation] != []
def test_add_word():
try:
username, password = signup(URL, driver) # sign up a new account and automatically log in
time.sleep(1)
# enter the word in the text area
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 = 'devour'
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())
elem.send_keys(word)
elem = driver.find_element_by_xpath('//form[1]//button[1]') # 找到"把生词加入我的生词库"按钮
elem.click()
elem = driver.find_element_by_name('add-btn') # 找到"加入我的生词簿"按钮
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.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()

View File

@ -1,95 +0,0 @@
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))

View File

@ -1,55 +0,0 @@
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()

View File

@ -1,44 +0,0 @@
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()

View File

@ -1,37 +0,0 @@
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()

View File

@ -1,58 +0,0 @@
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()

View File

@ -0,0 +1,85 @@
''' Contributed by Lin Junhong et al. 2023-06.'''
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import UnexpectedAlertPresentException, NoAlertPresentException
import random, time
import string
# 初始化webdriver
# driver = webdriver.Remote('http://localhost:4444/wd/hub', DesiredCapabilities.CHROME)
# driver.implicitly_wait(10)
driver = webdriver.Chrome("C:\\Users\\12993\AppData\Local\Programs\Python\Python38\\chromedriver.exe")
def test_next_article():
try:
driver.get("http://118.25.96.118:90")
assert 'English Pal -' in driver.page_source
# login
elem = driver.find_element_by_link_text('登录')
elem.click()
uname = 'abcdefg'
password = 'abcdefg'
elem = driver.find_element_by_id('username')
elem.send_keys(uname)
elem = driver.find_element_by_id('password')
elem.send_keys(password)
elem = driver.find_element_by_xpath('/html/body/div/button') # 找到登录按钮
elem.click()
time.sleep(0.5)
assert 'EnglishPal Study Room for ' + uname in driver.title
for i in range(50):
time.sleep(0.1)
# 找到固定按钮
elem = driver.find_element_by_xpath('//*[@id="load_next_article"]')
elem.click()
except Exception as e:
print(e)
def test_local_next_article():
try:
driver.get("http://127.0.0.1:5000")
assert 'English Pal -' in driver.page_source
# login
elem = driver.find_element_by_link_text('注册')
elem.click()
uname = 'abcdefg'
password = 'abcdefg'
elem = driver.find_element_by_id('username')
elem.send_keys(uname)
elem = driver.find_element_by_id('password')
elem.send_keys(password)
elem = driver.find_element_by_id('password2')
elem.send_keys(password)
time.sleep(0.5)
elem = driver.find_element_by_class_name('btn') # 找到提交按钮
elem.click()
time.sleep(0.5)
try:
WebDriverWait(driver, 1).until(EC.alert_is_present())
driver.switch_to.alert.accept()
except (UnexpectedAlertPresentException, NoAlertPresentException):
pass
time.sleep(0.5)
assert 'EnglishPal Study Room for ' + uname in driver.title
for i in range(50):
time.sleep(0.1)
# 找到固定按钮
elem = driver.find_element_by_xpath('//*[@id="load_next_article"]')
elem.click()
except Exception as e:
print(e)

View File

@ -1,43 +0,0 @@
''' 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()

View File

@ -15,9 +15,6 @@ 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__)
@ -35,9 +32,7 @@ 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,
@ -134,7 +129,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 = request.form['content']
content = escape(request.form['content'])
f = WordFreq(content)
lst = f.get_freq()
return render_template('userpage_post.html',username=username,lst = lst, yml=Yaml.yml)
@ -181,11 +176,7 @@ 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)
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'))))
pickle_idea2.save_frequency_to_pickle(d, user_freq_record)
return redirect(url_for('user_bp.userpage', username=username))
else:
return 'Under construction'

View File

@ -4,7 +4,6 @@
###########################################################################
import collections
import html
import string
import operator
import os, sys # 引入模块sys因为我要用里面的sys.argv列表中的信息来读取命令行参数。
@ -40,8 +39,7 @@ def file2str(fname):#文件转字符
def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。
special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|,。?!¥……()、《》:;·' # 把里面的字符都去掉
s = html.unescape(s) # 将HTML实体转换为对应的字符比如<会被识别为小于号
special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' # 把里面的字符都去掉
for c in special_characters:
s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况
s = s.replace('--', ' ')
@ -106,7 +104,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'):

View File

@ -2,7 +2,10 @@
DEPLOYMENT_DIR=/home/lanhui/englishpal2/EnglishPal
cd $DEPLOYMENT_DIR
pwd
# Install dependencies
pip3 install -r requirements.txt
# Stop service
sudo docker stop EnglishPal
@ -12,7 +15,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 --mount type=volume,src=englishpal-db,target=/app/db -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 -v ${DEPLOYMENT_DIR}/app/static/:/app/static/ -t englishpal # for permanently saving data
# Save space. Run it after sudo docker run
sudo docker system prune -a -f

94
test_vocabulary.py Normal file
View File

@ -0,0 +1,94 @@
# Run this test script on the command line:
# pytest test_vocabulary.py
#
# Last modified by Mr Lan Hui on 2025-03-05
from vocabulary import UserVocabularyLevel, ArticleVocabularyLevel
def test_article_level_empty_content():
''' Boundary case test '''
article = ArticleVocabularyLevel('')
assert article.level == 0
def test_article_level_punctuation_only():
''' Boundary case test '''
article = ArticleVocabularyLevel(',')
assert article.level == 0
def test_article_level_digit_only():
''' Boundary case test '''
article = ArticleVocabularyLevel('1')
assert article.level == 0
def test_article_level_single_word():
''' Boundary case test '''
article = ArticleVocabularyLevel('source')
assert 2 <= article.level <= 4
def test_article_level_subset_vs_superset():
''' Boundary case test '''
article1 = ArticleVocabularyLevel('source')
article2 = ArticleVocabularyLevel('open source')
assert article1.level < article2.level
def test_article_level_multiple_words():
''' Boundary case test '''
article = ArticleVocabularyLevel('Producing Open Source Software - How to Run a Successful Free Software Project')
assert 3 <= article.level <= 5
def test_article_level_short_paragraph():
''' Boundary case test '''
article = ArticleVocabularyLevel('At parties, people no longer give me a blank stare when I tell them I work in open source software. "Oh, yes — like Linux?" they say. I nod eagerly in agreement. "Yes, exactly! That\'s what I do." It\'s nice not to be completely fringe anymore. In the past, the next question was usually fairly predictable: "How do you make money doing that?" To answer, I\'d summarize the economics of free software: that there are organizations in whose interest it is to have certain software exist, but that they don\'t need to sell copies, they just want to make sure the software is available and maintained, as a tool instead of as a rentable monopoly.')
assert 4 <= article.level <= 6
def test_article_level_medium_paragraph():
''' Boundary case test '''
article = ArticleVocabularyLevel('In considering the Origin of Species, it is quite conceivable that a naturalist, reflecting on the mutual affinities of organic beings, on their embryological relations, their geographical distribution, geological succession, and other such facts, might come to the conclusion that each species had not been independently created, but had descended, like varieties, from other species. Nevertheless, such a conclusion, even if well founded, would be unsatisfactory, until it could be shown how the innumerable species inhabiting this world have been modified, so as to acquire that perfection of structure and coadaptation which most justly excites our admiration. Naturalists continually refer to external conditions, such as climate, food, etc., as the only possible cause of variation. In one very limited sense, as we shall hereafter see, this may be true; but it is preposterous to attribute to mere external conditions, the structure, for instance, of the woodpecker, with its feet, tail, beak, and tongue, so admirably adapted to catch insects under the bark of trees. In the case of the misseltoe, which draws its nourishment from certain trees, which has seeds that must be transported by certain birds, and which has flowers with separate sexes absolutely requiring the agency of certain insects to bring pollen from one flower to the other, it is equally preposterous to account for the structure of this parasite, with its relations to several distinct organic beings, by the effects of external conditions, or of habit, or of the volition of the plant itself.')
assert 5 <= article.level <= 7
def test_article_level_long_paragraph():
''' Boundary case test '''
article = ArticleVocabularyLevel('These several facts accord well with my theory. I believe in no fixed law of development, causing all the inhabitants of a country to change abruptly, or simultaneously, or to an equal degree. The process of modification must be extremely slow. The variability of each species is quite independent of that of all others. Whether such variability be taken advantage of by natural selection, and whether the variations be accumulated to a greater or lesser amount, thus causing a greater or lesser amount of modification in the varying species, depends on many complex contingencies,—on the variability being of a beneficial nature, on the power of intercrossing, on the rate of breeding, on the slowly changing physical conditions of the country, and more especially on the nature of the other inhabitants with which the varying species comes into competition. Hence it is by no means surprising that one species should retain the same identical form much longer than others; or, if changing, that it should change less. We see the same fact in geographical distribution; for instance, in the land-shells and coleopterous insects of Madeira having come to differ considerably from their nearest allies on the continent of Europe, whereas the marine shells and birds have remained unaltered. We can perhaps understand the apparently quicker rate of change in terrestrial and in more highly organised productions compared with marine and lower productions, by the more complex relations of the higher beings to their organic and inorganic conditions of life, as explained in a former chapter. When many of the inhabitants of a country have become modified and improved, we can understand, on the principle of competition, and on that of the many all-important relations of organism to organism, that any form which does not become in some degree modified and improved, will be liable to be exterminated. Hence we can see why all the species in the same region do at last, if we look to wide enough intervals of time, become modified; for those which do not change will become extinct.')
assert 6 <= article.level <= 8
def test_user_level_empty_dictionary():
''' Boundary case test '''
user = UserVocabularyLevel({})
assert user.level == 0
def test_user_level_one_simple_word():
''' Boundary case test '''
user = UserVocabularyLevel({'simple':['202408050930']})
assert 0 < user.level <= 4
def test_user_level_invalid_word():
''' Boundary case test '''
user = UserVocabularyLevel({'xyz':['202408050930']})
assert user.level == 0
def test_user_level_one_hard_word():
''' Boundary case test '''
user = UserVocabularyLevel({'pasture':['202408050930']})
assert 5 <= user.level <= 8
def test_user_level_multiple_words():
''' Boundary case test '''
user = UserVocabularyLevel(
{'sessile': ['202408050930'], 'putrid': ['202408050930'], 'prodigal': ['202408050930'], 'presumptuous': ['202408050930'], 'prehension': ['202408050930'], 'pied': ['202408050930'], 'pedunculated': ['202408050930'], 'pasture': ['202408050930'], 'parturition': ['202408050930'], 'ovigerous': ['202408050930'], 'ova': ['202408050930'], 'orifice': ['202408050930'], 'obliterate': ['202408050930'], 'niggard': ['202408050930'], 'neuter': ['202408050930'], 'locomotion': ['202408050930'], 'lineal': ['202408050930'], 'glottis': ['202408050930'], 'frivolous': ['202408050930'], 'frena': ['202408050930'], 'flotation': ['202408050930'], 'ductus': ['202408050930'], 'dorsal': ['202408050930'], 'dearth': ['202408050930'], 'crustacean': ['202408050930'], 'cornea': ['202408050930'], 'contrivance': ['202408050930'], 'collateral': ['202408050930'], 'cirriped': ['202408050930'], 'canon': ['202408050930'], 'branchiae': ['202408050930'], 'auditory': ['202408050930'], 'articulata': ['202408050930'], 'alimentary': ['202408050930'], 'adduce': ['202408050930'], 'aberration': ['202408050930']}
)
assert 6 <= user.level <= 8
def test_user_level_consider_only_most_recent_words_difficult_words_most_recent():
''' Consider only the most recent three words '''
user = UserVocabularyLevel(
{'pasture':['202408050930'], 'putrid': ['202408040000'], 'frivolous':['202408030000'], 'simple':['202408020000'], 'apple':['202408010000']}
)
assert 5 <= user.level <= 8
def test_user_level_consider_only_most_recent_words_easy_words_most_recent():
''' Consider only the most recent three words '''
user = UserVocabularyLevel(
{'simple':['202408050930'], 'apple': ['202408040000'], 'happy':['202408030000'], 'pasture':['202408020000'], 'putrid':['202408010000'], 'dearth':['202407310000']}
)
assert 4 <= user.level <= 5