Compare commits

..

9 Commits

21 changed files with 402 additions and 412 deletions

View File

@ -11,14 +11,15 @@ Hui Lan <hui.lan@cantab.net>
EnglishPal allows the user to build his list of new English words EnglishPal allows the user to build his list of new English words
picked from articles selected for him to read according his vocabulary level. EnglishPal will determine a user's vocabulary level based on his picked words. After that, it will recommend articles for him to read, in order to booster his English vocabulary furthermore. picked from articles selected for him according his vocabulary level.
## Run on your own laptop ## Run it on a local machine
`python3 main.py` `python3 main.py`
Make sure you have put the SQLite database file in the path `app/static` (see below). Make sure you have the SQLite database file in `app/static` (see below).
## Run it as a Docker container ## Run it as a Docker container
@ -28,32 +29,32 @@ Assuming that docker has been installed and that you are a sudo user (i.e., sudo
`sudo ./build.sh` `sudo ./build.sh`
Open your favourite Internet browser and enter this URL address: `http://ip-address:90`. Note: you must update the variable `DEPLOYMENT_DIR` in `build.sh`. Open your favourite Internet browser and enter this URL address: `http://ip-address:90`.
### Explanation on the commands in build.sh ### Explanation on the commands in build.sh
My steps for deploying English on a Ubuntu server. My steps for deploying English on the server.
- ssh to ubuntu@118.*.*.118 - ssh to ubuntu@118.*.*.118
- cd to `/home/lanhui/englishpal2/EnglishPal` - cd to /home/lanhui/englishpal2/EnglishPal
- Stop all docker service: `sudo service docker restart`. If you know the docker container ID, then the above command is an overkill. Use the following command instead: `sudo docker stop ContainerID`. You could get all container IDs with the following command: `sudo docker ps` - Stop all docker service: `sudo service docker restart`. If you know the docker container ID, then the above command is an overkill. Use the following command instead: `sudo docker stop ContainerID`. You could get all container IDs with the following command: `sudo docker ps`
- Rebuild container. Run the following command to rebuild a docker image each time after the source code gets updated: `sudo docker build -t englishpal .` - Rebuild container. Run the following command to rebuild a docker image after the code gets updated: `sudo docker build -t englishpal .`
- Run the application: `sudo docker run -d -p 90:80 -v /home/lanhui/englishpal2/EnglishPal/app/static/frequency:/app/static/frequency -t englishpal`. If you use `sudo docker run -d -p 90:80 -t englishpal`, data will be lost after terminating the program. If you want to automatically restart the docker image after each system reboot, add the option `--restart=always` after `docker run`. - Run the application: `sudo docker run -d -p 90:80 -v /home/lanhui/englishpal2/EnglishPal/app/static/frequency:/app/static/frequency -t englishpal`. If you use `sudo docker run -d -p 90:80 -t englishpal`, data will be lost after terminating the program.
- Save disk space: `sudo docker system prune -a -f` - Save space: `sudo docker system prune -a -f`
`build.sh` contains all the above commands. Run "sudo ./build.sh" to rebuild and start the web application.
#### Other useful docker commands ### Other useful docker commands
- `sudo docker ps -a` - `sudo docker ps -a`
- `sudo docker logs image_name`, where `image_name` could be obtained from `sudo docker ps`. - `sudo docker logs image_name`, where image_name could be obtained from `sudo docker ps`.
`build.sh` contains all the above commands. Run "sudo ./build.sh" to rebuild and run the web application.
@ -67,10 +68,6 @@ All articles are stored in the `article` table in a SQLite file called
To add articles, open and edit `app/static/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org). To add articles, open and edit `app/static/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org).
### Extending an account's expiry date
By default, an account's expiry is 30 days after first sign-up. To extend account's expiry date, open and edit `user` table in `app/static/wordfreqapp.db`. Simply update field `expiry_date`.
### Exporting the database ### Exporting the database
Export wordfreqapp.db to wordfreqapp.sql using the following commands: Export wordfreqapp.db to wordfreqapp.sql using the following commands:
@ -95,31 +92,33 @@ sqlite3 wordfreqapp.db`. Delete wordfreqapp.db first if it exists.
### Uploading wordfreqapp.db to the server ### 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/englishpal/app/static`
## Feedback ## Feedback
We welcome feedback on EnglishPal. Feedback examples: We welcome feedback on EnglishPal.
### Feedback 1 ### Respondent 1
- "Need a phone app. I use phone a lot. You cannot ask students to use computers."
### Feedback 2 "Need a phone app. I use phone a lot. You cannot ask students to use computers."
Can take a picture for text. Automatic translation.
### Respondent 2
- “成为会员”改成“注册” “成为会员”改成“注册”
- “登出”改成“退出” “登出”改成“退出”
- “收集生词吧”改成“生词收集栏” “收集生词吧”改成“生词收集栏”
- 不要自动显示下一篇 “不要自动显示下一篇”
- 需要有“上一篇”、“下一篇”按钮。 需要有“上一篇”、“下一篇”
@ -138,7 +137,7 @@ EnglishPal's bugs and improvement suggestions are recorded in [Bugzilla](http://
- Usability testing - Usability testing
## Improvements made by contributors (incomplete list) ## Improvements made by contributors
### 朱文绮 ### 朱文绮
@ -160,6 +159,7 @@ too many words that they already know, on the other hand, it can
reduce unnecessary memory occupied by the database, in addition, it reduce unnecessary memory occupied by the database, in addition, it
can also improve the simplicity of the page. can also improve the simplicity of the page.
More information at: http://118.25.96.118/kanboard/?controller=TaskViewController&action=readonly&task_id=736&token=81a561da57ff7a172da17a480f0d421ff3bc69efbd29437daef90b1b8959
### 占健豪 ### 占健豪
@ -181,14 +181,4 @@ Demo video link: https://b23.tv/QuB77m
Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=215 Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=215
*Last modified on 2021-10-17*
### 丁锐
修复了以下漏洞
漏洞:新用户在创建账号时,不需要输入确定密码也可以注册成功,并且新账户可以正常使用。
Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=489
*Last modified on 2023-01-30*

View File

@ -1,6 +1,6 @@
import hashlib import hashlib
import string import string
from datetime import datetime, timedelta from datetime import datetime
from UseSqlite import InsertQuery, RecordQuery from UseSqlite import InsertQuery, RecordQuery
path_prefix = '/var/www/wordfreq/wordfreq/' path_prefix = '/var/www/wordfreq/wordfreq/'
@ -23,7 +23,7 @@ def verify_user(username, password):
def add_user(username, password): def add_user(username, password):
start_date = datetime.now().strftime('%Y%m%d') start_date = datetime.now().strftime('%Y%m%d')
expiry_date = (datetime.now() + timedelta(days=30)).strftime('%Y%m%d') # will expire after 30 days expiry_date = '20221230'
# 将用户名和密码一起加密,以免暴露不同用户的相同密码 # 将用户名和密码一起加密,以免暴露不同用户的相同密码
password = md5(username + password) password = md5(username + password)
rq = InsertQuery(path_prefix + 'static/wordfreqapp.db') rq = InsertQuery(path_prefix + 'static/wordfreqapp.db')

View File

@ -1,13 +1,15 @@
''' """
Yaml.py Yaml.py
配置文件包括: 配置文件包括:
./static/config.yml ./static/config.yml
./layout/partial/header.html ./layout/partial/header.html
./layout/partial/footer.html ./layout/partial/footer.html
''' """
import yaml as YAML import yaml as YAML
import os import os
from app.file_open import FileOpen
path_prefix = './' # comment this line in deployment path_prefix = './' # comment this line in deployment
# YAML文件路径 # YAML文件路径
@ -15,13 +17,16 @@ ymlPath = path_prefix + 'static/config.yml'
# partial文件夹路径 # partial文件夹路径
partialPath = path_prefix + 'layout/partial/' partialPath = path_prefix + 'layout/partial/'
f = open(ymlPath, 'r', encoding='utf-8') # 以'UTF-8'格式打开YAML文件 # f = open(ymlPath, 'r', encoding='utf-8') # 以'UTF-8'格式打开YAML文件
f = FileOpen.read_only(ymlPath, 'r', encoding='utf-8') # 以'UTF-8'格式打开YAML文件
cont = f.read() # 以文本形式读取YAML cont = f.read() # 以文本形式读取YAML
yml = YAML.load(cont, Loader=YAML.FullLoader) # 加载YAML yml = YAML.load(cont, Loader=YAML.FullLoader) # 加载YAML
with open(partialPath + 'header.html', 'r', encoding='utf-8') as f: # with open(partialPath + 'header.html', 'r', encoding='utf-8') as f:
with FileOpen.read_only(partialPath + 'header.html', 'r', encoding='utf-8') as f:
yml['header'] = f.read() # header内的文本会被直接添加到所有页面的head标签内 yml['header'] = f.read() # header内的文本会被直接添加到所有页面的head标签内
with open(partialPath + 'footer.html', 'r', encoding='utf-8') as f: # with open(partialPath + 'footer.html', 'r', encoding='utf-8') as f:
with FileOpen.read_only(partialPath + 'footer.html', 'r', encoding='utf-8') as f:
yml['footer'] = f.read() # footer内的文本会被直接添加到所有页面的最底部 yml['footer'] = f.read() # footer内的文本会被直接添加到所有页面的最底部

View File

@ -1,10 +1,13 @@
from flask import * from flask import *
from markupsafe import escape
from Login import check_username_availability, verify_user, add_user, get_expiry_date, change_password, WarningMessage from Login import check_username_availability, verify_user, add_user, get_expiry_date, change_password, WarningMessage
# 初始化蓝图 # 初始化蓝图
accountService = Blueprint("accountService", __name__) accountService = Blueprint("accountService", __name__)
### Sign-up, login, logout ### ### Sign-up, login, logout ###
@accountService.route("/signup", methods=['GET', 'POST']) @accountService.route("/signup", methods=['GET', 'POST'])
def signup(): def signup():
@ -19,21 +22,18 @@ def signup():
# POST方法需判断是否注册成功再根据结果返回不同的内容 # POST方法需判断是否注册成功再根据结果返回不同的内容
username = escape(request.form['username']) username = escape(request.form['username'])
password = escape(request.form['password']) password = escape(request.form['password'])
password2 = escape(request.form['password2'])
#! 添加如下代码为了过滤注册时的非法字符 #! 添加如下代码为了过滤注册时的非法字符
warn = WarningMessage(username) warn = WarningMessage(username)
if str(warn) != 'OK': if str(warn) != 'OK':
return str(warn) return str(warn)
available = check_username_availability(username) available = check_username_availability(username)
if not available: # 用户名不可用 if not available: # 用户名不可用
flash('用户名 %s 已经被注册。' % (username)) flash('用户名 %s 已经被注册。' % (username))
return render_template('signup.html') return render_template('signup.html')
elif len(password.strip()) < 4: # 密码过短 elif len(password.strip()) < 4: # 密码过短
return '密码过于简单。' return '密码过于简单。'
elif password != password2:
return '确认密码与输入密码不一致!'
else: # 添加账户信息 else: # 添加账户信息
add_user(username, password) add_user(username, password)
verified = verify_user(username, password) verified = verify_user(username, password)
@ -50,7 +50,6 @@ def signup():
return '用户名密码验证失败。' return '用户名密码验证失败。'
@accountService.route("/login", methods=['GET', 'POST']) @accountService.route("/login", methods=['GET', 'POST'])
def login(): def login():
''' '''
@ -115,13 +114,6 @@ def reset():
# POST请求用于提交修改后信息 # POST请求用于提交修改后信息
old_password = escape(request.form['old-password']) old_password = escape(request.form['old-password'])
new_password = escape(request.form['new-password']) new_password = escape(request.form['new-password'])
re_new_password = escape(request.form['re-new-password']) # 确认新密码
if re_new_password != new_password: #验证新密码两次输入是否相同
return '新密码不匹配,请重新输入'
if len(new_password) < 4: #验证新密码长度,原则参照注册模块
return '密码过于简单。(密码长度至少4位)'
flag = change_password(username, old_password, new_password) # flag表示是否修改成功 flag = change_password(username, old_password, new_password) # flag表示是否修改成功
if flag: if flag:
session['logged_in'] = False session['logged_in'] = False

View File

@ -7,11 +7,14 @@
import pickle import pickle
import math import math
from app.file_open import FileOpen
from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order
def load_record(pickle_fname): def load_record(pickle_fname):
f = open(pickle_fname, 'rb') # f = open(pickle_fname, 'rb')
f = FileOpen.read_only(pickle_fname, 'rb')
d = pickle.load(f) d = pickle.load(f)
f.close() f.close()
return d return d
@ -237,7 +240,8 @@ We need — for our farmers, our manufacturers, for, frankly, unions and non-uni
#f = open('bbc-fulltext/bbc/entertainment/001.txt') #f = open('bbc-fulltext/bbc/entertainment/001.txt')
f = open('wordlist.txt') # f = open('wordlist.txt')
f = FileOpen.read_only('wordlist.txt')
s = f.read() s = f.read()
f.close() f.close()

47
app/file_open.py Normal file
View File

@ -0,0 +1,47 @@
import os
class ModeErrorException(Exception):
def __init__(self, error_info):
super().__init__(self)
self.error_info = error_info
def __str__(self):
return self.error_info
class FileOpen:
def __init__(self):
pass
@staticmethod
def read_only(path, mode='r', encoding=None):
if mode not in ['r', 'r+', 'rb', 'rt', 'rb+', 'rt+']:
raise ModeErrorException('the input mode is wrong')
my_dir = os.path.dirname(path) # 获得路径的目录
if not os.path.exists(my_dir): # 判断文件的所有父文件夹是否存在
os.makedirs(my_dir) # 创建所有父文件夹
# 读文件需要判断文件是否存在,不存在则创建
if not os.path.exists(path):
f = open(path, 'w', encoding)
f.close()
return open(path, mode=mode, encoding=encoding)
@staticmethod
def write_able(path, mode='w', encoding=None):
if mode not in ['w', 'w+', 'wb', 'wt', 'wb+', 'wt+']:
raise ModeErrorException('the input mode is wrong')
my_dir = os.path.dirname(path) # 获得路径的目录
if not os.path.exists(my_dir): # 判断文件的所有父文件夹是否存在
os.makedirs(my_dir) # 创建所有父文件夹
return open(path, mode=mode, encoding=encoding)
if __name__ == "__main__":
wf = FileOpen.write_able(r"D:\Workspace\gitcode\EnglishPal\app\static\test.txt")
wf.write("asdasdasd")
wf.close()
rf = FileOpen.read_only(r"D:\Workspace\gitcode\EnglishPal\app\static\test.txt")
for line in rf.readlines():
print(line)

View File

@ -9,6 +9,8 @@
import pickle import pickle
from datetime import datetime from datetime import datetime
from app.file_open import FileOpen
def lst2dict(lst, d): def lst2dict(lst, d):
''' '''
@ -37,14 +39,16 @@ def merge_frequency(lst1, lst2):
def load_record(pickle_fname): def load_record(pickle_fname):
f = open(pickle_fname, 'rb') # f = open(pickle_fname, 'rb')
f = FileOpen.read_only(pickle_fname, 'rb')
d = pickle.load(f) d = pickle.load(f)
f.close() f.close()
return d return d
def save_frequency_to_pickle(d, pickle_fname): def save_frequency_to_pickle(d, pickle_fname):
f = open(pickle_fname, 'wb') # f = open(pickle_fname, 'wb')
f = FileOpen.write_able(pickle_fname, 'wb')
#exclusion_lst = ['one', 'no', 'has', 'had', 'do', 'that', 'have', 'by', 'not', 'but', 'we', 'this', 'my', 'him', 'so', 'or', 'as', 'are', 'it', 'from', 'with', 'be', 'can', 'for', 'an', 'if', 'who', 'whom', 'whose', 'which', 'the', 'to', 'a', 'of', 'and', 'you', 'i', 'he', 'she', 'they', 'me', 'was', 'were', 'is', 'in', 'at', 'on', 'their', 'his', 'her', 's', 'said', 'all', 'did', 'been', 'w'] #exclusion_lst = ['one', 'no', 'has', 'had', 'do', 'that', 'have', 'by', 'not', 'but', 'we', 'this', 'my', 'him', 'so', 'or', 'as', 'are', 'it', 'from', 'with', 'be', 'can', 'for', 'an', 'if', 'who', 'whom', 'whose', 'which', 'the', 'to', 'a', 'of', 'and', 'you', 'i', 'he', 'she', 'they', 'me', 'was', 'were', 'is', 'in', 'at', 'on', 'their', 'his', 'her', 's', 'said', 'all', 'did', 'been', 'w']
exclusion_lst = [] exclusion_lst = []
d2 = {} d2 = {}
@ -55,20 +59,24 @@ def save_frequency_to_pickle(d, pickle_fname):
f.close() f.close()
def unfamiliar(path,word): def unfamiliar(path,word):
f = open(path,"rb") # f = open(path,"rb")
f = FileOpen.read_only(path, 'rb')
dic = pickle.load(f) dic = pickle.load(f)
dic[word] += [datetime.now().strftime('%Y%m%d%H%M')] dic[word] += [datetime.now().strftime('%Y%m%d%H%M')]
fp = open(path,"wb") # fp = open(path,"wb")
fp = FileOpen.write_able(path, 'wb')
pickle.dump(dic,fp) pickle.dump(dic,fp)
def familiar(path,word): def familiar(path,word):
f = open(path,"rb") # f = open(path,"rb")
f = FileOpen.read_only(path, 'rb')
dic = pickle.load(f) dic = pickle.load(f)
if len(dic[word])>1: if len(dic[word])>1:
del dic[word][0] del dic[word][0]
else: else:
dic.pop(word) dic.pop(word)
fp = open(path,"wb") # fp = open(path,"wb")
fp = FileOpen.write_able(path, 'wb')
pickle.dump(dic,fp) pickle.dump(dic,fp)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -11,6 +11,9 @@
import pickle import pickle
from datetime import datetime from datetime import datetime
from app.file_open import FileOpen
def lst2dict(lst, d): def lst2dict(lst, d):
''' '''
Store the information in list lst to dictionary d. Store the information in list lst to dictionary d.
@ -26,14 +29,16 @@ def lst2dict(lst, d):
d[word] += dates d[word] += dates
def deleteRecord(path,word): def deleteRecord(path,word):
with open(path, 'rb') as f: # with open(path, 'rb') as f:
with FileOpen.read_only(path, 'rb') as f:
db = pickle.load(f) db = pickle.load(f)
try: try:
db.pop(word) db.pop(word)
except KeyError: except KeyError:
print("sorry") print("sorry")
with open(path, 'wb') as ff: # with open(path, 'wb') as ff:
pickle.dump(db, ff) with FileOpen.write_able(path, 'wb') as ff:
pickle.dump(db, ff)
def dict2lst(d): def dict2lst(d):
if len(d) > 0: if len(d) > 0:
@ -56,14 +61,16 @@ def merge_frequency(lst1, lst2):
def load_record(pickle_fname): def load_record(pickle_fname):
f = open(pickle_fname, 'rb') # f = open(pickle_fname, 'rb')
f = FileOpen.read_only(pickle_fname, 'rb')
d = pickle.load(f) d = pickle.load(f)
f.close() f.close()
return d return d
def save_frequency_to_pickle(d, pickle_fname): def save_frequency_to_pickle(d, pickle_fname):
f = open(pickle_fname, 'wb') # f = open(pickle_fname, 'wb')
f = FileOpen.write_able(pickle_fname, 'wb')
exclusion_lst = ['one', 'no', 'has', 'had', 'do', 'that', 'have', 'by', 'not', 'but', 'we', 'this', 'my', 'him', 'so', 'or', 'as', 'are', 'it', 'from', 'with', 'be', 'can', 'for', 'an', 'if', 'who', 'whom', 'whose', 'which', 'the', 'to', 'a', 'of', 'and', 'you', 'i', 'he', 'she', 'they', 'me', 'was', 'were', 'is', 'in', 'at', 'on', 'their', 'his', 'her', 's', 'said', 'all', 'did', 'been', 'w'] exclusion_lst = ['one', 'no', 'has', 'had', 'do', 'that', 'have', 'by', 'not', 'but', 'we', 'this', 'my', 'him', 'so', 'or', 'as', 'are', 'it', 'from', 'with', 'be', 'can', 'for', 'an', 'if', 'who', 'whom', 'whose', 'which', 'the', 'to', 'a', 'of', 'and', 'you', 'i', 'he', 'she', 'they', 'me', 'was', 'were', 'is', 'in', 'at', 'on', 'their', 'his', 'her', 's', 'said', 'all', 'did', 'been', 'w']
d2 = {} d2 = {}
for k in d: for k in d:

View File

@ -1,107 +0,0 @@
/*样式应用于login、signup、reset三个页面*/
.container {
background-color: #FFFFFF;
width: 400px;
height: 500px;
margin: 7em auto;
border-radius: 1.5em;
box-shadow: 0px 11px 35px 2px rgba(0, 0, 0, 0.14);
}
/*增加一个类reset-heading*/
.signin-heading, .reset-heading {
padding-top: 5px;
color: #8C55AA;
font-family: 'Ubuntu', sans-serif;
font-weight: bold;
font-size: 23px;
text-align: center;
}
/*增加2个类.old-password和.new-password*/
.username, .email, .password, .re-password, .old-password, .new-password,.re-new-password {
width: 76%;
color: rgb(38, 50, 56);
font-weight: 700;
font-size: 14px;
letter-spacing: 1px;
background: rgba(136, 126, 126, 0.04);
padding: 10px 20px;
border: none;
border-radius: 20px;
outline: none;
box-sizing: border-box;
border: 2px solid rgba(124, 16, 97, 0.02);
margin-bottom: 50px;
margin-left: 46px;
text-align: center;
margin-bottom: 27px;
font-family: 'Ubuntu', sans-serif;
}
.btn {
width: 50%;
border: none;
border-radius: 20px;
box-sizing: border-box;
border: 2px solid #8C55AA;
margin-bottom: 50px;
margin-left: 90px;
padding: 10px 20px;
}
.btn:hover {
background: #8C55AA;
transition: .5s;
cursor: pointer;
color: #fff;
}
.signup {
display: flex;
justify-content: center;
align-items: center;
}
ul {
position: absolute;
display: flex;
left: 65%;
}
li {
padding: 10px;
margin: 10px;
}
a {
text-decoration: none;
list-style: none;
font-weight: bold;
font-family: 'ink free';
}
.main_menu a {
color: #fff;
font-size: 300px;
}
li :hover {
color: #8C55AA;
transition: .5s;
}
h1 {
font-family: 'ink free';
}
.main_menu h1 {
color: #fff;
}

View File

@ -1,5 +0,0 @@
This folder holds users' vocabulary files.
Each file ends with .pickle.
For example, mrlan.pickle is the vocabulary file for user mrlan.

View File

@ -62,7 +62,3 @@ function onReadClick() {
function onChooseClick() { function onChooseClick() {
isChoose = !isChoose; isChoose = !isChoose;
} }
function stopRead() {
reader.cancel();
}

View File

@ -22,21 +22,13 @@ function getWord() {
function highLight() { function highLight() {
if (!isHighlight) return; if (!isHighlight) return;
let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml使用innerText会把原文章中所包含的<br>标签去除,导致处理后的文章内容失去了原来的格式 let articleContent = document.getElementById("article").innerText;
let pickedWords = document.getElementById("selected-words"); // words picked to the text area 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 dictionaryWords = document.getElementById("selected-words2"); // words appearing in the user's new words list
let allWords = ""; //初始化allWords的值避免进入判断后编译器认为allWords未初始化的问题 let allWords = pickedWords.value + " " + dictionaryWords.value;
if(dictionaryWords != null){//增加一个判断检查生词本里面是否为空如果为空allWords只添加选中的单词 const list = allWords.split(" ");
allWords = pickedWords.value + " " + dictionaryWords.value;
}
else{
allWords = pickedWords.value + " ";
}
const list = allWords.split(" ");//将所有的生词放入一个list中用于后续处理
for (let i = 0; i < list.length; ++i) { for (let i = 0; i < list.length; ++i) {
list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); //消除单词两边的空字符 list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); //消除单词两边的空字符
list[i] = list[i].replace('|', "");
list[i] = list[i].replace('?', "");
if (list[i] !== "" && "<mark>".indexOf(list[i]) === -1 && "</mark>".indexOf(list[i]) === -1) { if (list[i] !== "" && "<mark>".indexOf(list[i]) === -1 && "</mark>".indexOf(list[i]) === -1) {
//将文章中所有出现该单词word的地方改为" <mark>" + word + "<mark> "。 正则表达式RegExp()中,"\\s"代表单词前后必须要有空格,以防止只对单词中的部分字符高亮的情况出现。 //将文章中所有出现该单词word的地方改为" <mark>" + word + "<mark> "。 正则表达式RegExp()中,"\\s"代表单词前后必须要有空格,以防止只对单词中的部分字符高亮的情况出现。
articleContent = articleContent.replace(new RegExp("\\s"+list[i]+"\\s", "g"), " <mark>" + list[i] + "</mark> "); articleContent = articleContent.replace(new RegExp("\\s"+list[i]+"\\s", "g"), " <mark>" + list[i] + "</mark> ");
@ -46,15 +38,15 @@ function highLight() {
} }
function cancelHighlighting() { function cancelHighlighting() {
let articleContent = document.getElementById("article").innerText;//将原来的.innerText改为.innerHtml原因同上 let articleContent = document.getElementById("article").innerText;
let pickedWords = document.getElementById("selected-words"); let pickedWords = document.getElementById("selected-words");
const dictionaryWords = document.getElementById("selected-words2"); const dictionaryWords = document.getElementById("selected-words2");
const list = pickedWords.value.split(" "); const list = pickedWords.value.split(" ");
if (pickedWords != null) { if (pickedWords != null) {
for (let i = 0; i < list.length; ++i) { for (let i = 0; i < list.length; ++i) {
list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); list[i] = list[i].replace(/(^\s*)|(\s*$)/g, "");
if (list[i] !== "") { //原来判断的代码中替换的内容为“list[i]”这个字符串这明显是错误的我们需要替换的是list[i]里的内容 if (list[i] !== "") {
articleContent = articleContent.replace(new RegExp("<mark>"+list[i]+"</mark>", "g"), list[i]); articleContent = articleContent.replace("<mark>" + list[i] + "</mark>", "list[i]");
} }
} }
} }
@ -63,8 +55,8 @@ function cancelHighlighting() {
for (let i = 0; i < list2.length; ++i) { for (let i = 0; i < list2.length; ++i) {
list2 = dictionaryWords.value.split(" "); list2 = dictionaryWords.value.split(" ");
list2[i] = list2[i].replace(/(^\s*)|(\s*$)/g, ""); list2[i] = list2[i].replace(/(^\s*)|(\s*$)/g, "");
if (list2[i] !== "") { //原来代码中替换的内容为“list[i]”这个字符串这明显是错误的我们需要替换的是list[i]里的内容 if (list2[i] !== "") {
articleContent = articleContent.replace(new RegExp("<mark>"+list2[i]+"</mark>", "g"), list2[i]); articleContent = articleContent.replace("<mark>" + list[i] + "</mark>", "list[i]");
} }
} }
} }

View File

@ -7,20 +7,10 @@ function familiar(theWord) {
url:"/" + username + "/" + word + "/familiar", url:"/" + username + "/" + word + "/familiar",
success:function(response){ success:function(response){
let new_freq = freq - 1; let new_freq = freq - 1;
const allow_move = document.getElementById("move_dynamiclly").checked; if(new_freq <1) {
if (allow_move) { $("#p_" + theWord).remove();
if (new_freq <= 0) {
removeWord(theWord);
} else {
renderWord({ word: theWord, freq: new_freq });
}
} else { } else {
if(new_freq <1) { $("#freq_" + theWord).text(new_freq);
$("#p_" + theWord).remove();
} else {
$("#freq_" + theWord).text(new_freq);
}
} }
} }
}); });
@ -35,139 +25,19 @@ function unfamiliar(theWord) {
url:"/" + username + "/" + word + "/unfamiliar", url:"/" + username + "/" + word + "/unfamiliar",
success:function(response){ success:function(response){
let new_freq = parseInt(freq) + 1; let new_freq = parseInt(freq) + 1;
const allow_move = document.getElementById("move_dynamiclly").checked; $("#freq_" + theWord).text(new_freq);
if (allow_move) {
renderWord({ word: theWord, freq: new_freq });
} else {
$("#freq_" + theWord).text(new_freq);
}
} }
}); });
} }
function delete_word(theWord) { function delete_word(theWord) {
let username = $("#username").text(); let username = $("#username").text();
let word = theWord.replace('&amp;', '&'); let word = $("#word_" + theWord).text();
$.ajax({ $.ajax({
type:"GET", type:"GET",
url:"/" + username + "/" + word + "/del", url:"/" + username + "/" + word + "/del",
success:function(response){ success:function(response){
const allow_move = document.getElementById("move_dynamiclly").checked; $("#p_" + theWord).remove();
if (allow_move) {
removeWord(theWord);
} else {
$("#p_" + theWord).remove();
}
} }
}); });
} }
/*
* interface Word {
* word: string,
* freq: number
* }
* */
/**
* 传入一个词频HTML元素将其解析为Word类型的对象
*/
function parseWord(element) {
const word = element
.querySelector("a.btn.btn-light[role=button]") // 获取当前词频元素的词汇元素
.innerText // 获取词汇值;
const freq = Number.parseInt(element.querySelector(`#freq_${word}`).innerText); // 获取词汇的数量
return {
word,
freq
};
}
/**
* 使用模板将传入的单词转换为相应的HTML字符串
*/
function wordTemplate(word) {
// 这个模板应当与 templates/userpage_get.html 中的 <p id='p_${word.word}' class="new-word" > ... </p> 保持一致
return `<p id='p_${word.word}' class="new-word" >
<a id="word_${word.word}" class="btn btn-light" href='http://youdao.com/w/eng/${word.word}/#keyfrom=dict2.index'
role="button">${word.word}</a>
( <a id="freq_${word.word}" title="${word.word}">${word.freq}</a> )
<a class="btn btn-success" onclick="familiar('${word.word}')" role="button">熟悉</a>
<a class="btn btn-warning" onclick="unfamiliar('${word.word}')" role="button">不熟悉</a>
<a class="btn btn-danger" onclick="delete_word('${word.word}')" role="button">删除</a>
</p>`;
}
/**
* 删除某一词频元素
* 此处word为词频元素对应的单词
*/
function removeWord(word) {
// 根据词频信息删除元素
word = word.replace('&amp;', '&');
const element_to_remove = document.getElementById(`p_${word}`);
if (element_to_remove != null) {
element_to_remove.remove();
}
}
function renderWord(word) {
const container = document.querySelector(".word-container");
// 删除原有元素
removeWord(word.word);
// 插入新元素
let inserted = false;
const new_element = elementFromString(wordTemplate(word));
for (const current of container.children) {
const cur_word = parseWord(current);
// 找到第一个词频比它小的元素,插入到这个元素前面
if (compareWord(cur_word, word) == -1) {
container.insertBefore(new_element, current);
inserted = true;
break;
}
}
// 当word就是词频最小的词时把他补回去
if (!inserted) {
container.appendChild(new_element);
}
// 让发生变化的元素抖动
new_element.classList.add("shaking");
// 移动到该元素
new_element.scrollIntoView({behavior: "smooth", block: "center", inline: "nearest"});
// 抖动完毕后删除抖动类
setTimeout(() => {
new_element.classList.remove("shaking");
}, 1600);
}
/**
* 从string中创建一个HTML元素并返回
*/
function elementFromString(string) {
const d = document.createElement('div');
d.innerHTML = string;
return d.children.item(0);
}
/**
* 对比两个单词
* 当first小于second时返回-1
* 当first等于second时返回0
* 当first大于second时返回1
*/
function compareWord(first, second) {
if (first.freq < second.freq) {
return -1;
}
if (first.freq > second.freq) {
return 1;
}
if (first.word < second.word) {
return -1;
}
if (first.word > second.word) {
return 1;
}
return 0;
}

View File

@ -5,7 +5,7 @@
<title>账号过期</title> <title>账号过期</title>
</head> </head>
<body> <body>
<p>您的账号过期(过期日 {{expiry_date}}</p> <p>您的账号{{ username }}过期</p>
<p>为了提高服务质量English Pal 收取会员费用, 每天1元。</p> <p>为了提高服务质量English Pal 收取会员费用, 每天1元。</p>
<p>请决定你要试用的时间长度,扫描下面支付宝二维码支付。 支付时请注明<i>English Pal Membership Fee</i>。 我们会于12小时内激活账号。</p> <p>请决定你要试用的时间长度,扫描下面支付宝二维码支付。 支付时请注明<i>English Pal Membership Fee</i>。 我们会于12小时内激活账号。</p>
<p><img src="static/donate-the-author-hidden.jpg" width="120px" alt="支付宝二维码" /></p> <p><img src="static/donate-the-author-hidden.jpg" width="120px" alt="支付宝二维码" /></p>

View File

@ -5,7 +5,115 @@ You're logged in already!
{% else %} {% 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" /> <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" />
<link rel="stylesheet" href="static/css/login_service.css"> <style>
.container{
background-color: #FFFFFF;
width: 400px;
height: 500px;
margin: 7em auto;
border-radius: 1.5em;
box-shadow: 0px 11px 35px 2px rgba(0, 0, 0, 0.14);
}
.signin-heading{
padding-top: 5px;
color: #8C55AA;
font-family: 'Ubuntu', sans-serif;
font-weight: bold;
font-size: 23px;
text-align: center;
}
.username, .email, .password, .re-password{
width: 76%;
color: rgb(38, 50, 56);
font-weight: 700;
font-size: 14px;
letter-spacing: 1px;
background: rgba(136, 126, 126, 0.04);
padding: 10px 20px;
border: none;
border-radius: 20px;
outline: none;
box-sizing: border-box;
border: 2px solid rgba(124, 16, 97, 0.02);
margin-bottom: 50px;
margin-left: 46px;
text-align: center;
margin-bottom: 27px;
font-family: 'Ubuntu', sans-serif;
}
.btn{
width: 50%;
border: none;
border-radius: 20px;
box-sizing: border-box;
border: 2px solid #8C55AA;
margin-bottom: 50px;
margin-left: 90px;
padding: 10px 20px;
}
.btn:hover{
background: #8C55AA;
transition: .5s;
cursor: pointer;
color: #fff;
}
.signup{
display: flex;
justify-content: center;
align-items: center;
}
ul{
position: absolute;
display: flex;
left: 65%;
}
li{
padding: 10px;
margin: 10px;
}
a{
text-decoration: none;
list-style: none;
font-weight: bold;
font-family: 'ink free';
}
.main_menu a{
color: #fff;
font-size: 300;
}
li :hover{
color: #8C55AA;
transition: .5s;
}
h1{
font-family: 'ink free';
}
.main_menu h1{
color: #fff;
}
</style>
<div class="container"> <div class="container">

View File

@ -1,22 +1,14 @@
{% block body %} <html>
<meta charset="utf-8" name="viewport" <body>
content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes"/> <form action="/reset" method='POST'>
<link rel="stylesheet" href="static/css/login_service.css"> 旧密码:
<input type="password" name="old-password" />
<div class="container"> <br/>
新密码:
<section class="reset-heading"> <input type="password" name="new-password" />
<h1>Reset Password</h1> <br/>
</section> <input type="submit" name="submit" value="提交" />
<input type="button" name="submit" value="放弃修改" onclick="window.location.href='/{{ username }}'"/>
<form action="/reset" method="POST"> </form>
<input type="password" placeholder="原密码" class="old-password" name="old-password" required> </body>
<input type="password" placeholder="新密码" class="new-password" name="new-password" required> </html>
<input type="password" placeholder="确认新密码" class="re-new-password" name="re-new-password" required>
<input type="submit" name="submit" class="btn" value="提交"/>
<input type="button" name="submit" class="btn" value="放弃修改"
onclick="window.location.href='/{{ username }}'"/>
</form>
</div>
{% endblock %}

View File

@ -5,9 +5,117 @@ You're logged in already! <a href="/logout">Logout</a>.
{% else %} {% else %}
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<link rel="stylesheet" href="static/css/login_service.css">
<p>{{ get_flashed_messages()[0] | safe }}</p> <p>{{ get_flashed_messages()[0] | safe }}</p>
<style>
.container{
background-color: #FFFFFF;
width: 400px;
height: 500px;
margin: 7em auto;
border-radius: 1.5em;
box-shadow: 0px 11px 35px 2px rgba(0, 0, 0, 0.14);
}
.signin-heading{
padding-top: 5px;
color: #8C55AA;
font-family: 'Ubuntu', sans-serif;
font-weight: bold;
font-size: 23px;
text-align: center;
}
.username,.password, .re-password{
width: 76%;
color: rgb(38, 50, 56);
font-weight: 700;
font-size: 14px;
letter-spacing: 1px;
background: rgba(136, 126, 126, 0.04);
padding: 10px 20px;
border: none;
border-radius: 20px;
outline: none;
box-sizing: border-box;
border: 2px solid rgba(124, 16, 97, 0.02);
margin-bottom: 50px;
margin-left: 46px;
text-align: center;
margin-bottom: 27px;
font-family: 'Ubuntu', sans-serif;
}
.btn{
width: 50%;
border: none;
border-radius: 20px;
box-sizing: border-box;
border: 2px solid #8C55AA;
margin-bottom: 50px;
margin-left: 90px;
padding: 10px 20px;
}
.btn:hover{
background: #8C55AA;
transition: .5s;
cursor: pointer;
color: #fff;
}
.signup{
display: flex;
justify-content: center;
align-items: center;
}
ul{
position: absolute;
display: flex;
left: 65%;
}
li{
padding: 10px;
margin: 10px;
}
a{
text-decoration: none;
list-style: none;
font-weight: bold;
font-family: 'ink free';
}
.main_menu a{
color: #fff;
font-size: 300;
}
li :hover{
color: #8C55AA;
transition: .5s;
}
h1{
font-family: 'ink free';
}
.main_menu h1{
color: #fff;
}
</style>
<div class="container"> <div class="container">
@ -17,8 +125,8 @@ You're logged in already! <a href="/logout">Logout</a>.
<form action="/signup" method="POST"> <form action="/signup" method="POST">
<p><input type="username" name="username" placeholder="输入用户名" required="required" class="username"></p> <p><input type="username" name="username" placeholder="输入用户名" required="required" class="username"></p>
<p><input type="password" name="password" placeholder="输入密码" required="required" class="password"></p> <p><input type="password" name="password" placeholder="输入密码" class="password"></p>
<p><input type="password" name="password2" placeholder="确认密码" required="required" class="password" ></p> <p><input type="password" name="password2" placeholder="确认密码" class="password"></p>
<button type="submit" class="btn">注册</button> <button type="submit" class="btn">注册</button>
</form> </form>

View File

@ -19,20 +19,6 @@
{% endif %} {% endif %}
<title>EnglishPal Study Room for {{ username }}</title> <title>EnglishPal Study Room for {{ username }}</title>
<style>
.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); }
}
</style>
</head> </head>
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
@ -81,30 +67,22 @@
{% endif %} {% endif %}
{% if d_len > 0 %} {% if d_len > 0 %}
<p> <p><b>我的生词簿</b></p>
<b>我的生词簿</b> {% for x in lst3 %}
<label for="move_dynamiclly"> {% set word = x[0] %}
<input type="checkbox" name="move_dynamiclly" id="move_dynamiclly" checked> {% set freq = x[1] %}
允许动态调整顺序 {% if session.get('thisWord') == x[0] and session.get('time') == 1 %}
</label> <a name="aaa"></a>
</p> {% endif %}
<a name="aaa"></a> <p id='p_{{ word }}' class="new-word" >
<div class="word-container"> <a id="word_{{ word }}" class="btn btn-light" href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index'
{% for x in lst3 %} role="button">{{ word }}</a>
{% set word = x[0] %} ( <a id="freq_{{ word }}" title="{{ word }}">{{ freq }}</a> )
{% set freq = x[1] %} <a class="btn btn-success" onclick="familiar('{{ word }}')" role="button">熟悉</a>
{% if session.get('thisWord') == x[0] and session.get('time') == 1 %} <a class="btn btn-warning" onclick="unfamiliar('{{ word }}')" role="button">不熟悉</a>
{% endif %} <a class="btn btn-danger" onclick="delete_word('{{ word }}')" role="button">删除</a>
<p id='p_{{ word }}' class="new-word" > </p>
<a id="word_{{ word }}" class="btn btn-light" href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index' {% endfor %}
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>
<a class="btn btn-danger" onclick="delete_word('{{ word }}')" role="button">删除</a>
</p>
{% endfor %}
</div>
<input id="selected-words2" type="hidden" value="{{ words }}"> <input id="selected-words2" type="hidden" value="{{ words }}">
{% endif %} {% endif %}
</div> </div>

View File

@ -20,7 +20,7 @@
<title>EnglishPal Study Room for {{username}}</title> <title>EnglishPal Study Room for {{username}}</title>
</head> </head>
<body> <body>
<p>取消勾选认识的单词</p> <p>勾选认识的单词</p>
<form method="post" action="/{{username}}/mark"> <form method="post" action="/{{username}}/mark">
<input type="submit" name="add-btn" value="加入我的生词簿"/> <input type="submit" name="add-btn" value="加入我的生词簿"/>
{% for x in lst %} {% for x in lst %}
@ -30,7 +30,7 @@
: :
<a href='http://youdao.com/w/eng/{{word}}/#keyfrom=dict2.index' title={{word}}>{{word}}</a> <a href='http://youdao.com/w/eng/{{word}}/#keyfrom=dict2.index' title={{word}}>{{word}}</a>
({{x[1]}}) ({{x[1]}})
<input type="checkbox" name="marked" value="{{word}}" checked> <input type="checkbox" name="marked" value="{{word}}">
</p> </p>
{% endfor %} {% endfor %}

View File

@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from flask import * from flask import *
from markupsafe import escape
# from app import Yaml # from app import Yaml
# from app.Article import get_today_article, load_freq_history # from app.Article import get_today_article, load_freq_history
@ -107,7 +108,7 @@ def userpage(username):
# 用户过期 # 用户过期
user_expiry_date = session.get('expiry_date') user_expiry_date = session.get('expiry_date')
if datetime.now().strftime('%Y%m%d') > user_expiry_date: if datetime.now().strftime('%Y%m%d') > user_expiry_date:
return render_template('expiry.html', expiry_date=user_expiry_date) return render_template('expiry.html')
# 获取session里的用户名 # 获取session里的用户名
username = session.get('username') username = session.get('username')

View File

@ -8,6 +8,8 @@ import string
import operator import operator
import os, sys # 引入模块sys因为我要用里面的sys.argv列表中的信息来读取命令行参数。 import os, sys # 引入模块sys因为我要用里面的sys.argv列表中的信息来读取命令行参数。
import pickle_idea import pickle_idea
from app.file_open import FileOpen
def freq(fruit): def freq(fruit):
''' '''
@ -32,14 +34,15 @@ def youdao_link(s): # 有道链接
def file2str(fname):#文件转字符 def file2str(fname):#文件转字符
f = open(fname) #打开 # f = open(fname) #打开
f = FileOpen.read_only(fname)
s = f.read() #读取 s = f.read() #读取
f.close() #关闭 f.close() #关闭
return s return s
def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。 def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。
special_characters = '\_©~<=>+-/[]*&$%^@.,?!:;#()"“”—‘’{}|' # 把里面的字符都去掉 special_characters = '_©~=+[]*&$%^@.,?!:;#()"“”—‘’' # 把里面的字符都去掉
for c in special_characters: for c in special_characters:
s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况 s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况
s = s.replace('--', ' ') s = s.replace('--', ' ')
@ -80,7 +83,8 @@ def make_html_page(lst, fname):
# <a href="">word</a> # <a href="">word</a>
s += '<p>%d <a href="%s">%s</a> (%d)</p>' % (count, youdao_link(x[0]), x[0], x[1]) s += '<p>%d <a href="%s">%s</a> (%d)</p>' % (count, youdao_link(x[0]), x[0], x[1])
count += 1 count += 1
f = open(fname, 'w') # f = open(fname, 'w')
f = FileOpen.write_able(fname)
f.write(s) f.write(s)
f.close() f.close()