1
0
Fork 0

Compare commits

...

65 Commits

Author SHA1 Message Date
Hui Lan 7d5b1c0ed4 Fix ImportError: cannot import name 'url_quote' 2024-02-22 16:27:31 +08:00
Lan Hui d9e28e3a2b Tweak button size so it looks better on tablets. 2023-08-13 21:08:13 +08:00
Lan Hui 41d1d9619d Stress test code contributed by students 2023-08-13 16:14:43 +08:00
Lan Hui 30b54f8023 Tweak button style 2023-08-12 17:59:49 +08:00
Lan Hui 1e3ac7a379 Use larger buttons 2023-08-12 17:42:25 +08:00
Lan Hui 8dd6a2a343 Use an arrow for Next Article or Previsou Article 2023-08-12 17:36:49 +08:00
Lan Hui d2f30daab1 Use PonyORM instead of class RecordQuery from UseSqlite.py. Incorporated changes from Pull Request 91 contributed by He Zhengzheng. 2023-08-12 15:29:12 +08:00
Lan Hui ed1d0fd714 Show only one place after the decimal point. 2023-08-11 21:02:22 +08:00
Lan Hui f3aa407c56 Use small letters for In and Up. 2023-08-11 19:32:34 +08:00
Lan Hui e9ac50422b Make the flash message dismissible. 2023-08-11 19:28:53 +08:00
Lan Hui f4df263d6e Flash message is informative. Why not add it? 2023-08-11 19:14:51 +08:00
Lan Hui dff560cc73 Indent code using web-mode. 2023-08-11 18:39:36 +08:00
Lan Hui c110de0393 Better spacing. 2023-08-11 18:38:35 +08:00
Lan Hui aaabd3e3bb Enlarge button size so it is more table-friendly. 2023-08-11 18:34:48 +08:00
Lan Hui 9da1a1cae6 Merge branch 'Alpha-snapshot20230621' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230621 2023-08-11 18:29:54 +08:00
Hui Lan 9b1d60748d Increase button size so that it is easier to use on tablets . 2023-08-11 18:29:35 +08:00
Lan Hui 83bbd8f600 Improve the speed of loading the next article further after incorporating Chen Qiuwei et al.'s suggestions. 2023-08-11 15:48:53 +08:00
Lan Hui 1b211f107d Speed up loading next article
The key change is replacing "d1 = load_freq_history(path_prefix + 'static/frequency/ferquency.p)" with "d1 = load_freq_history(user_word_list)"
in function get_today_article() from Article.py.  Now, with a user_word_list of size about 500, the next article can be loaded within 100ms.
The new d1 is much smaller than the old one, therefore the following computation "d3 = get_difficulty_level_for_user(d1, d2)" is much faster.
The students did not feel that loading next article is slow; this is because their frequency.p is quite small.

Also log information in app/log.txt
2023-08-11 11:59:48 +08:00
Lan Hui 10c291bed2 Highlight user difficulty level too. 2023-08-10 19:12:30 +08:00
Lan Hui 6d15b65e3c Make highlighting text difficulty level work. 2023-08-10 19:09:15 +08:00
Lan Hui e4f870c995 Create folder app/db with a README file. 2023-08-10 15:25:42 +08:00
Lan Hui 06f896a33a Update .gitignore 2023-08-10 15:22:30 +08:00
Hui Lan 25c2e0aca8 README.md: update the path to wordfreqapp.db. 2023-08-10 14:24:00 +08:00
Hui Lan dca76969eb 解决程序源文件更新了,但是 docker container 中的程序源文件没有更新的问题
问题出在 build.sh 中的 --mount type=volume,src=englishpal-db,target=/app 。
运行 docker container 时,会将整个 englishpal-db 对应的文件夹 /var/lib/docker/volumes/englishpal-db/_data
下面的内容拷贝到  docker container 里面的 /app 文件夹下面。

然而,/var/lib/docker/volumes/englishpal-db/_data 下面的源程序文件并不是最新的(比如其 main.py 是 7月17日的)。
将 target=/app 改为 target=/app/db。即可解决问题。
2023-08-10 14:19:37 +08:00
Hui Lan 00ae957b27 Try to figure out how to rebuild the image after updating the source code. 2023-08-10 10:47:57 +08:00
Hui Lan a397c756cf Merge branch 'Alpha-snapshot20230621' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230621 2023-08-10 10:05:29 +08:00
Lan Hui 14ab63c85c 文章段落正确分段(在 lead class 中添加 white-space: pre-wrap;) 2023-08-10 10:03:02 +08:00
Hui Lan b8c3d9bda7 No cache while building docker image 2023-07-17 17:46:55 +08:00
Lan Hui 43419ab4b6 Enlarge text difficult level for one second (make it work using vanilla JavaScript) 2023-07-17 16:28:08 +08:00
Hui Lan 78d9a66e88 After loading the next article, show its difficulty level in a larger size for one second. 2023-07-17 16:13:56 +08:00
Hui Lan 79bdec2a7d Dockerfile: update docker image version. 2023-07-17 10:38:17 +08:00
Hui Lan fb80e952b9 Simplify the docker run options 2023-07-17 10:25:07 +08:00
Hui Lan 6ea0b970a2 (1) Downgrade Flask version from 2.3.2 to 2.0.3 as installing the higher version reports errors. This is probably due to my outdated python version (3.6). (2) Persist SQLite database data in a docker volume. Created a docker volume called englishpal-db using command 'docker volume create englishpal-db' and associate this volume with the docker image file directory /app. So, now what happens in /app will be mirrored to englishpal-db. Where is englishpal-db located? Use command 'docker volume inspect englishpal-db' to find out. 2023-07-17 07:45:38 +08:00
Lan Hui 20051e1a93 article.py: correct data format 2023-07-14 09:17:11 +08:00
Lan Hui 5711f0e826 Update flask version in requirements.txt, use escape from markupsafe package. 2023-07-14 09:11:02 +08:00
Lan Hui cc92e5e29a admin_service.py: non-programmers probably do not know int. So, use integer instead. 2023-07-14 08:32:20 +08:00
Lan Hui cd562a745c admin_service.py: do not need to have seperate lines for a statement. 2023-07-14 08:30:58 +08:00
Lan Hui c284893097 admin_service.py: correct typo parmas. 2023-07-14 08:27:33 +08:00
Lan Hui 87fd594636 admin_service.py: refactor view function article(). 2023-07-14 08:26:37 +08:00
Lan Hui 18c37d583a admin_service.py: show article content for the newly added article after clicking the button '保存'. 2023-07-14 08:15:15 +08:00
Lan Hui 472c0c115f Fix Bug 541 2023-07-09 20:26:32 +08:00
Lan Hui 9a156ebf7e Fix Bug 539 2023-07-08 18:23:45 +08:00
Hui Lan 807d74741b 修复 Bug 493。不要转义(escape)表单提交的内容。否则类 WordFreq 不能正确工作,比如转义会把单引号变成 ',这不利于 WordFreq 类处理。 2023-07-07 16:13:48 +08:00
Hui Lan 287d496ae9 Merge branch 'Alpha-snapshot20230621' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230621 2023-07-01 15:17:34 +08:00
张艺腾 582f399f73 Bug537 and Bug538 2023-06-21 16:48:45 +08:00
Hui Lan c37ee98b77 Merge branch 'fix-vuln' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230619b 2023-06-19 21:50:48 +08:00
徐宣 f40a388277 Fix: Move wordfreqapp.db to new location 2023-06-19 14:48:35 +08:00
徐宣 2277473afe Fix: Add import for 'abort' function 2023-06-18 19:49:33 +08:00
徐宣 f01c334827 Fix: no-random secret key generation and XSS vulnerability 2023-06-18 19:44:19 +08:00
Laugh 4d2535a6e8 Merge branch 'Alpha-snapshot20230605' into Bug527-ZhouZhifang 2023-06-15 15:59:29 +08:00
Laugh bb2d0363e4 Feat: update fontsize and margin of some elements 2023-06-15 15:54:46 +08:00
黄子睿 9816596cf8 删除 'app/static/js/tanyanmei-fillword.js' 2023-06-02 21:34:27 +08:00
黄子睿 682247bff1 refactor partial function and code writing specifications 2023-06-02 21:33:21 +08:00
Hui Lan b22c654f0f Merge branch 'Bug529-GuHan' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230601 2023-06-01 07:40:27 +08:00
倪玲丽 d402bb45cb 刷新屏幕,点击上下篇,加入生词库,停止阅读(更改) 2023-05-31 18:37:05 +08:00
倪玲丽 cdf6180901 刷新屏幕,点击上下篇,加入生词库,停止阅读 2023-05-30 18:48:37 +08:00
倪玲丽 38837c9c2f 合并最新的Alpha-snapshot20230529 2023-05-30 18:45:23 +08:00
黄子睿 a0ddf4bdad 上传文件至 'app/static/js'
修复了 Bug492 选中问号出现多个问号的问题。
解决了选中紧跟标点符号的单词,单词能正常显示。
优化了选中较长的文章时页面容易出现卡顿的问题。
2023-05-27 17:33:37 +08:00
倪玲丽 df64065dcc 点击上下篇,停止阅读 2023-05-06 18:19:24 +08:00
倪玲丽 ce28b91bd5 屏幕刷新,停止阅读 2023-05-06 18:18:12 +08:00
倪玲丽 d6bd24ee1c Merge branch 'Alpha' of http://121.4.94.30:3000/mrlan/EnglishPal into Bug393-TanYanMei
merge alpha
2023-04-24 12:01:08 +08:00
Hui Lan e9e2bd3d23 Remove static\js 2023-04-24 11:43:43 +08:00
Hui Lan 320a99d479 Move Tan Yanmei's fillword.js to app/static/js/tanyanmei-fillword.js. Delete the strange folder static js. 2023-04-24 11:38:16 +08:00
Hui Lan 3eca9234a9 Merge branch 'Bug393-TanYanMei' of http://121.4.94.30:3000/mrlan/EnglishPal into Bug393-TanYanMei 2023-04-24 11:32:15 +08:00
覃艳美 8924166975 上传文件至 'static\js' 2022-06-12 21:23:34 +08:00
29 changed files with 301 additions and 283 deletions

6
.gitignore vendored
View File

@ -2,12 +2,16 @@
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__/
app/log.txt

View File

@ -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/

4
Jenkinsfile vendored
View File

@ -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') {

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/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/`

View File

@ -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']),

View File

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

View File

@ -1,87 +0,0 @@
###########################################################################
# 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,4 +1,5 @@
from flask import *
from markupsafe import escape
from Login import check_username_availability, verify_user, add_user, get_expiry_date, change_password, WarningMessage

View File

@ -1,5 +1,6 @@
# System Library
from flask import *
from markupsafe import escape
# Personal library
from Yaml import yml
@ -37,6 +38,22 @@ def admin():
@adminService.route("/admin/article", methods=["GET", "POST"])
def article():
def _make_title_and_content(article_lst):
for article in article_lst:
text = escape(article.text) # Fix XSS vulnerability, contributed by Xu Xuan
article.title = text.split("\n")[0]
article.content = '<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()
@ -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 = '<br/>'.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)

1
app/db/README.txt Normal file
View File

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

View File

@ -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.
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: # 用户的词

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 escape
from flask import abort
from markupsafe import escape
from Login import *
from Article import *
import Yaml
from user_service import userService
from account_service import accountService
from admin_service import adminService, ADMIN_NAME
import os
app = Flask(__name__)
app.secret_key = 'lunch.time!'
app.secret_key = os.urandom(32)
# 将蓝图注册到Lab app
app.register_blueprint(userService)
@ -54,7 +54,6 @@ def appears_in_test(word, d):
else:
return ','.join(d[word])
@app.route("/mark", methods=['GET', 'POST'])
def mark_word():
'''

View File

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

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,3 +32,17 @@ def get_page_articles(num, size):
x
for x in Article.select().order_by(desc(Article.article_id)).page(num, size)
]
def get_all_articles():
articles = []
with db_session:
for article in Article.select():
articles.append(article.to_dict())
return articles
def get_article_by_id(article_id):
with db_session:
article = Article.get(article_id=article_id)
return [article.to_dict()]

View File

@ -9,6 +9,7 @@ js:
- ../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

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

View File

@ -29,3 +29,8 @@ function onReadClick() {
function onChooseClick() {
isChoose = !isChoose;
}
// 如果网页刷新,停止播放声音
if (performance.getEntriesByType("navigation")[0].type == "reload") {
Reader.stopRead();
}

View File

@ -22,62 +22,38 @@ function getWord() {
function highLight() {
if (!isHighlight) return;
let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml使用innerText会把原文章中所包含的<br>标签去除,导致处理后的文章内容失去了原来的格式
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;
}
else{
allWords = pickedWords.value + " ";
}
const list = allWords.split(" ");//将所有的生词放入一个list中用于后续处理
let allWords = dictionaryWords === null ? pickedWords.value + " " : pickedWords.value + " " + dictionaryWords.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('|', "");
list[i] = list[i].replace('?', "");
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>");
list[i] = list[i].replace(/(^\W*)|(\W*$)/g, ""); // 消除单词两边的非单词字符
if (list[i] != "" && !totalSet.has(list[i])) {
// 返回所有匹配单词的集合, 正则表达式RegExp()中, "\b"匹配一个单词的边界, g 表示全局匹配, i 表示对大小写不敏感。
let matches = new Set(articleContent.match(new RegExp("\\b" + list[i] + "\\b", "gi")));
if (matches.has("mark")) {
// 优先处理单词为 "mark" 的情况
totalSet = new Set(["mark", ...totalSet]);
}
totalSet = new Set([...totalSet, ...matches]);
}
}
// 删除所有的mark标签,防止标签发生嵌套
articleContent = articleContent.replace(/<(mark)[^>]*>/gi, "");
articleContent = articleContent.replace(/<(\/mark)[^>]*>/gi, "");
// 将文章中所有出现该单词word的地方改为"<mark>" + word + "<mark>"。
for (let word of totalSet) {
articleContent = articleContent.replace(new RegExp("\\b" + word + "\\b", "g"), "<mark>" + word + "</mark>");
}
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("<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]);
}
}
}
let articleContent = document.getElementById("article").innerHTML;
articleContent = articleContent.replace(/<(mark)[^>]*>/gi, "");
articleContent = articleContent.replace(/<(\/mark)[^>]*>/gi, "");
document.getElementById("article").innerHTML = articleContent;
}

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,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);
@ -114,7 +113,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();
}
}
@ -129,7 +128,7 @@ function renderWord(word) {
for (const current of container.children) {
const cur_word = parseWord(current);
// 找到第一个词频比它小的元素,插入到这个元素前面
if (compareWord(cur_word, word) == -1) {
if (compareWord(cur_word, word) === -1) {
container.insertBefore(new_element, current);
inserted = true;
break;
@ -165,17 +164,11 @@ function elementFromString(string) {
* 当first大于second时返回1
*/
function compareWord(first, second) {
if (first.freq < second.freq) {
return -1;
if (first.freq !== second.freq) {
return first.freq < second.freq ? -1 : 1;
}
if (first.freq > second.freq) {
return 1;
}
if (first.word < second.word) {
return -1;
}
if (first.word > second.word) {
return 1;
if (first.word !== second.word) {
return first.word < second.word ? -1 : 1;
}
return 0;
}

View File

@ -1,7 +1,7 @@
{% block body %}
{% if session['logged_in'] %}
你已登录 <a href="/{{ session['username'] }}">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>
你已登录 <a href="/{{ session['username'] }}/userpage">{{ 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" />
@ -32,14 +32,13 @@
<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

@ -44,6 +44,7 @@
<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 }}

View File

@ -53,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

@ -5,6 +5,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"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
{{ yml['header'] | safe }}
{% if yml['css']['item'] %}
@ -32,6 +34,24 @@
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;
}
.arrow {
padding: 0;
font-size: 20px;
line-height: 21px;
display: inline-block;
}
.arrow:hover {
cursor: pointer;
}
</style>
</head>
<body>
@ -45,22 +65,28 @@
<a class="btn btn-secondary" href="/reset" role="button" onclick="stopRead()">重设密码</a>
</p>
{# {% for message in flashed_messages %}#} {# 根据user_service.userpage,取消了参数flashed_messages因此注释了这段代码 #}
{# <div class="alert alert-warning" role="alert">Congratulations! {{ message }}</div>#}
{# {% endfor %}#}
{% 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>
{% endfor %}
<button class="btn btn-success" id="load_next_article" onclick="load_next_article()"> 下一篇 Next Article </button>
<button class="btn btn-success" id="load_pre_article" onclick="load_pre_article()" > 上一篇 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="badge bg-success" id="user-level">{{ today_article["user_level"] }}</span> and we have chosen an article with a difficulty level of <span class="badge bg-success" id="text_level">{{ today_article["text_level"] }}</span> for you.</div>
<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"><br/>
<p class="display-5" id="article_title">{{ today_article["article_title"] }}</p><br/>
<p class="lead"><font id="article" size=2>{{ today_article["article_body"] }}</font></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/>
<script type="text/javascript">
function toggle_visibility(id) { {# https://css-tricks.com/snippets/javascript/showhide-element/#}
@ -97,8 +123,8 @@
<p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p>
<form method="post" action="/{{ username }}/userpage">
<textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/>
<input type="submit" value="把生词加入我的生词库"/>
<input type="reset" value="清除"/>
<button class="btn btn-primary btn-lg" type="submit" onclick="Reader.stopRead()">把生词加入我的生词库</button>
<button class="btn btn-primary btn-lg" type="reset">清除</button>
</form>
{% if session.get['thisWord'] %}
<script type="text/javascript">
@ -147,6 +173,12 @@
{% endfor %}
{% endif %}
<script type="text/javascript">
window.onload = function () { // 页面加载时执行
// 刷新页面或进入页面时判断,若不是首篇文章,则上一篇按钮可见
if(sessionStorage.getItem('pre_page_button')!="display" && sessionStorage.getItem('pre_page_button')){
$('#load_pre_article').show();
}
};
function load_next_article(){
$.ajax({
url: '/get_next_article/{{username}}',
@ -175,7 +207,7 @@
});
}
function update(today_article){
$('#user-level').html(today_article['user_level']);
$('#user_level').html(today_article['user_level']);
$('#text_level').html(today_article["text_level"]);
$('#date').html('Article added on: '+today_article["date"]);
$('#article_title').html(today_article["article_title"]);
@ -183,13 +215,19 @@
$('#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);
document.querySelector('#user_level').classList.add('mark'); // do the same thing for user difficulty level
setTimeout(() => {document.querySelector('#user_level').classList.remove('mark');}, 2000);
}
<!-- 检查是否存在上一篇或下一篇,不存在则对应按钮隐藏-->
function check_pre(visited_articles){
if((visited_articles=='')||(visited_articles['index']<=0)){
$('#load_pre_article').hide();
sessionStorage.setItem('pre_page_button', 'display')
}else{
$('#load_pre_article').show();
sessionStorage.setItem('pre_page_button', 'show')
}
}
function check_next(result_of_generate_article){

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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" />
@ -18,11 +18,15 @@
{% endif %}
<title>EnglishPal Study Room for {{username}}</title>
</head>
<body>
<p>取消勾选认识的单词</p>
</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">
<input type="submit" name="add-btn" value="加入我的生词簿"/>
<button type="submit" name="add-btn" class="btn btn-outline-primary btn-lg">加入我的生词簿</button>
{% for x in lst %}
{% set word = x[0]%}
<p>
@ -41,5 +45,6 @@
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
</body>
</div>
</body>
</html>

43
app/test/test_stress.py Executable file
View File

@ -0,0 +1,43 @@
''' Contributed by Lin Junhong et al. 2023-06.'''
import requests
import multiprocessing
import time
def stress(username):
try:
data = {
'username': username,
'password': '123123'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36 Edg/114.0.1823.51'
}
session = requests.session()
response = session.post(url='http://127.0.0.1:5000/signup', data=data, headers=headers)
print('Sign up ', response.status_code)
time.sleep(0.5)
response = session.post(url='http://127.0.0.1:5000/login', data=data, headers=headers)
print('Sign in ', response.status_code)
time.sleep(0.5)
response = session.get(url=f'http://127.0.0.1:5000/{username}/userpage', headers=headers)
print('User page', response.status_code)
time.sleep(0.5)
print(session.cookies)
for i in range(5):
response = session.get(url=f'http://127.0.0.1:5000/get_next_article/{username}', headers=headers, cookies=session.cookies)
time.sleep(0.5)
print(f'Next page ({i}) [{username}]')
print(response.status_code)
print(response.json()['today_article']['article_title'])
except Exception as e:
print(e)
if __name__ == '__main__':
username = 'Learner'
pool = multiprocessing.Pool(processes=10)
for i in range(10):
pool.apply_async(stress, (f'{username}{i}',))
pool.close()
pool.join()

View File

@ -15,6 +15,9 @@ from wordfreqCMD import sort_in_descending_order
import pickle_idea
import pickle_idea2
import logging
logging.basicConfig(filename='log.txt', format='%(asctime)s %(message)s', level=logging.DEBUG)
# 初始化蓝图
userService = Blueprint("user_bp", __name__)
@ -32,7 +35,9 @@ def get_next_article(username):
else: # 当前不为“null”直接 index+=1
visited_articles["index"] += 1
session["visited_articles"] = visited_articles
logging.debug('/get_next_article: start calling get_today_arcile()')
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
logging.debug('/get_next_arcile: done.')
data = {
'visited_articles': visited_articles,
'today_article': today_article,
@ -129,7 +134,7 @@ def userpage(username):
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
if request.method == 'POST': # when we submit a form
content = escape(request.form['content'])
content = request.form['content']
f = WordFreq(content)
lst = f.get_freq()
return render_template('userpage_post.html',username=username,lst = lst, yml=Yaml.yml)
@ -176,7 +181,11 @@ def user_mark_word(username):
for word in request.form.getlist('marked'):
lst.append((word, [get_time()]))
d = pickle_idea2.merge_frequency(lst, lst_history)
if len(lst_history) > 999:
flash('You have way too many words in your difficult-words book. Delete some first.')
else:
pickle_idea2.save_frequency_to_pickle(d, user_freq_record)
flash('Added %s.' % (', '.join(request.form.getlist('marked'))))
return redirect(url_for('user_bp.userpage', username=username))
else:
return 'Under construction'

View File

@ -2,10 +2,7 @@
DEPLOYMENT_DIR=/home/lanhui/englishpal2/EnglishPal
cd $DEPLOYMENT_DIR
# Install dependencies
pip3 install -r requirements.txt
pwd
# Stop service
sudo docker stop EnglishPal
@ -15,7 +12,7 @@ sudo docker rm EnglishPal
sudo docker build -t englishpal .
# Run the application
sudo docker run --restart=always -d --name EnglishPal -p 90:80 -v ${DEPLOYMENT_DIR}/app/static/frequency:/app/static/frequency -v ${DEPLOYMENT_DIR}/app/static/:/app/static/ -t englishpal # for permanently saving data
sudo docker run --restart=always -d --name EnglishPal -p 90:80 -v ${DEPLOYMENT_DIR}/app/static/frequency:/app/static/frequency --mount type=volume,src=englishpal-db,target=/app/db -t englishpal # for permanently saving data
# Save space. Run it after sudo docker run
sudo docker system prune -a -f

View File

@ -1,5 +1,6 @@
Flask==1.1.2
Flask==2.0.3
selenium==3.141.0
PyYAML~=6.0
pony==0.7.16
snowballstemmer==2.2.0
Werkzeug==2.2.2