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
'.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
标签去除,导致处理后的文章内容失去了原来的格式
+ 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..8b3ac6c 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);
@@ -103,6 +102,8 @@ function wordTemplate(word) {
不熟悉
删除
朗读
+ 笔记
+
粘贴1篇文章 (English only)
{% if d_len > 0 %}最常见的词
@@ -44,6 +44,7 @@ {{x[0]}} {{x[1]}} {% endfor %} {% endif %} +Version: 20240618
{{ yml['footer'] | safe }} @@ -52,5 +53,22 @@ {% endfor %} {% endif %} +