Compare commits

...

227 Commits

Author SHA1 Message Date
李思楠 8cbc7c9a0c 修复快速点击下一页按钮点击频率过快时页面跳转到未知名页面 2024-05-24 22:00:08 +08:00
丁晟晔 ff6286cf01 删除 app/test/test_bug551_DingZeYu.py 2024-05-06 11:42:32 +08:00
丁晟晔 1d7e61d751 上传文件至 app/test 2024-05-06 11:36:36 +08:00
顾涵 708a6a2821 Merge pull request 'WIP:Bug529-GuHan' (#88) from Bug529-GuHan into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/88
2023-06-04 12:39:34 +08:00
顾涵 688a198768 已经与Alpha-snapshot20230525 分支同步,重新提交 2023-05-28 16:31:12 +08:00
寻宇灿 1543b3095d Merge remote-tracking branch 'origin/Alpha-snapshot20230519' into Refactor-XunYucan 2023-05-25 22:30:06 +08:00
寻宇灿 c6bf323c60 修改格式 2023-05-25 21:23:25 +08:00
寻宇灿 03ccb3527a 重构前端阅读js,新增阅读器全局对象,新增生词朗读按钮 2023-05-25 17:35:31 +08:00
Hui Lan b41e1044bc difficulty.py: add some stop words, hoping that getting the next article can be faster. 2023-05-24 10:12:44 +08:00
Hui Lan 67e921ba60 difficulty.py: todo. 2023-05-23 22:25:40 +08:00
Hui Lan a5c3564f15 difficulty.py: do not stem a word twice. 2023-05-23 22:22:57 +08:00
Hui Lan 1295616d5b Merge branch 'Bug476-YuHuangtao' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230519 2023-05-23 19:50:30 +08:00
俞黄焘 c151a0efaa 去掉了get_difficulty_level_for_user的多出的break 2023-05-23 19:40:33 +08:00
顾涵 030b89706e special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' 用于过滤字符,我将其中的“-”删去,使连字符没有被过滤,实现录入例如fifty-six等组合词的功能。另外对于删除过滤是否会引发字符bug,答案是肯定的,但是这段代码中的过滤字符虽然多,但是并没有完全过滤掉所有字符,(过滤的只是键盘上能打出的字符,不包括输入法中能打出的特殊字符),所以字符bug本身就一直存在,我认为减少一个“-”字符对程序的过滤过程不会造成问题。 2023-05-20 15:29:12 +08:00
Hui Lan 349488167b requirements.txt: install snowballstemmer for better computing a word's difficulty level. 2023-05-19 09:03:20 +08:00
俞黄焘 39d96014d9 pull最新的snapshot-20230511,后更新了difficulty.py和Article.py的部分代码,提交了新的pickle文件 2023-05-18 23:29:38 +08:00
顾涵 acd8db6e3e special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' 用于过滤字符,我将其中的“-”删去,使连字符没有被过滤,实现录入例如fifty-six等组合词的功能。另外对于删除过滤是否会引发字符bug,答案是肯定的,但是这段代码中的过滤字符虽然多,但是并没有完全过滤掉所有字符,(过滤的只是键盘上能打出的字符,不包括输入法中能打出的特殊字符),所以字符bug本身就一直存在,我认为减少一个对“1-”字符的过滤不会造成问题。 2023-05-15 19:24:43 +08:00
顾涵 9f3f5b43e1 special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' 用于过滤字符,我将其中的“-”删去,使连字符没有被过滤,实现录入例如fifty-six等组合词的功能。另外对于删除过滤是否会引发字符bug,答案是肯定的,但是这段代码中的过滤字符虽然多,但是并没有完全过滤掉所有字符,(过滤的只是键盘上能打出的字符,不包括输入法中能打出的特殊字符),所以字符bug本身就一直存在,我认为减少一个对“-”字符的过滤不会造成问题。 2023-05-15 19:15:30 +08:00
huangdan d9f6df7fbe AJAX载入文章数据 2023-05-11 15:51:10 +08:00
huangdan 5039f5710e AJAX载入文章数据 2023-05-08 14:33:48 +08:00
Hui Lan becef7e343 Merge branch 'Bug502-YuGaoXiang' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230506 2023-05-07 15:59:35 +08:00
吴宇涵 01ecc83768 refactor: refactor the way to check article level 2023-05-06 17:42:04 +08:00
吴宇涵 f64d06fbbf fix: fix Bug 531 and use ES6 grammar 2023-05-06 17:24:51 +08:00
Hui Lan a4cc4fd011 Merge branch 'Bug522-HuangZirui' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230506 2023-05-06 17:16:08 +08:00
ZhuZhihao 18ca48b422 Merge branch 'Bug522-HuangZirui' of http://121.4.94.30:3000/mrlan/EnglishPal into Bug522-HuangZirui 2023-05-05 17:21:49 +08:00
ZhuZhihao a80b062b87 refactor: remove variable 'count' 2023-05-05 17:20:58 +08:00
Hui Lan 779dafefe8 Merge branch 'Bug509-XieQiuHan-WangZiming' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230427 2023-04-27 07:21:15 +08:00
Hui Lan e118d92659 Merge branch 'Alpha-snapshot20230425' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230427 2023-04-27 07:20:21 +08:00
王梓铭 5654fbf9bc 修改:使用新的/<username>/userpage路由 2023-04-26 18:49:59 +08:00
王梓铭 d30a434b2a 修改变量名had_read_articles->visited_articles 2023-04-25 17:47:51 +08:00
Hui Lan b88bc8f36b Merge branch 'Bug509-XieQiuHan-WangZiming' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha-snapshot20230425 2023-04-25 11:40:42 +08:00
王梓铭 6be035f282 修复当没有找到文章或者文章读完时,直接刷新页面或者session不关闭重新进入页面,导致的错误; 2023-04-25 11:38:01 +08:00
Hui Lan ef786795e2 Resolve merge conflict 2023-04-25 08:47:22 +08:00
Hui Lan 21a77ef2df Merge branch 'Alpha' of http://121.4.94.30:3000/mrlan/EnglishPal into Alpha 2023-04-25 08:42:18 +08:00
Hui Lan 58d7349afe Change from bug359-zhangkeli 2023-04-25 08:40:26 +08:00
王梓铭 fc3e27488b 给标签添加id,方便测试 2023-04-21 05:33:26 +08:00
王梓铭 03145b57d9 修复边界值问题(当刚开始就没有找到文章或者就根本被没有文章的时候,会出现上一篇按钮) 2023-04-21 02:36:51 +08:00
王梓铭 70917df47b 删除测试代码 2023-04-20 23:15:12 +08:00
王梓铭 8f132ed87b 添加了阅读完所有文章的提示 2023-04-20 22:53:30 +08:00
王梓铭 da13e5bbd5 修复Bug(没找到文章后立即上一篇会回到上上篇文章) & 标签添加id方便测试 2023-04-20 21:28:29 +08:00
王梓铭 84affaeb69 修改 /<username> 路由存在的问题(每次调用别的路由他都会被调用),新路由为 /<username>/userpage;同时因为修改了路由导致访问userpage_get的时候会导致静态文件路径生成错误,这里修改了\static\config.yml中的静态资源路径,修改后也都可以正常访问到的 2023-04-20 20:30:14 +08:00
王梓铭 16de0a7fd9 修改变量命名:existing_articles → had_read_articles 2023-04-20 15:40:11 +08:00
zzhaofisher ce2e1f2978 Merge branch 'DevLocal' into Bug522-HuangZirui 2023-04-18 21:52:28 +08:00
zzhaofisher 11ae093fd7 Merge branch 'Alpha' into Bug522-HuangZirui 2023-04-18 21:52:01 +08:00
zzhaofisher cc8ca47f8c refactor: remove sql sentences 2023-04-18 21:50:54 +08:00
zzhaofisher 5d20e92061 Merge branch 'Bug522-HuangZirui' of http://121.4.94.30:3000/mrlan/EnglishPal into DevLocal 2023-04-18 21:50:18 +08:00
Lan Hui f3d609c92b Merge Wang Ziming's work and Wu Yuhan's work. 2023-04-07 06:41:49 +08:00
王梓铭 15bb925024 将记录阅读过文章的数据结果改为字典,以及修改了flag的问题 2023-04-04 22:31:53 +08:00
Lan Hui 688ed72473 Correct grammar。 2023-04-01 16:07:59 +08:00
吴宇涵 1f150fc847 refactor: use ajax to get expiry_date 2023-03-31 13:39:28 +08:00
王梓铭 7107f634c2 Merge branch 'test' into dev-fixBug509-reconstruction 2023-03-31 04:58:21 +08:00
王梓铭 6f1dd13419 测试的print忘删了 2023-03-31 04:50:41 +08:00
Hui Lan 4417cf7017 Article.py: remove debug statement. 2023-03-30 16:10:22 +08:00
王梓铭 0c16a4dc6f 判断文章是否已经出现的语句写错位置了,改正下 2023-03-29 20:53:38 +08:00
王梓铭 5b2f5199a8 1. 取消userpage_get.html中提示删除单词信息的代码 和 取消user_service.userpage中render_template的flashed_messages参数。因为删除单词操作已经是异步了,而提示信息的出现是同步执行,所以就注释了代码且没有产生太大影响。
2. 修改取消user_service.deleteword中对注释flash代码的注释,根据上一步进行了重新解释。
2023-03-27 14:28:54 +08:00
Lan Hui 0ce1c6eb6e 文章管理页面:每篇文章中保留换行,方便查看。 2023-03-26 21:14:29 +08:00
Lan Hui d4ac709385 将删除按钮移到第一行,避免因为文章的标题过长跨行导致删除按钮形状改变。 2023-03-26 21:05:05 +08:00
Lan Hui 9eb5210d3f Level与Date的冒号后面加个空格,使得后面的信息更加看得清楚。 2023-03-26 20:58:37 +08:00
Lan Hui 0e25737381 管理文章页面的文章列表中,每篇文章不再在内容部分重新显示标题。 2023-03-26 20:56:08 +08:00
Lan Hui b3b154a24f 简化管理文章与管理用户页面信息。 2023-03-26 19:06:04 +08:00
Lan Hui 4d99405bfa 简化管理员页面信息。删除退出登录按钮,可以返回到前一页后再退出,不影响使用体验。删除'管理员您好'欢迎词,没啥意义。 2023-03-26 18:59:15 +08:00
Lan Hui 8d8b9197b6 手动输入的文字最高难度等级是4 2023-03-26 09:59:06 +08:00
Lan Hui 3bc61a602f 添加文章成功后,修改用户信息成功后,页面显示成功信息(flash messages)。 2023-03-26 09:44:39 +08:00
Hui Lan fb6d0b23ce admin_manage_user.html: 修改 JavaScript 函数名. 2023-03-25 22:21:49 +08:00
Hui Lan c6010ccbbd admin_manage_user.html: 不再需要,所以删除. 2023-03-25 21:45:37 +08:00
吴宇涵 f17995a35c fix: using new pagination mod func 2023-03-25 21:31:32 +08:00
吴宇涵 ce28a5bf65 feat: auto select expiry_date when select user 2023-03-25 21:20:19 +08:00
吴宇涵 99aa4e0990 fix: fix article title show 2023-03-25 20:41:09 +08:00
Hui Lan a220450b03 mainpage_get.html: 首页不显示管理链接(可能会安全点). 2023-03-23 22:05:36 +08:00
Hui Lan 7eb276937a admin_manage_user.html: 将默认过期时间设为365天以后. 2023-03-23 21:54:21 +08:00
Hui Lan b97210a9e0 admin_manage_article.html: 更为详细的 placeholder 内容. 2023-03-23 21:19:20 +08:00
Hui Lan e27985127a admin_manage_article.html: 文章最高难度等级是4. 2023-03-23 21:00:31 +08:00
吴宇涵 7941e5d1eb fix: fix the way to show article title 2023-03-23 17:34:37 +08:00
吴宇涵 2cf65123e9 feat: admin can manage articles and users without interfering with each other 2023-03-23 17:12:23 +08:00
吴宇涵 3e35679a91 refactor: refactor the model 2023-03-23 17:09:25 +08:00
吴宇涵 82896de336 update .gitignore 2023-03-23 16:21:02 +08:00
吴宇涵 13ccbaf25c fix: use select to choose article level 2023-03-23 13:58:11 +08:00
吴宇涵 ec6a2249ae fix: fix the pagination 2023-03-23 13:47:53 +08:00
吴宇涵 bdda754af6 fix: check current user is admin 2023-03-23 13:40:22 +08:00
吴宇涵 52025d55bc fix: add a blankspace 2023-03-23 13:35:10 +08:00
吴宇涵 5cffa1fada fix: use single quotation mark 'admin' 2023-03-23 13:32:11 +08:00
王梓铭 c9bfa08658 注释flash的使用,因为其对页面会有影响 2023-03-21 19:19:51 +08:00
王梓铭 6df25c58b4 查漏,业务中的两处前端标签不做修改,因为不被使用了 2023-03-21 18:57:00 +08:00
Hui Lan 2909b4d973 admin_service.py: 管理员页面显示的用户名按照名字排序。 2023-03-21 16:22:45 +08:00
Hui Lan 9075fe9eea Resolve merge conflicts. 2023-03-21 16:08:55 +08:00
Hui Lan 691c5b0d43 build.sh: Install requirements first. 2023-03-21 16:07:10 +08:00
吴宇涵 44db2218c1 fix: Use better conditional judgment methods 2023-03-21 12:45:57 +08:00
吴宇涵 48de496caa fix: Remove 'Assignment Expresions' & Fix annotation words 2023-03-21 12:42:59 +08:00
吴宇涵 1015704e23 fix: fix the way to check article level 2023-03-21 12:35:27 +08:00
吴宇涵 cabf6702a7 fix: add one way to set article level & rename some functions and vars 2023-03-21 12:35:27 +08:00
Lan Hui b34f260d98 requirements.txt: use an older version of Flask to avoid deployment ERROR: No matching distribution found for Flask==2.1.0. 2023-03-21 11:48:09 +08:00
Lan Hui 70d44fcf5c Login.py: fix SyntaxWrning: 'is not' with a literal. 2023-03-21 11:44:05 +08:00
吴宇涵 b80fbc936c fix: fix admin_name 2023-03-20 20:19:56 +08:00
吴宇涵 ade10e5843 feat: add admin_service blueprint 2023-03-20 20:16:48 +08:00
吴宇涵 13d8977636 fix: set specific management displays for admin 2023-03-20 20:15:58 +08:00
吴宇涵 e2b165ada8 fix: add url 'admin' into banned url list 2023-03-20 20:12:41 +08:00
吴宇涵 df82748518 feat: create classes necessary for orm operations 2023-03-20 20:09:32 +08:00
吴宇涵 376ef9bcbc feat: add pong orm requirement 2023-03-20 20:08:14 +08:00
吴宇涵 90db6534ab fix: merge latest remote master branch code 2023-03-20 15:08:06 +08:00
王梓铭 944c931c9b 完成了对bug509的修复,以及重构项目(去掉了业务中的前端脚本) 2023-03-08 16:33:13 +08:00
王梓铭 cb0132fd31 pull master代码,并成功运行 2023-02-21 20:23:24 +08:00
王梓铭 93390374ad 标注之前队伍的改动 2023-02-21 20:05:48 +08:00
mrlan 43c719b6b2 Merge pull request 'Fix bug 501 - 特殊字符&加入生词库后删除按钮失效' (#81) from Bug501-Hui into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/81
2023-01-31 16:45:50 +08:00
Hui Lan a1955341c6 Fix bug 501 - 特殊字符&加入生词库后删除按钮失效 2023-01-31 16:39:11 +08:00
mrlan 92a8b4a994 Lanhui-update-README2 (#80)
Co-authored-by: Lan Hui <1348141770@qq.com>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/80
Co-authored-by: mrlan <mrlan@noreply.121.4.94.30>
Co-committed-by: mrlan <mrlan@noreply.121.4.94.30>
2023-01-30 15:44:01 +08:00
王志豪 e10dbf9d67 Bug507-WuWenZhuo (#70)
### 修复了生词簿为空时,双击文章单词无法高亮的问题
通过增加一个判断,判断生词簿为空时,不把生词簿的内容进行处理,仅处理选中单词。

### 修复了生词簿为空时,取消高亮后,导致文章格式混乱的问题
原代码中,从数据库提取文章放到网页上时,使用的是.innerText的方法,导致原文章里包含的<br>标签丢失,导致文章格式混乱的问题。

Co-authored-by: unknown <Alcatraz@qq.com>
Co-authored-by: Hui Lan <lanhui@zjnu.edu.cn>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/70
Co-authored-by: 王志豪 <1594799762@qq.com>
Co-committed-by: 王志豪 <1594799762@qq.com>
2023-01-29 12:48:52 +08:00
李雨峰 9cdc9c6f7f Bug521-LiYuFeng-Refactor (#72)
@mrlan
蓝老师:

    本次改进内容如下:
        1. 对生词居中问题进行修改,现在已经不会居中了。
        2. 对于单词数量基数大而导致的排序速度慢的问题,我们进行了优化,提升了排序的速度。
        3. 对用户交互进行了优化,当用户点击“熟悉”或“不熟悉”之后,会自动进行排序,并会跳转到那个单词的位置,用抖动的效果来提示用户。

Co-authored-by: isaac <1141730046@qq.com>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/72
Co-authored-by: 李雨峰 <1141730046@qq.com>
Co-committed-by: 李雨峰 <1141730046@qq.com>
2023-01-29 12:01:19 +08:00
陈秋伟 972a1a5524 Bug490-ChenQiuwei (#63)
修复Bug-490,使注册时确认密码能够发挥作用,在确认密码与所设置密码不一致时,能够提示“确认密码与输入密码不一致”。

Co-authored-by: 2658626578 <2658626578@qq.com>
Co-authored-by: Hui Lan <lanhui@zjnu.edu.cn>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/63
Co-authored-by: 陈秋伟 <2658626578@qq.com>
Co-committed-by: 陈秋伟 <2658626578@qq.com>
2023-01-29 11:49:27 +08:00
mrlan c52d53596f Merge pull request 'Bug489-DingRui' (#74) from SPM2022F-CONTRIBUTORS-DingRui into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/74
2023-01-29 11:31:41 +08:00
mrlan ca8c1bf8de Merge pull request 'Bug508-CenHaotian' (#62) from Bug508-CenHaotian into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/62
2023-01-29 11:19:49 +08:00
mrlan febd0fc932 Merge pull request '用简单的方法(创建 frequency 文件夹)修复Bug 499' (#64) from Bug499-Hui into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/64
2023-01-29 11:06:17 +08:00
mrlan 03353d49b1 Bug525-Hui (#79)
Co-authored-by: Hui Lan <lanhui@zjnu.edu.cn>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/79
Co-authored-by: mrlan <mrlan@noreply.121.4.94.30>
Co-committed-by: mrlan <mrlan@noreply.121.4.94.30>
2023-01-29 10:57:58 +08:00
mrlan 1373df2a3e Merge pull request '增加单词默认勾选,并修改提示 Fix Bug 495 - Liang Li Gang' (#77) from Bug495-LiangLiGang into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/77
2023-01-29 10:27:22 +08:00
yugaoxiang 43bd0bd09d update requirement.txt 2023-01-01 22:05:32 +08:00
yugaoxiang 0209548896 提交作业 2022-12-31 18:25:13 +08:00
徐嘉辉 74eccfbebd 重复文章刷新 2022-12-20 19:55:02 +08:00
np1717 086dfcb6eb fix bug 489 2022-12-19 17:54:10 +08:00
Hui Lan 3bce450620 黄子睿: 修复 'Otherwise,' 这种情况无法高亮的问题,即 Otherwise 后面跟了个逗号 2022-12-15 10:50:04 +08:00
徐嘉辉 c76d4f21ec SPM2022F-CONTRIBUTORS-XieQiuHan 2022-12-13 20:01:50 +08:00
任榆 f0afd5f40f Merge pull request 'Bug 512 - 文章朗读问题' (#59) from Bug512-RenYu into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/59
2022-12-13 13:39:23 +08:00
任榆 5cc981b549 Merge branch 'master' into Bug512-RenYu 2022-12-13 13:28:48 +08:00
Hui Lan 417dbc22f8 highlight.js: fix Bug 522. 2022-12-09 13:19:36 +08:00
岑昊天 f37ea182f6 加入生词库过滤| 2022-12-06 17:02:59 +08:00
MR LAN 6327b11711 Made folder 'frequency' under folder 'static', and added a README file under folder 'frequency' 2022-12-06 16:06:13 +08:00
岑昊天 d58dacd71c 修复Bug508,解决带有特殊字符|的单词在文章中的高亮问题 2022-12-06 14:40:50 +08:00
张艺腾 e74f1ff477 Bug505-ZhangYiteng (#61)
bug修改只涉及到account_service.py中新增的5行。
其他增删都是重写reset.html(页面样式和login、signup页面相一致),并将reset、signup、login三个页面的共同样式抽离出独立的css文件。

Co-authored-by: Q_yt <2483750517@qq.com>
Co-authored-by: Hui Lan <lanhui@zjnu.edu.cn>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/61
Co-authored-by: 张艺腾 <2483750517@qq.com>
Co-committed-by: 张艺腾 <2483750517@qq.com>
2022-12-03 20:52:01 +08:00
丁锐 89c12337d5 在README中修改内容 2022-12-02 15:48:45 +08:00
梁立港 b65ffa6054 增加单词默认勾选,并修改提示 2022-12-01 21:04:12 +08:00
丁锐 7e3004a2e6 撤回操作 2022-11-29 16:55:48 +08:00
丁锐 3609421976 对确认密码部分保证有数据读入 2022-11-29 16:53:59 +08:00
任榆 5c85041135 Bug 512 - 文章朗读问题
在fillwowrd.js中添加了stopRead()函数,将其添加给对应按钮或超链接以终止朗读。
2022-11-25 15:42:37 +08:00
mrlan 671df67723 Bug487-WuYuhan-Refactor (#58)
将所有用于用户名验证的逻辑放入到 `UserName` 类中。

Hui

Co-authored-by: Lan Hui <1348141770@qq.com>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/58
Co-authored-by: mrlan <mrlan@noreply.121.4.94.30>
Co-committed-by: mrlan <mrlan@noreply.121.4.94.30>
2022-11-10 19:03:59 +08:00
mrlan f909201615 Merge pull request 'Bug487-WuYuhan-Refactor' (#57) from Bug487-WuYuhan-Refactor into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/57
2022-11-08 19:53:09 +08:00
Hui Lan 29ffada7eb Login.py: improve comments. 2022-11-03 22:28:25 +08:00
Hui Lan d3a796428d account_service.py: module re is no longer necessary. 2022-11-03 22:21:34 +08:00
Lan Hui 702205940c Login.py: must convert warn to string before comparing to OK 2022-11-03 22:06:24 +08:00
Lan Hui f0b5adc5e4 Login.py: fix function name 2022-11-03 22:02:32 +08:00
Hui Lan 3cfec31c3f Login.py: add missing colon 2022-11-03 22:00:47 +08:00
Hui Lan 286e884dd8 Refactor Wu Yuhan's code 2022-11-03 21:59:12 +08:00
mrlan bfd87c51f6 Merge pull request 'Bug487-WuYuhan' (#56) from Bug487-WuYuhan into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/56
2022-11-03 21:28:55 +08:00
mrlan ab01b8e19b Hui-Build (#55)
Make English Pal docker run automatically each time after Ubuntu reboot.

Hui

Co-authored-by: Hui Lan <lanhui@zjnu.edu.cn>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/55
Co-authored-by: mrlan <mrlan@noreply.121.4.94.30>
Co-committed-by: mrlan <mrlan@noreply.121.4.94.30>
2022-11-01 21:43:01 +08:00
吴宇涵 59d95d8e9f account_service.py: 导入re库使用正则匹配过滤了注册时用户名的非法字符 2022-10-21 11:07:20 +08:00
吴宇涵 5844eab6d5 account_service.py: 添加注册时用户名的非法字符过滤 2022-10-21 10:44:39 +08:00
mrlan 02ffcd3b59 Merge pull request 'Bug412-JiangLetian-Refactor' (#54) from Bug412-JiangLetian-Refactor into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/54
2022-08-05 16:18:06 +08:00
Lan Hui ecc354bc0d Refactor: use better function 2022-08-02 12:33:41 +08:00
Lan Hui 1d8671c5c7 Refactor: use better function name 2022-08-02 12:30:27 +08:00
Lan Hui 8cb34e56ba Refactor: remove duplicate code block 2022-08-02 12:26:18 +08:00
Lan Hui b5dacb9ad2 Improve comments 2022-08-02 11:52:40 +08:00
Lan Hui 47e745e774 Use better variable name (use articleContent instead of txt, and use camelCase) 2022-08-02 11:45:21 +08:00
Lan Hui 1dfe370983 Use better variable names 2022-08-02 11:39:35 +08:00
mrlan 9927515b11 Merge pull request 'Bug412-JiangLetian' (#39) from Bug412-JiangLetian into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/39
2022-08-02 11:03:39 +08:00
Lan Hui c15746bbb2 Resolve conflicts 2022-08-02 11:00:33 +08:00
mrlan b745da4c90 Merge pull request 'IMPROVE-WangWeiLong' (#35) from IMPROVE-WangWeiLong into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/35
2022-07-31 09:11:53 +08:00
mrlan 7663dfb8f4 Merge pull request 'Hui-EscapeUserInput' (#53) from Hui-EscapeUserInput into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/53
2022-07-29 16:20:01 +08:00
Lan Hui 0098fa8746 Prevent attribute injection 2022-07-29 15:26:19 +08:00
Lan Hui 828cef406c Escape user input first 2022-07-29 15:22:42 +08:00
徐幸 2c1bc98833 Bug422-XuXing (#46)
增加了返回上一篇的按钮及相关功能的实现,当点击下一篇文章跳转至下一篇时,页面中会增加一个返回上一篇按钮,点击返回上一篇按钮后可以回到上一篇。

Co-authored-by: Lan Hui <1348141770@qq.com>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/46
Co-authored-by: 徐幸 <2567198082@qq.com>
Co-committed-by: 徐幸 <2567198082@qq.com>
2022-07-21 23:13:33 +08:00
mrlan 9a89510f4e Merge pull request 'Improvement-Stewart' (#49) from Improvement-Stewart into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/49
2022-07-20 17:31:01 +08:00
Lan Hui e9eb604a22 Improve spacing and indentation. 2022-07-20 17:10:03 +08:00
Lan Hui 9beb1ad1d2 Make up the enclosing >. 2022-07-20 17:09:04 +08:00
Lan Hui 8998d6e4af Improve spacing. 2022-07-20 17:08:07 +08:00
Lan Hui 8747f35fd8 Make up the enclosing >. 2022-07-20 17:07:09 +08:00
Lan Hui fdb432031f Better indentation. 2022-07-20 17:06:11 +08:00
Lan Hui 1401870591 Use Chinese UI language. 2022-07-20 17:04:34 +08:00
Lan Hui 1ca90bb2a9 Remove superfluous 'jjj'. 2022-07-20 17:00:13 +08:00
Lan Hui ea19658212 Merge branch 'Improvement-Stewart' 2022-07-20 16:57:31 +08:00
mrlan 028e2f9d56 Merge pull request '[Refactor]: Remove loop.index0, as it is hard to understand.' (#52) from Bug400-QiuZhonghui-Refactor into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/52
2022-07-18 19:58:01 +08:00
Lan Hui 50a1093781 Remove loop.index0, as it is hard to understand. 2022-07-18 19:51:23 +08:00
陈靖毅 c21659ba7e Bug477 重构 and 增加功能 (#48)
Co-authored-by: cjybyjk <cjybyjk@zjnu.edu.cn>
Co-authored-by: Lan Hui <1348141770@qq.com>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/48
Co-authored-by: 陈靖毅 <cjybyjk@zjnu.edu.cn>
Co-committed-by: 陈靖毅 <cjybyjk@zjnu.edu.cn>
2022-07-18 16:00:08 +08:00
邱忠辉 9d420acd8b [REFACTOR] user_service.py: Added id to user name, word and frequency for click to execute AJAX request in JS (#30)
1. Add jquery.js and word_operation.js to the static folder.

2. Changed the return type of the familiar, unfamiliar and deleteword methods of user_service.py.
    ```
    original: return redirect(url_for('userpage', username=username))
    now: return "success"
    ```
3. In order to get elements in JS to make ajax requests we add ids in userpage_get.html for familiar, unfamiliar, delete buttons and so on.

4. When the user's word book was retrieved, the list returned was already sorted in descending order of frequency, so redundant code was removed in userpage_get.html.

Co-authored-by: PeterQiu <www.1392993990@qq.com>
Co-authored-by: Lan Hui <1348141770@qq.com>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/30
Co-authored-by: 邱忠辉 <1392993990@qq.com>
Co-committed-by: 邱忠辉 <1392993990@qq.com>
2022-07-18 13:29:04 +08:00
stewy 6823c10043 improved 2022-06-14 13:25:42 +08:00
stewy ee44372848 improved 2022-06-14 12:37:28 +08:00
蒋乐天 041cbd97fc 更新 'app/static/js/highlight.js' 2022-06-13 21:32:49 +08:00
lin b53e7031e5 Bug412-JiangLetian 2022-06-13 11:40:20 +08:00
缪宸硕 e48008550a Merge branch 'Bug394-MiaoChenShuo' into master 2022-06-12 21:50:36 +08:00
李凯 fde3be4c23 更新 'app/Article.py' 2022-06-11 23:21:17 +08:00
李凯 4817557099 更新 'app/Article.py' 2022-06-11 23:20:41 +08:00
李凯 5d5f4cf8f2 更新 'app/Article.py' 2022-06-11 23:13:08 +08:00
李凯 e40ebac452 更新 'app/static/css/bootstrap.css' 2022-06-11 23:08:29 +08:00
miaochenshuo 260f62967b 修复 Bug394 2022-06-05 23:36:55 +08:00
mrlan b9cf94da74 Merge pull request 'build.sh: use a variable DEPLOYMENT_DIR to represent the code folder' (#24) from Deployment into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/24
2022-02-07 13:58:04 +08:00
Lan Hui 8647af3158 build.sh: use a variable DEPLOYMENT_DIR to represent the code folder 2022-02-07 13:56:14 +08:00
Hui Lan 2a92881581 README.md: update path 2022-02-05 23:05:00 +08:00
Hui Lan 2d4c516676 README.md: new code repository folder for deployment 2022-02-05 23:03:43 +08:00
Hui Lan 5976f758a0 Remove pyc files. 2022-02-05 23:01:01 +08:00
Hui Lan 18684ada6b build.sh: deploy the new code 2022-02-05 22:59:45 +08:00
mrlan e288b19678 Merge pull request 'ChenTianle-TypoCorrection' (#23) from ChenTianle-TypoCorrection into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/23
2022-01-27 20:18:17 +08:00
Lan Hui f675870401 templates/mainpage_get.html: Use more appropriate word. 将'登出'按钮改为'退出' -- 陈天乐 2022-01-27 20:15:26 +08:00
Lan Hui 8dab62df7e templates/mainpage_get.html: fix typo -- 陈天乐 2022-01-27 20:12:10 +08:00
mrlan c43e4c3f7c Merge pull request 'Use named style for sqlite3 execute()' (#22) from Hui-SaferSQL into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/22
2022-01-27 17:09:29 +08:00
Lan Hui 18dfad910b Use named style for sqlite3 execute() 2022-01-27 17:01:03 +08:00
mrlan 1d7939acf4 Merge pull request 'templates/signup.html: user name must not be empty. Bug reported and fixed by 丁宇超. For more details, see http://118.25.96.118/bugzilla/show_bug.cgi?id=347 and http://118.25.96.118/kanboard/?controller=BoardViewController&action=show&project_id=183' (#21) from BugFix347 into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/21
2022-01-27 13:04:26 +08:00
Lan Hui 788bca8873 templates/signup.html: user name must not be empty. Bug reported and fixed by 丁宇超. For more details, see http://118.25.96.118/bugzilla/show_bug.cgi?id=347 and http://118.25.96.118/kanboard/?controller=BoardViewController&action=show&project_id=183 2022-01-27 13:02:27 +08:00
mrlan 922e1ee652 Merge pull request 'WangXuan-Highlight-Pronounce' (#20) from WangXuan-Highlight-Pronounce into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/20
2022-01-27 12:57:45 +08:00
Lan Hui 42356c222d templates/mainpage_get.html: remove superfluous () 2022-01-27 12:48:25 +08:00
Lan Hui edb801b95e Show the expiry notice if the account has expired. 2022-01-27 12:45:09 +08:00
Lan Hui 718b98ef02 Login.py: use password instead of psd because psd may cause confusion. 2022-01-27 12:28:41 +08:00
Lan Hui 0c923f7db2 Login.py: simplify md5. 2022-01-27 12:24:20 +08:00
Lan Hui d4c1b1fdcc Login.py: update expiry date. 2022-01-27 11:57:40 +08:00
Lan Hui a94a333366 Remove redundant css files. 2022-01-27 11:49:41 +08:00
Lan Hui ba2ff7dee2 Remove music player js because that is not the focus of EnglishPal. If people want to listen music, they have better options than EnglishPal. 2022-01-27 11:40:58 +08:00
Lan Hui b0d80096d6 templates/userpage_get.html: change the position and order of the checkboxes. 2022-01-27 11:27:26 +08:00
Lan Hui 4d2dd2b68e Repalce old app folder with SoftArch王炫/english-pal-master/app/ 2022-01-26 21:10:09 +08:00
Lan Hui 72deae7d3a requirements.txt: include PyYAML. 2022-01-26 21:03:03 +08:00
mrlan abd8882541 Merge pull request 'Lanhui-Selenium' (#19) from Lanhui-Selenium into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/19
2021-12-03 09:19:56 +08:00
Hui Lan 246ddcc62a Remove HOME_PAGE as we use URL now. 2021-12-03 09:18:34 +08:00
Hui Lan f8a5cc2a40 test: use localhost to test 2021-12-03 09:16:18 +08:00
Hui Lan 3c118a53e2 add conftest.py 2021-12-03 09:08:45 +08:00
mrlan 5cc430714a Merge pull request 'README.md: correct section levels.' (#18) from Lanhui-update-README into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/18
2021-10-17 15:03:30 +08:00
Lan Hui 9ffc124ac3 README.md: correct section levels. 2021-10-17 15:02:54 +08:00
mrlan 9bba40d31b Merge pull request 'Lanhui-update-README' (#17) from Lanhui-update-README into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/17
2021-10-17 14:55:49 +08:00
Lan Hui 4c9b471317 README.md: better organization. 2021-10-17 14:54:48 +08:00
Lan Hui 84e98d8bd9 Merge branch 'master' of http://121.4.94.30:3000/mrlan/EnglishPal into Lanhui-update-README 2021-10-17 14:32:10 +08:00
Lan Hui a3de113751 commit build.sh 2021-10-17 14:31:58 +08:00
mrlan 7153c5aeb5 Merge pull request 'BugFix300' (#16) from BugFix300 into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/16
2021-09-05 20:52:31 +08:00
Lan Hui ffc33852ae main.py: make sure the key articleID exists. 2021-09-05 20:51:28 +08:00
Lan Hui 91ca6fe33b Merge branch 'BugFix300' of http://121.4.94.30:3000/mrlan/EnglishPal into BugFix300 2021-09-05 20:44:55 +08:00
Lan Hui 23288f5282 Merge branch 'master' of http://121.4.94.30:3000/mrlan/EnglishPal 2021-09-05 20:44:14 +08:00
Hui Lan 02a5d4bd47 main.py: fix bug300: http://118.25.96.118/bugzilla/show_bug.cgi?id=300 2021-09-05 20:42:18 +08:00
mrlan ba2a48611e Merge pull request 'Lanhui-IncreaseChanceOfGettingDifficultArticles' (#15) from Lanhui-IncreaseChanceOfGettingDifficultArticles into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/15
2021-08-31 21:32:19 +08:00
Hui Lan 2a509f83cc Merge branch 'master' of http://121.4.94.30:3000/mrlan/EnglishPal into BugFix293 2021-08-31 21:27:37 +08:00
Hui Lan f8acf03acc main.py: let the user have a small chance to get a much more difficult article than his vocabulary level. 2021-08-31 21:25:17 +08:00
mrlan 6754930707 Merge pull request 'BugFix293' (#14) from BugFix293 into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/14
2021-08-30 08:31:56 +08:00
Lan Hui 7091969703 Merge branch 'master' of http://121.4.94.30:3000/mrlan/EnglishPal 2021-08-20 19:33:58 +08:00
Lan Hui ac227c4c1a Downloaded this repository using ftp. file mode changed. 2021-08-20 19:33:51 +08:00
mrlan 943c50f964 Merge pull request 'requirements.txt includes dependencies of the program. difficulty.py is modified in this fix. Not sure why build.sh is included.' (#13) from BugFix284 into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/13
2021-08-20 19:29:52 +08:00
Lan Hui 5e322f067b requirements.txt includes dependencies of the program. difficulty.py is modified in this fix. Not sure why build.sh is included. 2021-08-20 19:27:44 +08:00
48 changed files with 14158 additions and 925 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ app/static/frequency/frequency.p
app/static/wordfreqapp.db
app/static/donate-the-author.jpg
app/static/donate-the-author-hidden.jpg
app/model/__pycache__/

View File

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

195
README.md
View File

@ -1,5 +1,5 @@
EnglishPal - Learn English Words Smartly
==========================================
# EnglishPal - Learn English Words Smartly
Hui Lan <hui.lan@cantab.net>
@ -7,68 +7,58 @@ Hui Lan <hui.lan@cantab.net>
1 November 2019
What is it?
-----------------
## What is it?
EnglishPal allows the user to build his list of new English words
picked from articles selected for him according his vocabulary level.
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.
Run it on a local machine
-------------------------
## Run on your own laptop
python3 main.py
`python3 main.py`
Make sure you have put the SQLite database file in the path `app/static` (see below).
## Run it as a Docker container
Assuming that docker has been installed and that you are a sudo user (i.e., sudoer), start the program by typing the following command in directory `EnglishPal`:
`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`.
### Explanation on the commands in build.sh
My steps for deploying English on a Ubuntu server.
- ssh to ubuntu@118.*.*.118
- 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`
- Rebuild container. Run the following command to rebuild a docker image each time after the source 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`.
- Save disk 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
- `sudo docker ps -a`
- `sudo docker logs image_name`, where `image_name` could be obtained from `sudo docker ps`.
Run it within Docker
--------------------
## Importing articles
Assuming that docker has been installed and that you are a sudoer ...
ssh to ubuntu@118.25.96.118
cd to /home/lanhui/englishpal
\# Stop all docker service
sudo service docker restart
\# If you know the container ID, then the above comamnd is an overkill. Use the following command instead.
sudo docker stop ContainerID
\# You could get all container ID with the following command.
sudo docker ps
\# Rebuild container. Run this after modifying the source code.
sudo docker build -t englishpal .
\# Run the application
sudo docker run -d -p 90:80 -v /home/lanhui/englishpal/app/static/frequency:/app/static/frequency -t englishpal \# for permanently saving data
sudo docker run -d -p 90:80 -t englishpal \# data will be lost after existing
\# Save space. Run it after sudo docker run
sudo docker system prune -a -f
\# Other commands
sudo docker ps -a
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 the web application.
Update articles
---------------
All articles are stored in the `article` table in a SQLite file called
`app/static/wordfreqapp.db`.
@ -77,6 +67,12 @@ 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).
### 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
Export wordfreqapp.db to wordfreqapp.sql using the following commands:
- sqlite3 wordfreqapp.db
@ -91,59 +87,71 @@ Put wordfreqapp.sql (not wordfreqapp.db) under version control.
### Creating SQLite file from wordfreqapp.sql
Create wordfreqapp.db using this command: `cat wordfreqapp.sql |
sqlite3 wordfreqapp.db`. Delete wordfreqapp.db first if it exists.
### Uploading wordfreqapp.db
pscp wordfreqapp.db lanhui@118.25.96.118:/home/lanhui/englishpal/app/static
### Uploading wordfreqapp.db to the server
`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/static`
Feedback
---------
## Feedback
We welcome feedback on EnglishPal. Feedback examples:
### Feedback 1
- "Need a phone app. I use phone a lot. You cannot ask students to use computers."
### Respondent 1 -- Tianhua
### 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 --- Paid 10 yuan
- “收集生词吧”改成“生词收集栏”
- 不要自动显示下一篇
- 需要有“上一篇”、“下一篇”按钮。
“成为会员”改成“注册”
“登出”改成“退出”
“收集生词吧”改成“生词收集栏”
***不要自动显示下一篇
需要有“上一篇”、“下一篇”
### TODO
## Bug tracking
Fix Bug: Internal server error when register using email address.
Usability testing
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.
Improvements made by contributors
-----------------------------------
- 朱文绮
## TODO
- Fix Bug: Internal server error when register using an email address.
- Usability testing
## Improvements made by contributors (incomplete list)
### 朱文绮
在生词簿每个单词后面,加上两个按钮,熟悉与不熟悉:
1.如果点熟悉就将生词簿中该单词后面记录的添加次数减一直至减为0就将该单词从生词簿中移除。
- 如果点熟悉就将生词簿中该单词后面记录的添加次数减一直至减为0就将该单词从生词簿中移除。
2.如果点不熟悉,就将生词簿中该单词后面记录的添加次数加一。
- 如果点不熟悉,就将生词簿中该单词后面记录的添加次数加一。
### 李康恬
- 李康恬
Add the function of "Delete already known and well-known words from
the words' library", on the one hand, it can conform to the usage
@ -152,10 +160,10 @@ too many words that they already know, on the other hand, it can
reduce unnecessary memory occupied by the database, in addition, it
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
- 占健豪
### 占健豪
Click the Familiar or Unfamiliar button (current word frequency>1), the current word position is displayed at the top of the page;
@ -163,13 +171,26 @@ Click the Familiar or Unfamiliar button (current word frequency is 1), and the p
Demo video link: https://b23.tv/QuB77m
- 张小飞
### 张小飞
修复了以下漏洞。
漏洞:用****这段字符可以作为任何账号的密码登录。
漏洞:用 ` or 1=1` 这段字符可以作为任何账号的密码登录。
Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=215
*Last modified on 2021-07-16*
### 丁锐
修复了以下漏洞
漏洞:新用户在创建账号时,不需要输入确定密码也可以注册成功,并且新账户可以正常使用。
Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=489
*Last modified on 2023-01-30*

140
app/Article.py Normal file
View File

@ -0,0 +1,140 @@
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
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
path_prefix = '/var/www/wordfreq/wordfreq/'
path_prefix = './' # 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)
def get_article_title(s):
return s.split('\n')[0]
def get_article_body(s):
lst = s.split('\n')
lst.pop(0) # remove the first line
return '\n'.join(lst)
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")
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()
random.shuffle(result)
# Choose article according to reader's level
d1 = load_freq_history(path_prefix + 'static/frequency/frequency.p')
d2 = load_freq_history(path_prefix + 'static/words_and_tests.p')
d3 = get_difficulty_level_for_user(d1, d2)
d = None
result_of_generate_article = "not found"
d_user = load_freq_history(user_word_list)
user_level = user_difficulty_level(d_user, d3) # more consideration as user's behaviour is dynamic. Time factor should be considered.
text_level = 0
if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章
amount_of_visited_articles = len(visited_articles["article_ids"])
amount_of_existing_articles = result.__len__()
if amount_of_visited_articles == amount_of_existing_articles: # 如果当前阅读过的文章的数量 == 存在的文章的数量,即所有的书本都阅读过了
result_of_generate_article = "had read all articles"
else:
for k in range(3): # 最多尝试3次
for reading in result:
text_level = text_difficulty_level(reading['text'], d3)
factor = random.gauss(0.8, 0.1) # a number drawn from Gaussian distribution with a mean of 0.8 and a stand deviation of 1
if reading['article_id'] not in visited_articles["article_ids"] and within_range(text_level, user_level, (8.0 - user_level) * factor): # 新的文章之前没有出现过且符合一定范围的水平
d = reading
visited_articles["article_ids"].append(d['article_id']) # 列表添加新的文章id下面进行
result_of_generate_article = "found"
break
if result_of_generate_article == "found": # 用于成功找到文章后及时退出外层循环
break
if result_of_generate_article != "found": # 阅读完所有文章或者循环3次没有找到适合的文章则放入空“null”
visited_articles["article_ids"].append('null')
else: # 生成已经阅读过的文章
d = random.choice(result)
text_level = text_difficulty_level(d['text'], d3)
result_of_generate_article = "found"
today_article = None
if d:
today_article = {
"user_level": '%4.2f' % user_level,
"text_level": '%4.2f' % text_level,
"date": d['date'],
"article_title": get_article_title(d['text']),
"article_body": get_article_body(d['text']),
"source": d["source"],
"question": get_question_part(d['question']),
"answer": get_answer_part(d['question'])
}
return visited_articles, today_article, result_of_generate_article
def load_freq_history(path):
d = {}
if os.path.exists(path):
d = pickle_idea.load_record(path)
return d
def within_range(x, y, r):
return x > y and abs(x - y) <= r
def get_question_part(s):
s = s.strip()
result = []
flag = 0
for line in s.split('\n'):
line = line.strip()
if line == 'QUESTION':
result.append(line)
flag = 1
elif line == 'ANSWER':
flag = 0
elif flag == 1:
result.append(line)
return '\n'.join(result)
def get_answer_part(s):
s = s.strip()
result = []
flag = 0
for line in s.split('\n'):
line = line.strip()
if line == 'ANSWER':
flag = 1
elif flag == 1:
result.append(line)
return '\n'.join(result)

95
app/Login.py Normal file
View File

@ -0,0 +1,95 @@
import hashlib
import string
from datetime import datetime, timedelta
from UseSqlite import InsertQuery, RecordQuery
def md5(s):
'''
MD5摘要
:param str: 字符串
:return: 经MD5以后的字符串
'''
h = hashlib.md5(s.encode(encoding='utf-8'))
return h.hexdigest()
# import model.user after the defination of md5(s) to avoid circular import
from model.user import get_user_by_username, insert_user, update_password_by_username
path_prefix = '/var/www/wordfreq/wordfreq/'
path_prefix = './' # comment this line in deployment
def verify_pass(newpass,oldpass):
if(newpass==oldpass):
return True
def verify_user(username, password):
user = get_user_by_username(username)
encoded_password = md5(username + password)
return user is not None and user.password == encoded_password
def add_user(username, password):
start_date = datetime.now().strftime('%Y%m%d')
expiry_date = (datetime.now() + timedelta(days=30)).strftime('%Y%m%d') # will expire after 30 days
# 将用户名和密码一起加密,以免暴露不同用户的相同密码
password = md5(username + password)
insert_user(username=username, password=password, start_date=start_date, expiry_date=expiry_date)
def check_username_availability(username):
existed_user = get_user_by_username(username)
return existed_user is None
def change_password(username, old_password, new_password):
'''
修改密码
:param username: 用户名
:param old_password: 旧的密码
:param new_password: 新密码
:return: 修改成功:True 否则:False
'''
if not verify_user(username, old_password): # 旧密码错误
return False
# 将用户名和密码一起加密,以免暴露不同用户的相同密码
if verify_pass(new_password,old_password): #新旧密码一致
return False
update_password_by_username(username, new_password)
return True
def get_expiry_date(username):
user = get_user_by_username(username)
if user is None:
return '20191024'
else:
return user.expiry_date
class UserName:
def __init__(self, username):
self.username = username
def validate(self):
if len(self.username) > 20:
return f'{self.username} is too long. The user name cannot exceed 20 characters.'
if self.username.startswith('.'): # a user name must not start with a dot
return 'Period (.) is not allowed as the first letter in the user name.'
if ' ' in self.username: # a user name must not include a whitespace
return 'Whitespace is not allowed in the user name.'
for c in self.username: # a user name must not include special characters, except non-leading periods or underscores
if c in string.punctuation and c != '.' and c != '_':
return f'{c} is not allowed in the user name.'
if self.username in ['signup', 'login', 'logout', 'reset', 'mark', 'back', 'unfamiliar', 'familiar', 'del', 'admin']:
return 'You used a restricted word as your user name. Please come up with a better one.'
return 'OK'
class WarningMessage:
def __init__(self, s):
self.s = s
def __str__(self):
return UserName(self.s).validate()

View File

@ -1,87 +1,87 @@
###########################################################################
# Copyright 2019 (C) Hui Lan <hui.lan@cantab.net>
# Written permission must be obtained from the author for commercial uses.
###########################################################################
# Reference: Dusty Phillips. Python 3 Objected-oriented Programming Second Edition. Pages 326-328.
# Copyright (C) 2019 Hui Lan
import sqlite3
class Sqlite3Template:
def __init__(self, db_fname):
self.db_fname = db_fname
def connect(self, db_fname):
self.conn = sqlite3.connect(self.db_fname)
def instructions(self, query_statement):
raise NotImplementedError()
def operate(self):
self.conn.row_factory = sqlite3.Row
self.results = self.conn.execute(self.query) # self.query is to be given in the child classes
self.conn.commit()
def format_results(self):
raise NotImplementedError()
def do(self):
self.connect(self.db_fname)
self.instructions(self.query)
self.operate()
def instructions_with_parameters(self, query_statement, parameters):
self.query = query_statement
self.parameters = parameters
def do_with_parameters(self):
self.connect(self.db_fname)
self.instructions_with_parameters(self.query, self.parameters)
self.operate_with_parameters()
def operate_with_parameters(self):
self.conn.row_factory = sqlite3.Row
self.results = self.conn.execute(self.query, self.parameters) # self.query is to be given in the child classes
self.conn.commit()
class InsertQuery(Sqlite3Template):
def instructions(self, query):
self.query = query
class RecordQuery(Sqlite3Template):
def instructions(self, query):
self.query = query
def format_results(self):
output = []
for row_dict in self.results.fetchall():
lst = []
for k in dict(row_dict):
lst.append( row_dict[k] )
output.append(', '.join(lst))
return '\n\n'.join(output)
def get_results(self):
result = []
for row_dict in self.results.fetchall():
result.append( dict(row_dict) )
return result
if __name__ == '__main__':
#iq = InsertQuery('RiskDB.db')
#iq.instructions("INSERT INTO inspection Values ('FoodSupplies', 'RI2019051301', '2019-05-13', '{}')")
#iq.do()
#iq.instructions("INSERT INTO inspection Values ('CarSupplies', 'RI2019051302', '2019-05-13', '{[{\"risk_name\":\"elevator\"}]}')")
#iq.do()
rq = RecordQuery('wordfreqapp.db')
rq.instructions("SELECT * FROM article WHERE level=3")
rq.do()
#print(rq.format_results())
###########################################################################
# 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())

27
app/Yaml.py Normal file
View File

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

110
app/account_service.py Normal file
View File

@ -0,0 +1,110 @@
from flask import *
from Login import check_username_availability, verify_user, add_user, get_expiry_date, change_password, WarningMessage
# 初始化蓝图
accountService = Blueprint("accountService", __name__)
### Sign-up, login, logout ###
@accountService.route("/signup", methods=['GET', 'POST'])
def signup():
'''
注册
:return: 根据注册是否成功返回不同界面
'''
if request.method == 'GET':
# GET方法直接返回注册页面
return render_template('signup.html')
elif request.method == 'POST':
# POST方法需判断是否注册成功再根据结果返回不同的内容
username = escape(request.form['username'])
password = escape(request.form['password'])
#! 添加如下代码为了过滤注册时的非法字符
warn = WarningMessage(username)
if str(warn) != 'OK':
return jsonify({'status': '3', 'warn': str(warn)})
available = check_username_availability(username)
if not available: # 用户名不可用
return jsonify({'status': '0'})
else: # 添加账户信息
add_user(username, password)
verified = verify_user(username, password)
if verified:
# 写入session
session['logged_in'] = True
session[username] = username
session['username'] = username
session['expiry_date'] = get_expiry_date(username)
session['visited_articles'] = None
return jsonify({'status': '2'})
else:
return jsonify({'status': '1'})
@accountService.route("/login", methods=['GET', 'POST'])
def login():
'''
登录
:return: 根据登录是否成功返回不同页面
'''
if request.method == 'GET':
# GET请求
return render_template('login.html')
elif request.method == 'POST':
# POST方法用于判断登录是否成功
# check database and verify user
username = escape(request.form['username'])
password = escape(request.form['password'])
verified = verify_user(username, password)
if verified:
# 登录成功写入session
session['logged_in'] = True
session[username] = username
session['username'] = username
user_expiry_date = get_expiry_date(username)
session['expiry_date'] = user_expiry_date
session['visited_articles'] = None
return jsonify({'status': '1'})
else:
return jsonify({'status': '0'})
@accountService.route("/logout", methods=['GET', 'POST'])
def logout():
'''
登出
:return: 重定位到主界面
'''
# 将session标记为登出状态
session['logged_in'] = False
return redirect(url_for('mainpage'))
@accountService.route("/reset", methods=['GET', 'POST'])
def reset():
'''
重设密码
:return: 返回适当的页面
'''
# 下列方法用于防止未登录状态下的修改密码
if not session.get('logged_in'):
return render_template('login.html')
username = session['username']
if username == '':
return redirect('/login')
if request.method == 'GET':
# GET请求返回修改密码页面
return render_template('reset.html', username=session['username'], state='wait')
else:
# POST请求用于提交修改后信息
old_password = escape(request.form['old-password'])
new_password = escape(request.form['new-password'])
flag = change_password(username, old_password, new_password) # flag表示是否修改成功
if flag:
session['logged_in'] = False
return jsonify({'status':'1'}) # 修改成功
else:
return jsonify({'status':'2'}) # 修改失败

142
app/admin_service.py Normal file
View File

@ -0,0 +1,142 @@
# System Library
from flask import *
# Personal library
from Yaml import yml
from model.user import *
from model.article import *
ADMIN_NAME = "lanhui" # unique admin name
_cur_page = 1 # current article page
_page_size = 5 # article sizes per page
adminService = Blueprint("admin_service", __name__)
def check_is_admin():
# 未登录,跳转到未登录界面
if not session.get("logged_in"):
return render_template("not_login.html")
# 用户名不是admin_name
if session.get("username") != ADMIN_NAME:
return "You are not admin!"
return "pass"
@adminService.route("/admin", methods=["GET"])
def admin():
is_admin = check_is_admin()
if is_admin != "pass":
return is_admin
return render_template(
"admin_index.html", yml=yml, username=session.get("username")
)
@adminService.route("/admin/article", methods=["GET", "POST"])
def article():
global _cur_page, _page_size
is_admin = check_is_admin()
if is_admin != "pass":
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
except ValueError:
return "page parmas must be int!"
_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:])
context = {
"article_number": _article_number,
"text_list": _articles,
"page_size": _page_size,
"cur_page": _cur_page,
"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!"
if delete_id: # delete article
delete_article_by_id(delete_id)
_update_context()
elif request.method == "POST":
data = request.form
content = data.get("content", "")
source = data.get("source", "")
question = data.get("question", "")
level = data.get("level", "4")
if content:
if level not in ['1', '2', '3', '4']:
return "Level must be between 1 and 4."
add_article(content, source, level, question)
_update_context()
title = content.split('\n')[0]
flash(f'Article added. Title: {title}')
return render_template("admin_manage_article.html", **context)
@adminService.route("/admin/user", methods=["GET", "POST"])
def user():
is_admin = check_is_admin()
if is_admin != "pass":
return is_admin
context = {
"user_list": get_users(),
"username": session.get("username"),
}
if request.method == "POST":
data = request.form
username = data.get("username","")
new_password = data.get("new_password", "")
expiry_time = data.get("expiry_time", "")
if username:
if new_password:
update_password_by_username(username, new_password)
flash(f'Password updated to {new_password}')
if expiry_time:
update_expiry_time_by_username(username, "".join(expiry_time.split("-")))
flash(f'Expiry date updated to {expiry_time}.')
return render_template("admin_manage_user.html", **context)
@adminService.route("/admin/expiry", methods=["GET"])
def user_expiry_time():
is_admin = check_is_admin()
if is_admin != "pass":
return is_admin
username = request.args.get("username", "")
if not username:
return "Username can't be empty."
user = get_user_by_username(username)
if not user:
return "User does not exist."
return user.expiry_date

View File

@ -8,6 +8,7 @@
import pickle
import math
from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order
import snowballstemmer
def load_record(pickle_fname):
@ -17,51 +18,68 @@ def load_record(pickle_fname):
return d
def difficulty_level_from_frequency(word, d):
level = 1
if not word in d:
return level
if 'what' in d:
ratio = (d['what']+1)/(d[word]+1) # what is a frequent word
level = math.log( max(ratio, 1), 2)
def convert_test_type_to_difficulty_level(d):
"""
对原本的单词库中的单词进行难度评级
:param d: 存储了单词库pickle文件中的单词的字典
:return:
"""
result = {}
L = list(d.keys()) # in d, we have test types (e.g., CET4,CET6,BBC) for each word
level = min(level, 8)
return level
for k in L:
if 'CET4' in d[k]:
result[k] = 4 # CET4 word has level 4
elif 'OXFORD3000' in d[k]:
result[k] = 5
elif 'CET6' in d[k] or 'GRADUATE' in d[k]:
result[k] = 6
elif 'OXFORD5000' in d[k] or 'IELTS' in d[k]:
result[k] = 7
elif 'BBC' in d[k]:
result[k] = 8
return result # {'apple': 4, ...}
def get_difficulty_level(d1, d2):
d = {}
L = list(d1.keys()) # in d1, we have freuqence for each word
L2 = list(d2.keys()) # in d2, we have test types (e.g., CET4,CET6,BBC) for each word
L.extend(L2)
L3 = list(set(L)) # L3 contains all words
for k in L3:
if k in d2:
if 'CET4' in d2[k]:
d[k] = 4 # CET4 word has level 4
elif 'CET6' in d2[k]:
d[k] = 6
elif 'BBC' in d2[k]:
d[k] = 8
if k in d1: # BBC could contain easy words that are not in CET4 or CET6. So 4 is not reasonable. Recompute difficulty level.
d[k] = min(difficulty_level_from_frequency(k, d1), d[k])
elif k in d1:
d[k] = difficulty_level_from_frequency(k, d1)
def get_difficulty_level_for_user(d1, d2):
"""
d2 来自于词库的35511个已标记单词
d1 用户不会的词
在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, ...}
stemmer = snowballstemmer.stemmer('english')
return d
for k in d1: # 用户的词
if k in d2: # 如果用户的词以原型的形式存在于词库d2中
continue # 无需评级,跳过
else:
stem = stemmer.stemWord(k)
if stem in d2: # 如果用户的词的词根存在于词库d2的词根库中
d2[k] = d2[stem] # 按照词根进行评级
else:
d2[k] = 3 # 如果k的词根都不在那么就当认为是3级
return d2
def revert_dict(d):
'''
In d2, time is the key, and the value is a list of words picked at that time.
In d, word is the key, and value is a list of dates.
In d2 (the returned value of this function), time is the key, and the value is a list of words picked at that time.
'''
d2 = {}
for k in d:
lst = d[k]
if type(d[k]) is list: # d[k] is a list of dates.
lst = d[k]
elif type(d[
k]) is int: # for backward compatibility. d was sth like {'word':1}. The value d[k] is not a list of dates, but a number representing how frequent this word had been added to the new word book.
freq = d[k]
lst = freq * ['2021082019'] # why choose this date? No particular reasons. I fix the bug in this date.
for time_info in lst:
date = time_info[:10] # until hour
date = time_info[:10] # until hour
if not date in d2:
d2[date] = [k]
else:
@ -70,42 +88,44 @@ def revert_dict(d):
def user_difficulty_level(d_user, d):
d_user2 = revert_dict(d_user) # key is date, and value is a list of words added in that date
d_user2 = revert_dict(d_user) # key is date, and value is a list of words added in that date
count = 0
geometric = 1
for date in sorted(d_user2.keys(), reverse=True): # most recently added words are more important while determining user's level
lst = d_user2[date] # a list of words
lst2 = [] # a list of tuples, (word, difficulty level)
for word in lst:
for date in sorted(d_user2.keys(),
reverse=True): # most recently added words are more important while determining user's level
lst = d_user2[date] # a list of words
lst2 = [] # a list of tuples, (word, difficulty level)
for word in lst:
if word in d:
lst2.append((word, d[word]))
lst3 = sort_in_ascending_order(lst2) # easiest tuple first
#print(lst3)
lst3 = sort_in_ascending_order(lst2) # easiest tuple first
# print(lst3)
for t in lst3:
word = t[0]
hard = t[1]
#print('WORD %s HARD %4.2f' % (word, hard))
# print('WORD %s HARD %4.2f' % (word, hard))
geometric = geometric * (hard)
count += 1
if count >= 10:
return geometric**(1/count)
return geometric ** (1 / count)
return geometric**(1/max(count,1))
return geometric ** (1 / max(count, 1))
def text_difficulty_level(s, d):
s = remove_punctuation(s)
L = freq(s)
lst = [] # a list of tuples, each tuple being (word, difficulty level)
lst = [] # a list of tuples, each tuple being (word, difficulty level)
stop_words = {'the':1, 'and':1, 'of':1, 'to':1, 'what':1, 'in':1, 'there':1, 'when':1, 'them':1, 'would':1, 'will':1, 'out':1, 'his':1, 'mr':1, 'that':1, 'up':1, 'more':1, 'your':1, 'it':1, 'now':1, 'very':1, 'then':1, 'could':1, 'he':1, 'any':1, 'some':1, 'with':1, 'into':1, 'you':1, 'our':1, 'man':1, 'other':1, 'time':1, 'was':1, 'than':1, 'know':1, 'about':1, 'only':1, 'like':1, 'how':1, 'see':1, 'is':1, 'before':1, 'such':1, 'little':1, 'two':1, 'its':1, 'as':1, 'these':1, 'may':1, 'much':1, 'down':1, 'for':1, 'well':1, 'should':1, 'those':1, 'after':1, 'same':1, 'must':1, 'say':1, 'first':1, 'again':1, 'us':1, 'great':1, 'where':1, 'being':1, 'come':1, 'over':1, 'good':1, 'himself':1, 'am':1, 'never':1, 'on':1, 'old':1, 'here':1, 'way':1, 'at':1, 'go':1, 'upon':1, 'have':1, 'had':1, 'without':1, 'my':1, 'day':1, 'be':1, 'but':1, 'though':1, 'from':1, 'not':1, 'too':1, 'another':1, 'this':1, 'even':1, 'still':1, 'her':1, 'yet':1, 'under':1, 'by':1, 'let':1, 'just':1, 'all':1, 'because':1, 'we':1, 'always':1, 'off':1, 'yes':1, 'so':1, 'while':1, 'why':1, 'which':1, 'me':1, 'are':1, 'or':1, 'no':1, 'if':1, 'an':1, 'also':1, 'thus':1, 'who':1, 'cannot':1, 'she':1, 'whether':1} # ignore these words while computing the artile's difficulty level
for x in L:
word = x[0]
if word in d:
if word not in stop_words and word in d:
lst.append((word, d[word]))
lst2 = sort_in_descending_order(lst) # most difficult words on top
#print(lst2)
lst2 = sort_in_descending_order(lst) # most difficult words on top
# print(lst2)
count = 0
geometric = 1
for t in lst2:
@ -113,24 +133,20 @@ def text_difficulty_level(s, d):
hard = t[1]
geometric = geometric * (hard)
count += 1
if count >= 20: # we look for n most difficult words
return geometric**(1/count)
return geometric**(1/max(count,1))
if count >= 20: # we look for n most difficult words
return geometric ** (1 / count)
return geometric ** (1 / max(count, 1))
if __name__ == '__main__':
d1 = load_record('frequency.p')
#print(d1)
# print(d1)
d2 = load_record('words_and_tests.p')
#print(d2)
# print(d2)
d3 = get_difficulty_level(d1, d2)
d3 = get_difficulty_level_for_user(d1, d2)
s = '''
South Lawn
@ -191,7 +207,6 @@ Amidst the aftermath of this shocking referendum vote, there is great uncertaint
'''
s = '''
British Prime Minister Boris Johnson walks towards a voting station during the Brexit referendum in Britain, June 23, 2016. (Photo: EPA-EFE)
@ -212,7 +227,6 @@ The prime minister was forced to ask for an extension to Britain's EU departure
Johnson has repeatedly pledged to finalize the first stage, a transition deal, of Britain's EU divorce battle by Oct. 31. A second stage will involve negotiating its future relationship with the EU on trade, security and other salient issues.
'''
s = '''
Thank you very much. We have a Cabinet meeting. Well have a few questions after grace. And, if you would, Ben, please do the honors.
@ -227,17 +241,11 @@ 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')
s = f.read()
f.close()
print(text_difficulty_level(s, d3))

View File

@ -0,0 +1,21 @@
<!--
以html的形式插入到所有页面的页尾
-->
<!--
<br/><br/><br/>
<div class="aplayer-container">
<meting-js
theme='#1BCDFC'
autoplay='false'
volume='0.5'
loop='all'
order='list'
fixed='true'
list-max-height='160px'
server='netease'
type='playlist'
id='6927872292'
list-folded='true'>
</meting-js>
</div>
-->

View File

@ -0,0 +1,3 @@
<!--
以html的形式插入所有网页的head标签中
-->

View File

@ -1,496 +1,118 @@
#! /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 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
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, text_difficulty_level, user_difficulty_level
app = Flask(__name__)
app.secret_key = 'lunch.time!'
path_prefix = '/var/www/wordfreq/wordfreq/'
path_prefix = './' # comment this line in deployment
def get_random_image(path):
img_path = random.choice(glob.glob(os.path.join(path, '*.jpg')))
return img_path[img_path.rfind('/static'):]
def get_random_ads():
ads = random.choice(['个性化分析精准提升', '你的专有单词本', '智能捕捉阅读弱点,针对性提高你的阅读水平'])
return ads + '。 <a href="/signup">试试</a>吧!'
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)
def load_freq_history(path):
d = {}
if os.path.exists(path):
d = pickle_idea.load_record(path)
return d
def verify_user(username, password):
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
rq.instructions_with_parameters("SELECT * FROM user WHERE name=? AND password=?", (username, password))
rq.do_with_parameters()
result = rq.get_results()
return result != []
def add_user(username, password):
start_date = datetime.now().strftime('%Y%m%d')
expiry_date = '20211230'
rq = InsertQuery(path_prefix + 'static/wordfreqapp.db')
rq.instructions("INSERT INTO user Values ('%s', '%s', '%s', '%s')" % (username, password, start_date, expiry_date))
rq.do()
def check_username_availability(username):
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
rq.instructions("SELECT * FROM user WHERE name='%s'" % (username))
rq.do()
result = rq.get_results()
return result == []
def get_expiry_date(username):
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
rq.instructions("SELECT expiry_date FROM user WHERE name='%s'" % (username))
rq.do()
result = rq.get_results()
if len(result) > 0:
return result[0]['expiry_date']
else:
return '20191024'
def within_range(x, y, r):
return x > y and abs(x - y) <= r
def get_article_title(s):
return s.split('\n')[0]
def get_article_body(s):
lst = s.split('\n')
lst.pop(0) # remove the first line
return '\n'.join(lst)
def get_today_article(user_word_list, articleID):
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
if articleID == None:
rq.instructions("SELECT * FROM article")
else:
rq.instructions('SELECT * FROM article WHERE article_id=%d' % (articleID))
rq.do()
result = rq.get_results()
random.shuffle(result)
# Choose article according to reader's level
d1 = load_freq_history(path_prefix + 'static/frequency/frequency.p')
d2 = load_freq_history(path_prefix + 'static/words_and_tests.p')
d3 = get_difficulty_level(d1, d2)
d = {}
d_user = load_freq_history(user_word_list)
user_level = user_difficulty_level(d_user, d3) # more consideration as user's behaviour is dynamic. Time factor should be considered.
random.shuffle(result) # shuffle list
d = random.choice(result)
text_level = text_difficulty_level(d['text'], d3)
if articleID == None:
for reading in result:
text_level = text_difficulty_level(reading['text'], d3)
if within_range(text_level, user_level, (8.0 - user_level)*0.8):
d = reading
break
s = '<div class="alert alert-success" role="alert">According to your word list, your level is <span class="badge bg-success">%4.2f</span> and we have chosen an article with a difficulty level of <span class="badge bg-success">%4.2f</span> for you.</div>' % (user_level, text_level)
s += '<p class="text-muted">Article added on: %s</p>' % (d['date'])
s += '<div class="p-3 mb-2 bg-light text-dark">'
article_title = get_article_title(d['text'])
article_body = get_article_body(d['text'])
s += '<p class="display-3">%s</p>' % (article_title)
s += '<p class="lead">%s</p>' % (article_body)
s += '<p><small class="text-muted">%s</small></p>' % (d['source'])
s += '<p><b>%s</b></p>' % (get_question_part(d['question']))
s = s.replace('\n', '<br/>')
s += '%s' % (get_answer_part(d['question']))
s += '</div>'
session['articleID'] = d['article_id']
return s
def appears_in_test(word, d):
if not word in d:
return ''
else:
return ','.join(d[word])
def get_time():
return datetime.now().strftime('%Y%m%d%H%M') # upper to minutes
def get_question_part(s):
s = s.strip()
result = []
flag = 0
for line in s.split('\n'):
line = line.strip()
if line == 'QUESTION':
result.append(line)
flag = 1
elif line == 'ANSWER':
flag = 0
elif flag == 1:
result.append(line)
return '\n'.join(result)
def get_answer_part(s):
s = s.strip()
result = []
flag = 0
for line in s.split('\n'):
line = line.strip()
if line == 'ANSWER':
flag = 1
elif flag == 1:
result.append(line)
# https://css-tricks.com/snippets/javascript/showhide-element/
js = '''
<script type="text/javascript">
function toggle_visibility(id) {
var e = document.getElementById(id);
if(e.style.display == 'block')
e.style.display = 'none';
else
e.style.display = 'block';
}
</script>
'''
html_code = js
html_code += '\n'
html_code += '<button onclick="toggle_visibility(\'answer\');">ANSWER</button>\n'
html_code += '<div id="answer" style="display:none;">%s</div>\n' % ('\n'.join(result))
return html_code
def get_flashed_messages_if_any():
messages = get_flashed_messages()
s = ''
for message in messages:
s += '<div class="alert alert-warning" role="alert">'
s += f'Congratulations! {message}'
s += '</div>'
return s
@app.route("/<username>/reset", methods=['GET', 'POST'])
def user_reset(username):
if request.method == 'GET':
session['articleID'] = None
return redirect(url_for('userpage', username=username))
else:
return 'Under construction'
@app.route("/mark", methods=['GET', 'POST'])
def mark_word():
if request.method == 'POST':
d = load_freq_history(path_prefix + 'static/frequency/frequency.p')
lst_history = pickle_idea.dict2lst(d)
lst = []
for word in request.form.getlist('marked'):
lst.append((word, 1))
d = pickle_idea.merge_frequency(lst, lst_history)
pickle_idea.save_frequency_to_pickle(d, path_prefix + 'static/frequency/frequency.p')
return redirect(url_for('mainpage'))
else:
return 'Under construction'
@app.route("/", methods=['GET', 'POST'])
def mainpage():
if request.method == 'POST': # when we submit a form
content = request.form['content']
f = WordFreq(content)
lst = f.get_freq()
page = '<form method="post" action="/mark">\n'
count = 1
for x in lst:
page += '<p><font color="grey">%d</font>: <a href="%s">%s</a> (%d) <input type="checkbox" name="marked" value="%s"></p>\n' % (count, youdao_link(x[0]), x[0], x[1], x[0])
count += 1
page += ' <input type="submit" value="确定并返回"/>\n'
page += '</form>\n'
# save history
d = load_freq_history(path_prefix + 'static/frequency/frequency.p')
lst_history = pickle_idea.dict2lst(d)
d = pickle_idea.merge_frequency(lst, lst_history)
pickle_idea.save_frequency_to_pickle(d, path_prefix + 'static/frequency/frequency.p')
return page
elif request.method == 'GET': # when we load a html page
page = '''
<html lang="zh">
<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" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>EnglishPal 英文单词高效记</title>
</head>
<body>
'''
page += '<div class="container-fluid">'
page += '<p><b><font size="+3" color="red">English Pal - Learn English smartly!</font></b></p>'
if session.get('logged_in'):
page += ' <a href="%s">%s</a></p>\n' % (session['username'], session['username'])
else:
page += '<p><a href="/login">登录</a> <a href="/signup">成为会员</a> <a href="/static/usr/instructions.html">使用说明</a></p>\n'
#page += '<p><img src="%s" width="400px" alt="advertisement"/></p>' % (get_random_image(path_prefix + 'static/img/'))
page += '<p><b>%s</b></p>' % (get_random_ads())
page += '<div class="alert alert-success" role="alert">共有文章 <span class="badge bg-success"> %d </span> 篇</div>' % (total_number_of_essays())
page += '<p>粘帖1篇文章 (English only)</p>'
page += '<form method="post" action="/">'
page += ' <textarea name="content" rows="10" cols="120"></textarea><br/>'
page += ' <input type="submit" value="get文章中的词频"/>'
page += ' <input type="reset" value="清除"/>'
page += '</form>'
d = load_freq_history(path_prefix + 'static/frequency/frequency.p')
if len(d) > 0:
page += '<p><b>最常见的词</b></p>'
for x in sort_in_descending_order(pickle_idea.dict2lst(d)):
if x[1] <= 99:
break
page += '<a href="%s">%s</a> %d\n' % (youdao_link(x[0]), x[0], x[1])
page += ' <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>'
page += '</div>'
page += '</body></html>'
return page
@app.route("/<username>/mark", methods=['GET', 'POST'])
def user_mark_word(username):
username = session[username]
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
if request.method == 'POST':
d = load_freq_history(user_freq_record)
lst_history = pickle_idea2.dict2lst(d)
lst = []
for word in request.form.getlist('marked'):
lst.append((word, [get_time()]))
d = pickle_idea2.merge_frequency(lst, lst_history)
pickle_idea2.save_frequency_to_pickle(d, user_freq_record)
return redirect(url_for('userpage', username=username))
else:
return 'Under construction'
@app.route("/<username>/<word>/unfamiliar", methods=['GET', 'POST'])
def unfamiliar(username,word):
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
pickle_idea.unfamiliar(user_freq_record,word)
session['thisWord'] = word # 1. put a word into session
session['time'] = 1
return redirect(url_for('userpage', username=username))
@app.route("/<username>/<word>/familiar", methods=['GET', 'POST'])
def familiar(username,word):
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
pickle_idea.familiar(user_freq_record,word)
session['thisWord'] = word # 1. put a word into session
session['time'] = 1
return redirect(url_for('userpage', username=username))
@app.route("/<username>/<word>/del", methods=['GET', 'POST'])
def deleteword(username,word):
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
pickle_idea2.deleteRecord(user_freq_record,word)
flash(f'<strong>{word}</strong> is no longer in your word list.')
return redirect(url_for('userpage', username=username))
@app.route("/<username>", methods=['GET', 'POST'])
def userpage(username):
if not session.get('logged_in'):
return '<p>请先<a href="/login">登录</a>。</p>'
user_expiry_date = session.get('expiry_date')
if datetime.now().strftime('%Y%m%d') > user_expiry_date:
return '<p>账号 %s 过期。</p><p>为了提高服务质量English Pal 收取会员费用, 每天0元。</p> <p>请决定你要试用的时间长度,扫描下面支付宝二维码支付。 支付时请注明<i>English Pal Membership Fee</i>。 我们会于12小时内激活账号。</p><p><img src="static/donate-the-author-hidden.jpg" width="120px" alt="支付宝二维码" /></p><p>如果有问题,请加开发者微信 torontohui。</p> <p><a href="/logout">登出</a></p>' % (username)
username = session.get('username')
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
if request.method == 'POST': # when we submit a form
content = request.form['content']
f = WordFreq(content)
lst = f.get_freq()
page = '<meta charset="UTF8">'
page += '<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />'
page += '<p>勾选不认识的单词</p>'
page += '<form method="post" action="/%s/mark">\n' % (username)
page += ' <input type="submit" name="add-btn" value="加入我的生词簿"/>\n'
count = 1
words_tests_dict = pickle_idea.load_record(path_prefix + 'static/words_and_tests.p')
for x in lst:
page += '<p><font color="grey">%d</font>: <a href="%s" title="%s">%s</a> (%d) <input type="checkbox" name="marked" value="%s"></p>\n' % (count, youdao_link(x[0]), appears_in_test(x[0], words_tests_dict), x[0], x[1], x[0])
count += 1
page += '</form>\n'
return page
elif request.method == 'GET': # when we load a html page
page = '<meta charset="UTF8">\n'
page += '<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />\n'
page += '<meta name="format-detection" content="telephone=no" />\n' # forbid treating numbers as cell numbers in smart phones
page += '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">'
page += '<title>EnglishPal Study Room for %s</title>' % (username)
page += '<div class="container-fluid">'
page += '<p><b>English Pal for <font color="red">%s</font></b> <a class="btn btn-secondary" href="/logout" role="button">登出</a></p>' % (username)
page += get_flashed_messages_if_any()
page += '<p><b>阅读文章并回答问题</b></p>\n'
page += '<p><a class="btn btn-success" href="/%s/reset" role="button"> 下一篇 Next Article </a></p>' % (username)
page += '<div id="text-content">%s</div>' % (get_today_article(user_freq_record, session['articleID']))
page += '<p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p>'
page += '<form method="post" action="/%s">' % (username)
page += ' <textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/>'
page += ' <input type="submit" value="get 所有词的频率"/>'
page += ' <input type="reset" value="清除"/>'
page += '</form>\n'
page += '''
<script>
function getWord(){
var word = window.getSelection?window.getSelection():document.selection.createRange().text;
return word;
}
function fillinWord(){
var element = document.getElementById("selected-words");
element.value = element.value + " " + getWord();
}
document.getElementById("text-content").addEventListener("click", fillinWord, false);
document.getElementById("text-content").addEventListener("touchstart", fillinWord, false);
</script>
'''
if session.get('thisWord'):
page += '''
<script type="text/javascript">
//point to the anchor in the page whose id is aaa if it exists
window.onload = function(){
var element = document.getElementsByName("aaa");
if (element != null)
document.getElementsByName("aaa")[0].scrollIntoView(true);
}
</script>
'''
d = load_freq_history(user_freq_record)
if len(d) > 0:
page += '<p><b>我的生词簿</b></p>'
lst = pickle_idea2.dict2lst(d)
lst2 = []
for t in lst:
lst2.append((t[0], len(t[1])))
for x in sort_in_descending_order(lst2):
word = x[0]
freq = x[1]
if session.get('thisWord') == x[0] and session.get('time') == 1:
page += '<a name="aaa"></a>' # 3. anchor
session['time'] = 0 # discard anchor
if isinstance(d[word], list): # d[word] is a list of dates
if freq > 1:
page += '<p class="new-word"> <a class="btn btn-light" href="%s" role="button">%s</a>(<a title="%s">%d</a>) <a class="btn btn-success" href="%s/%s/familiar" role="button">熟悉</a> <a class="btn btn-warning" href="%s/%s/unfamiliar" role="button">不熟悉</a> <a class="btn btn-danger" href="%s/%s/del" role="button">删除</a> </p>\n' % (youdao_link(word), word, '; '.join(d[word]), freq,username, word,username,word, username,word)
else:
page += '<p class="new-word"> <a class="btn btn-light" href="%s" role="button">%s</a>(<a title="%s">%d</a>) <a class="btn btn-success" href="%s/%s/familiar" role="button">熟悉</a> <a class="btn btn-warning" href="%s/%s/unfamiliar" role="button">不熟悉</a> <a class="btn btn-danger" href="%s/%s/del" role="button">删除</a> </p>\n' % (youdao_link(word), word, '; '.join(d[word]), freq,username, word,username,word, username,word)
elif isinstance(d[word], int): # d[word] is a frequency. to migrate from old format.
page += '<a href="%s">%s</a>%d\n' % (youdao_link(word), word, freq)
page += '<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>'
page += '</div>'
return page
### Sign-up, login, logout ###
@app.route("/signup", methods=['GET', 'POST'])
def signup():
if request.method == 'GET':
return render_template('signup.html')
elif request.method == 'POST':
username = request.form['username']
password = request.form['password']
available = check_username_availability(username)
if not available:
flash('用户名 %s 已经被注册。' % (username))
return render_template('signup.html')
elif len(password.strip()) < 4:
return '密码过于简单。'
else:
add_user(username, password)
verified = verify_user(username, password)
if verified:
session['logged_in'] = True
session[username] = username
session['username'] = username
return '<p>恭喜,你已成功注册, 你的用户名是 <a href="%s">%s</a>。</p>\
<p><a href="/%s">开始使用</a> <a href="/">返回首页</a><p/>' % (username, username, username)
else:
return '用户名密码验证失败。'
@app.route("/login", methods=['GET', 'POST'])
def login():
if request.method == 'GET':
if not session.get('logged_in'):
return render_template('login.html')
else:
return '你已登录 <a href="/%s">%s</a>。 登出点击<a href="/logout">这里</a>。' % (session['username'], session['username'])
elif request.method == 'POST':
# check database and verify user
username = request.form['username']
password = request.form['password']
verified = verify_user(username, password)
if verified:
session['logged_in'] = True
session[username] = username
session['username'] = username
user_expiry_date = get_expiry_date(username)
session['expiry_date'] = user_expiry_date
session['articleID'] = None
return redirect(url_for('userpage', username=username))
else:
return '无法通过验证。'
@app.route("/logout", methods=['GET', 'POST'])
def logout():
session['logged_in'] = False
return redirect(url_for('mainpage'))
if __name__ == '__main__':
#app.secret_key = os.urandom(16)
#app.run(debug=False, port='6000')
app.run(debug=True)
#app.run(debug=True, port='6000')
#app.run(host='0.0.0.0', debug=True, port='6000')
#! /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 Login import *
from Article import *
import Yaml
from user_service import userService
from account_service import accountService
from admin_service import adminService, ADMIN_NAME
app = Flask(__name__)
app.secret_key = 'lunch.time!'
# 将蓝图注册到Lab app
app.register_blueprint(userService)
app.register_blueprint(accountService)
app.register_blueprint(adminService)
path_prefix = '/var/www/wordfreq/wordfreq/'
path_prefix = './' # comment this line in deployment
def get_random_image(path):
'''
返回随机图
:param path: 图片文件(JPEG格式)不包含后缀名
:return:
'''
img_path = random.choice(glob.glob(os.path.join(path, '*.jpg')))
return img_path[img_path.rfind('/static'):]
def get_random_ads():
'''
返回随机广告
:return: 一个广告(包含HTML标签)
'''
return random.choice(['个性化分析精准提升', '你的专有单词本', '智能捕捉阅读弱点,针对性提高你的阅读水平'])
def appears_in_test(word, d):
'''
如果字符串里没有指定的单词则返回逗号加单词
:param word: 指定单词
:param d: 字符串
:return: 逗号加单词
'''
if not word in d:
return ''
else:
return ','.join(d[word])
@app.route("/mark", methods=['GET', 'POST'])
def mark_word():
'''
标记单词
:return: 重定位到主界面
'''
if request.method == 'POST':
d = load_freq_history(path_prefix + 'static/frequency/frequency.p')
lst_history = pickle_idea.dict2lst(d)
lst = []
for word in request.form.getlist('marked'):
lst.append((word, 1))
d = pickle_idea.merge_frequency(lst, lst_history)
pickle_idea.save_frequency_to_pickle(d, path_prefix + 'static/frequency/frequency.p')
return redirect(url_for('mainpage'))
else: # 不回应GET请求
return 'Under construction'
@app.route("/", methods=['GET', 'POST'])
def mainpage():
'''
根据GET或POST方法来返回不同的主界面
:return: 主界面
'''
if request.method == 'POST': # when we submit a form
content = escape(request.form['content'])
f = WordFreq(content)
lst = f.get_freq()
# save history
d = load_freq_history(path_prefix + 'static/frequency/frequency.p')
lst_history = pickle_idea.dict2lst(d)
d = pickle_idea.merge_frequency(lst, lst_history)
pickle_idea.save_frequency_to_pickle(d, path_prefix + 'static/frequency/frequency.p')
return render_template('mainpage_post.html', lst=lst, yml=Yaml.yml)
elif request.method == 'GET': # when we load a html page
random_ads = get_random_ads()
number_of_essays = total_number_of_essays()
d = load_freq_history(path_prefix + 'static/frequency/frequency.p')
d_len = len(d)
lst = sort_in_descending_order(pickle_idea.dict2lst(d))
return render_template('mainpage_get.html',
admin_name=ADMIN_NAME,
random_ads=random_ads,
d_len=d_len,
lst=lst,
yml=Yaml.yml,
number_of_essays=number_of_essays)
if __name__ == '__main__':
'''
运行程序
'''
# app.secret_key = os.urandom(16)
# app.run(debug=False, port='6000')
app.run(debug=True)
# app.run(debug=True, port='6000')
# app.run(host='0.0.0.0', debug=True, port='6000')
# print(mod5('123'))

30
app/model/__init__.py Normal file
View File

@ -0,0 +1,30 @@
from pony.orm import *
db = Database()
db.bind("sqlite", "../static/wordfreqapp.db", create_db=True) # bind sqlite file
class User(db.Entity):
_table_ = "user" # table name
name = PrimaryKey(str)
password = Optional(str)
start_date = Optional(str)
expiry_date = Optional(str)
class Article(db.Entity):
_table_ = "article" # table name
article_id = PrimaryKey(int, auto=True)
text = Optional(str)
source = Optional(str)
date = Optional(str)
level = Optional(str)
question = Optional(str)
db.generate_mapping(create_tables=True) # must mapping after class declaration
if __name__ == "__main__":
with db_session:
print(Article[2].text) # test get article which id=2 text content

34
app/model/article.py Normal file
View File

@ -0,0 +1,34 @@
from model import *
from datetime import datetime
def add_article(content, source="manual_input", level="5", question="No question"):
with db_session:
# add one article to sqlite
Article(
text=content,
source=source,
date=datetime.now().strftime("%-d %b %Y"), # format style of `5 Oct 2022`
level=level,
question=question,
)
def delete_article_by_id(article_id):
article_id &= 0xFFFFFFFF # max 32 bits
with db_session:
article = Article.select(article_id=article_id)
if article:
article.first().delete()
def get_number_of_articles():
with db_session:
return len(Article.select()[:])
def get_page_articles(num, size):
with db_session:
return [
x
for x in Article.select().order_by(desc(Article.article_id)).page(num, size)
]

30
app/model/user.py Normal file
View File

@ -0,0 +1,30 @@
from model import *
from Login import md5
from pony import orm
def get_users():
with db_session:
return User.select().order_by(User.name)[:]
def get_user_by_username(username):
with db_session:
user = User.select(name=username)
if user:
return user.first()
def insert_user(username, password, start_date, expiry_date):
with db_session:
user = User(name=username, password=password, start_date=start_date, expiry_date=expiry_date)
orm.commit()
def update_password_by_username(username, password="123456"):
with db_session:
user = User.select(name=username)
if user:
user.first().password = md5(username + password)
def update_expiry_time_by_username(username, expiry_time="20230323"):
with db_session:
user = User.select(name=username)
if user:
user.first().expiry_date = expiry_time

View File

@ -68,7 +68,7 @@ def save_frequency_to_pickle(d, pickle_fname):
d2 = {}
for k in d:
if not k in exclusion_lst and not k.isnumeric() and not len(k) < 2:
d2[k] = list(sorted(set(d[k])))
d2[k] = list(sorted(d[k])) # 原先这里是d2[k] = list(sorted(set(d[k])))
pickle.dump(d2, f)
f.close()

18
app/static/config.yml Normal file
View File

@ -0,0 +1,18 @@
# 全局引入的css文件地址
css:
item:
- ../static/css/bootstrap.css
# 全局引入的js文件地址
js:
head: # 在页面加载之前加载
- ../static/js/jquery.js
- ../static/js/read.js
- ../static/js/word_operation.js
bottom: # 在页面加载完之后加载
- ../static/js/fillword.js
- ../static/js/highlight.js
# 高亮样式,目前仅支持修改颜色
highlight:
color: ff0000

11192
app/static/css/bootstrap.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
/*样式应用于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

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

31
app/static/js/fillword.js Normal file
View File

@ -0,0 +1,31 @@
let isRead = true;
let isChoose = true;
function getWord() {
return window.getSelection ? window.getSelection() : document.selection.createRange().text;
}
function fillInWord() {
let word = getWord();
if (isRead) Reader.read(word, inputSlider.value);
if (!isChoose) return;
const element = document.getElementById("selected-words");
element.value = element.value + " " + word;
}
document.getElementById("text-content").addEventListener("click", fillInWord, false);
const sliderValue = document.getElementById("rangeValue");
const inputSlider = document.getElementById("rangeComponent");
inputSlider.oninput = () => {
let value = inputSlider.value;
sliderValue.textContent = value + '×';
};
function onReadClick() {
isRead = !isRead;
}
function onChooseClick() {
isChoose = !isChoose;
}

102
app/static/js/highlight.js Normal file
View File

@ -0,0 +1,102 @@
let isHighlight = true;
function cancelBtnHandler() {
cancelHighlighting();
document.getElementById("text-content").removeEventListener("click", fillInWord, false);
document.getElementById("text-content").removeEventListener("touchstart", fillInWord, false);
document.getElementById("text-content").addEventListener("click", fillInWord2, false);
document.getElementById("text-content").addEventListener("touchstart", fillInWord2, false);
}
function showBtnHandler() {
document.getElementById("text-content").removeEventListener("click", fillInWord2, false);
document.getElementById("text-content").removeEventListener("touchstart", fillInWord2, false);
document.getElementById("text-content").addEventListener("click", fillInWord, false);
document.getElementById("text-content").addEventListener("touchstart", fillInWord, false);
highLight();
}
function getWord() {
return window.getSelection ? window.getSelection() : document.selection.createRange().text;
}
function highLight() {
if (!isHighlight) return;
let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml使用innerText会把原文章中所包含的<br>标签去除,导致处理后的文章内容失去了原来的格式
let pickedWords = document.getElementById("selected-words"); // words picked to the text area
let dictionaryWords = document.getElementById("selected-words2"); // words appearing in the user's new words list
let allWords = ""; //初始化allWords的值避免进入判断后编译器认为allWords未初始化的问题
if(dictionaryWords != null){//增加一个判断检查生词本里面是否为空如果为空allWords只添加选中的单词
allWords = pickedWords.value + " " + dictionaryWords.value;
}
else{
allWords = pickedWords.value + " ";
}
const list = allWords.split(" ");//将所有的生词放入一个list中用于后续处理
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>");
}
}
}
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]);
}
}
}
document.getElementById("article").innerHTML = articleContent;
}
function fillInWord() {
highLight();
}
function fillInWord2() {
cancelHighlighting();
}
function toggleHighlighting() {
if (isHighlight) {
isHighlight = false;
cancelHighlighting();
} else {
isHighlight = true;
highLight();
}
}
showBtnHandler();

2
app/static/js/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

35
app/static/js/read.js Normal file
View File

@ -0,0 +1,35 @@
var Reader = (function() {
let reader = window.speechSynthesis;
let current_position = 0;
let original_position = 0;
let to_speak = "";
function makeUtterance(str, rate) {
let msg = new SpeechSynthesisUtterance(str);
msg.rate = rate;
msg.lang = "en-US";
msg.onboundary = ev => {
if (ev.name == "word") {
current_position = ev.charIndex;
}
}
return msg;
}
function read(s, rate) {
to_speak = s.toString();
original_position = 0;
current_position = 0;
let msg = makeUtterance(to_speak, rate);
reader.speak(msg);
}
function stopRead() {
reader.cancel();
}
return {
read: read,
stopRead: stopRead
};
})();

View File

@ -0,0 +1,181 @@
function familiar(theWord) {
let username = $("#username").text();
let word = $("#word_" + theWord).text();
let freq = $("#freq_" + theWord).text();
$.ajax({
type:"GET",
url:"/" + username + "/" + word + "/familiar",
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 });
}
} else {
if(new_freq <1) {
$("#p_" + theWord).remove();
} else {
$("#freq_" + theWord).text(new_freq);
}
}
}
});
}
function unfamiliar(theWord) {
let username = $("#username").text();
let word = $("#word_" + theWord).text();
let freq = $("#freq_" + theWord).text();
$.ajax({
type:"GET",
url:"/" + username + "/" + word + "/unfamiliar",
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 });
} else {
$("#freq_" + theWord).text(new_freq);
}
}
});
}
function delete_word(theWord) {
let username = $("#username").text();
let word = theWord.replace('&amp;', '&');
$.ajax({
type:"GET",
url:"/" + username + "/" + word + "/del",
success:function(response){
const allow_move = document.getElementById("move_dynamiclly").checked;
if (allow_move) {
removeWord(theWord);
} else {
$("#p_" + theWord).remove();
}
}
});
}
function read_word(theWord) {
let to_speak = $("#word_" + theWord).text();
original_position = 0;
current_position = 0;
Reader.read(to_speak, inputSlider.value);
}
/*
* 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>
<a class="btn btn-info" onclick="read_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

@ -1,86 +1,86 @@
<!DOCTYPE html>
<html lang="zh"><head>
<meta http-equiv="content-type" content="text/html; 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 charset="utf-8">
<title>怎么用English Pal</title>
<style>
li { margin: 3px 0; }
ul {list-style-type: none;}
body {font-size: 100%;}
* {font-family:SimSun !important;}
/* http://webtricksandtreats.com/table-style-css/ */
table.table-style-one {
font-family: verdana,arial,sans-serif;
font-size:14px;
color:#333333;
border-width: 1px;
border-color: #3A3A3A;
border-collapse: collapse;
}
table.table-style-one th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #3A3A3A;
background-color: #F0F8FF;
}
table.table-style-one td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #3A3A3A;
background-color: #ffffff;
}
</style>
</head>
<body>
<p>本软件的宗旨是:珍惜你的时间, 提高你的获取英文信息的速度与准确度。 不管你是英语爱好者,备考的学生,还是英语老师,都能从 English Pal 中发现用处。</p>
<p>如果你教英语English Pal 可以帮你掌握题目词汇规律, 提高教学质量。</p>
<p>如果你学英语English Pal 可以帮你迅速提高词汇, 轻松应对各种考试。 </p>
<p>1秒内闪电查词告别字典。 私人定制的单词簿,永久相伴,记录奋斗岁月。 </p>
<p>现在就<a href="/signup">试试看</a>吧。 English Pal 期待你的捷报, English Pal 期待你的<a href="./20161113-christ-piece-cambridge.jpg">远航</a></p>
<h2>使用方法</h2>
<p>在EnglishPal主页点击<a href="http://118.25.96.118:90/signup">成为会员</a>链接。支付会员费后即可开始使用。</p>
<p>活跃会员还有机会获得英文阅读10分钟一对一指导。</p>
<table class="table-style-one">
<thead>
<tr>
<th>截图</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="./EnglishPal-screenshot-01.jpg"><img src="./EnglishPal-screenshot-01.jpg" width="300px"/></a></td>
<td>精选短文,让你窥见世界。 统计词频,让你掌握规律。</td>
</tr>
<tr>
<td><a href="./EnglishPal-screenshot-02.jpg"><img src="./EnglishPal-screenshot-02.jpg" width="300px"/></a></td>
<td>时间无价,个性化的生词簿,为你节省记背单词时间。</td>
</tr>
<tr>
<td><a href="./EnglishPal-screenshot-03.jpg"><img src="./EnglishPal-screenshot-03.jpg" width="300px"/></a></td>
<td>考试人人都怕,单词的考试分类,让你目的明确。</td>
</tr>
</tbody>
</table>
</body>
</html>
<!DOCTYPE html>
<html lang="zh"><head>
<meta http-equiv="content-type" content="text/html; 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 charset="utf-8">
<title>怎么用English Pal</title>
<style>
li { margin: 3px 0; }
ul {list-style-type: none;}
body {font-size: 100%;}
* {font-family:SimSun !important;}
/* http://webtricksandtreats.com/table-style-css/ */
table.table-style-one {
font-family: verdana,arial,sans-serif;
font-size:14px;
color:#333333;
border-width: 1px;
border-color: #3A3A3A;
border-collapse: collapse;
}
table.table-style-one th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #3A3A3A;
background-color: #F0F8FF;
}
table.table-style-one td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #3A3A3A;
background-color: #ffffff;
}
</style>
</head>
<body>
<p>本软件的宗旨是:珍惜你的时间, 提高你的获取英文信息的速度与准确度。 不管你是英语爱好者,备考的学生,还是英语老师,都能从 English Pal 中发现用处。</p>
<p>如果你教英语English Pal 可以帮你掌握题目词汇规律, 提高教学质量。</p>
<p>如果你学英语English Pal 可以帮你迅速提高词汇, 轻松应对各种考试。 </p>
<p>1秒内闪电查词告别字典。 私人定制的单词簿,永久相伴,记录奋斗岁月。 </p>
<p>现在就<a href="/signup">试试看</a>吧。 English Pal 期待你的捷报, English Pal 期待你的<a href="./20161113-christ-piece-cambridge.jpg">远航</a></p>
<h2>使用方法</h2>
<p>在EnglishPal主页点击<a href="http://118.25.96.118:90/signup">成为会员</a>链接。支付会员费后即可开始使用。</p>
<p>活跃会员还有机会获得英文阅读10分钟一对一指导。</p>
<table class="table-style-one">
<thead>
<tr>
<th>截图</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="./EnglishPal-screenshot-01.jpg"><img src="./EnglishPal-screenshot-01.jpg" width="300px"/></a></td>
<td>精选短文,让你窥见世界。 统计词频,让你掌握规律。</td>
</tr>
<tr>
<td><a href="./EnglishPal-screenshot-02.jpg"><img src="./EnglishPal-screenshot-02.jpg" width="300px"/></a></td>
<td>时间无价,个性化的生词簿,为你节省记背单词时间。</td>
</tr>
<tr>
<td><a href="./EnglishPal-screenshot-03.jpg"><img src="./EnglishPal-screenshot-03.jpg" width="300px"/></a></td>
<td>考试人人都怕,单词的考试分类,让你目的明确。</td>
</tr>
</tbody>
</table>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<meta name="format-detection" content="telephone=no" />
{{ yml['header'] | safe }}
{% if yml['css']['item'] %}
{% for css in yml['css']['item'] %}
<link href="{{ css }}" rel="stylesheet">
{% endfor %}
{% endif %}
{% if yml['js']['head'] %}
{% for js in yml['js']['head'] %}
<script src="{{ js }}"></script>
{% endfor %}
{% endif %}
</head>
<body class="container" style="width: 800px; margin: auto; margin-top:24px;">
<nav class="navbar navbar-expand-lg bg-light">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/{{ username }}/userpage">返回 {{ username }}</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="card" style="margin-top:24px;">
<div class="card-header">
请选择您需要的操作
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<div class="d-grid gap-2">
<a href="/admin/article" class="btn btn-outline-primary" type="button">管理文章</a>
<a href="/admin/user" class="btn btn-outline-primary" type="button">管理用户</a>
</div>
</li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<meta name="format-detection" content="telephone=no" />
<link href="../static/css/bootstrap.css" rel="stylesheet">
</head>
<body class="container" style="width: 800px; margin: auto; margin-top:24px;">
<nav class="navbar navbar-expand-lg bg-light">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/admin">前一页</a>
</li>
</ul>
</div>
</div>
</nav>
{% for message in get_flashed_messages() %}
<div class="alert alert-success" role="alert">
{{ message }}
</div>
{% endfor %}
<div class="card" style="margin-top:24px;">
{% if tips %}
<div class="alert alert-success" role="alert">
{{ tips }}
</div>
{% endif %}
<div class="card-content">
<h5 style="margin-top: 10px;padding-left: 10px;">录入文章</h5>
<form action="" method="post" class="container mb-3">
<div class="mb-3">
<label class="form-label">文章内容</label>
<textarea id="content" name="content" class="form-control" rows="8" placeholder="首行是标题,后面是正文。"></textarea>
<label class="form-label">文章来源</label>
<textarea id="source" name="source" class="form-control" placeholder="推荐格式Source: HTTP 链接。"></textarea>
<label class="form-label">文章等级</label>
<select id="level" class="form-select" name="level">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option selected value="4">4</option>
</select>
<label class="form-label">文章问题</label>
<textarea id="question" name="question" class="form-control" rows="6" placeholder="格式:&#x0a; QUESTION&#x0a; What?&#x0a;&#x0a; ANSWER&#x0a; Apple. "></textarea>
</div>
<input type="submit" value="保存" class="btn btn-outline-primary">
</form>
</div>
</div>
<div class="card" style="margin-top:24px;">
<h5 style="margin-top: 10px;padding-left: 10px;">文章列表</h5>
<div class="list-group">
{% for text in text_list %}
<div class="list-group-item list-group-item-action" aria-current="true">
<div>
<a type="button" href="/admin/article?delete_id={{text.article_id}}" class="btn btn-outline-danger btn-sm">删除</a>
</div>
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ text.title }}</h5>
</div>
<div><small>{{ text.source }}</small></div>
<div class="d-flex w-100 justify-content-between">
<small>Level: {{text.level }}</small>
<small>Date: {{ text.date }}</small>
</div>
{{ text.content | safe }}
</div>
{% endfor %}
</div>
</div>
<div style="margin:20px 0;">
<ul class="pagination pagination-sm justify-content-center">
<li class="page-item"><a class="page-link" href="/admin/article?page={{ cur_page - 1 }}&size={{ page_size }}">Previous</a>
</li>
{% for i in range(1, article_number // page_size + (article_number % page_size > 0) + 1) %}
{% if cur_page == i %}
<li class="page-item active"><a class="page-link" href="/admin/article?page={{ i }}&size={{ page_size }}">{{ i }}</a>
</li>
{% else %}
<li class="page-item"><a class="page-link" href="/admin/article?page={{ i }}&size={{ page_size }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
<li class="page-item"><a class="page-link" href="/admin/article?page={{ cur_page + 1 }}&size={{ page_size }}">Next</a>
</li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<meta name="format-detection" content="telephone=no" />
<link href="../static/css/bootstrap.css" rel="stylesheet">
<script src="../static/js/jquery.js"></script>
</head>
<body class="container" style="width: 800px; margin: auto; margin-top:24px;">
<nav class="navbar navbar-expand-lg bg-light">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/admin">前一页</a>
</li>
</ul>
</div>
</div>
</nav>
{% for message in get_flashed_messages() %}
<div class="alert alert-success" role="alert">
{{ message }}
</div>
{% endfor %}
<div class="card" style="margin-top:24px;">
<h5 style="margin-top: 10px;padding-left: 10px;">重置选中用户的信息</h5>
<form id="user_form" action="" method="post" class="container mb-3">
<div>
<label class="form-label" style="padding-top: 10px;">用户</label>
<select onchange="loadUserExpiryDate()" id="username" name="username" class="form-select" aria-label="Default select example">
<option selected>选择用户</option>
{% for user in user_list %}
<option value="{{ user.name }}">{{ user.name }}</option>
{% endfor %}
</select>
<label class="form-label" style="padding-top: 10px;">修改密码</label>
<div>
<button type="button" id="reset_pwd_btn" class="btn btn-outline-success">获取12位随机密码</button>
<input style="margin-left: 20px;border: 0; font-size: 20px;" name="new_password"
id="new_password"></input>
</div>
<label class="form-label" style="padding-top: 10px;">过期时间</label>
<div>
<input type="date" id="expiry_date" name="expiry_time" placeholder="YYYY-MM-DD" pattern="yyyyMMdd">
</div>
</div>
<button style="margin-top: 50px;" type="submit" class="btn btn-primary">更新用户信息</button>
</form>
</div>
</body>
<script>
// 密码生成器
function generatePassword(length) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^*()_+~`|}{[]\:;?,./-=";
let password = "";
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
document.getElementById("reset_pwd_btn").addEventListener("click", () => {
// 生成12位随机密码
let pwd = generatePassword(12)
document.getElementById("new_password").value = pwd
})
// 选择用户后更新其过期时间
function loadUserExpiryDate() {
const cur_user = $('#username').val();
$.ajax({
type: "GET",
url: `/admin/expiry?username=${cur_user}`,
success: function(resp) {
const year = resp.substr(0,4);
const month = resp.substr(4,2);
const day = resp.substr(6,2);
document.getElementById("expiry_date").value = year + '-' + month + '-' + day
}
})
}
</script>
</html>

16
app/templates/expiry.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>账号过期</title>
</head>
<body>
<p>您的账号过期(过期日 {{expiry_date}})。</p>
<p>为了提高服务质量English Pal 收取会员费用, 每天1元。</p>
<p>请决定你要试用的时间长度,扫描下面支付宝二维码支付。 支付时请注明<i>English Pal Membership Fee</i>。 我们会于12小时内激活账号。</p>
<p><img src="static/donate-the-author-hidden.jpg" width="120px" alt="支付宝二维码" /></p>
<p>如果有问题,请联系开发者 dev@qq.com。</p>
<p><a href="/logout">退出</a></p>
</body>
</html>

View File

@ -1,15 +1,47 @@
{% block body %}
{% if session['logged_in'] %}
You're logged in already!
{% else %}
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<form action="/login" method="POST">
<p><input type="username" name="username" placeholder="邮箱地址、电话号码"></p>
<p><input type="password" name="password" placeholder="密码"></p>
<p><input type="submit" value="登录"></p>
</form>
{% endif %}
{% endblock %}
{% block body %}
{% if session['logged_in'] %}
你已登录 <a href="/{{ session['username'] }}">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>
{% else %}
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<link rel="stylesheet" href="static/css/login_service.css">
<script src="static/js/jquery.js"></script>
<script>
function login(){
let username = $("#username").val();
let password = $("#password").val();
if (username === "" || password === ""){
alert('输入不能为空!');
return false;
}
$.post(
"/login", {'username': username, 'password': password},
function (response) {
if (response.status === '0') {
alert('无法通过验证。');
window.location.href = "/login";
} else if (response.status === '1') {
window.location.href = "/"+username+"/userpage";
}
}
)
return false;
}
</script>
<div class="container">
<section class="signin-heading">
<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 %}
{% endblock %}

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="zh">
<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" />
{{ yml['header'] | safe }}
{% if yml['css']['item'] %}
{% for css in yml['css']['item'] %}
<link href="{{ css }}" rel="stylesheet">
{% endfor %}
{% endif %}
{% if yml['js']['head'] %}
{% for js in yml['js']['head'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
<title>EnglishPal 英文单词高效记</title>
</head>
<body>
<div class="container-fluid">
<p><b><font size="+3" color="red">English Pal - Learn English smartly!</font></b></p>
{% if session['logged_in'] %}
<a href="/{{ session['username'] }}/userpage">{{ session['username'] }}</a>
{% if session['username'] == admin_name %}
<a href="/admin">管理</a></p>
{% endif %}
{% else %}
<p><a href="/login">登录</a> <a href="/signup">注册</a> <a href="/static/usr/instructions.html">使用说明</a></p >
<p><b> {{ random_ads }}。 <a href="/signup">试试</a>吧!</b></p>
{% endif %}
<div class="alert alert-success" role="alert">共有文章 <span class="badge bg-success"> {{ number_of_essays }} </span></div>
<p>粘贴1篇文章 (English only)</p>
<form method="post" action="/">
<textarea name="content" rows="10" cols="120"></textarea><br/>
<input type="submit" value="get文章中的词频"/>
<input type="reset" value="清除"/>
</form>
{% if d_len > 0 %}
<p><b>最常见的词</b></p>
{% for x in lst if x[1]>99 %}
<a href="http://youdao.com/w/eng/{{x[0]}}/#keyfrom=dict2.index">{{x[0]}}</a> {{x[1]}}
{% endfor %}
{% endif %}
<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 }}
{% if yml['js']['bottom'] %}
{% for js in yml['js']['bottom'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
</body>
</html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
{{ yml['header'] | safe }}
{% if yml['css']['item'] %}
{% for css in yml['css']['item'] %}
<link href="{{ css }}" rel="stylesheet">
{% endfor %}
{% endif %}
{% if yml['js']['head'] %}
{% for js in yml['js']['head'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
</head>
<body>
<form method="post" action="/mark">
{% for x in lst %}
<p>
<font color="grey">{{loop.index}}</font>
:
<a href="http://youdao.com/w/eng/{{x[0]}}/#keyfrom=dict2.index">{{x[0]}}</a>
({{x[1]}})
<input type="checkbox" name="marked" value="{{x[0]}}">
</p>
{% endfor %}
<input type="submit" value="确定并返回"/>
</form>
{{ yml['footer'] | safe }}
{% if yml['js']['bottom'] %}
{% for js in yml['js']['bottom'] %}
<script src="{{ js }}" ></script>
{% endfor %}
{% endif %}
</body>
</html>

View File

@ -0,0 +1,5 @@
{% block body %}
{% if not session['logged_in'] %}
<p>请先<a href="/login">登录</a></p>
{% endif %}
{% endblock %}

51
app/templates/reset.html Normal file
View File

@ -0,0 +1,51 @@
{% block body %}
<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">
<script src="static/js/jquery.js"></script>
<script>
function reset() {
let old_password = $("#old-password").val();
let new_password = $("#new-password").val();
let re_new_password = $("#re-new-password").val();
if (old_password === "" || new_password === "" || re_new_password === ""){
alert('输入不能为空!');
return false;
}
if (new_password !== re_new_password) {
alert('新密码不匹配,请重新输入');
return false;
}
if (new_password.length < 4) {
alert('密码过于简单。(密码长度至少4位)');
return false;
}
$.post("/reset", {'old-password': old_password, 'new-password': new_password},
function (response) {
if (response.status === '1') {
alert('密码修改成功,请重新登录。');
window.location.href = "/login";
} else if (response.status === '2') {
alert('密码修改失败');
window.location.href = "/reset";
}
}
)
return false;
}
</script>
<div class="container">
<section class="reset-heading">
<h1>Reset Password</h1>
</section>
<input type="password" placeholder="原密码" class="old-password" name="old-password" id="old-password"/>
<input type="password" placeholder="新密码" class="new-password" name="new-password" id="new-password"/>
<input type="password" placeholder="确认新密码" class="re-new-password" name="re-new-password" id="re-new-password"/>
<button id="submit" class="btn" onclick="reset()">提交</button>
<button class="btn" onclick="window.location.href='/{{ username }}/userpage'">放弃修改</button>
</div>
{% endblock %}

View File

@ -1,19 +1,68 @@
{% block body %}
{% if session['logged_in'] %}
You're logged in already! <a href="/logout">Logout</a>.
{% else %}
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
<p>{{ get_flashed_messages()[0] | safe }}</p>
<p>Sign up here.</p>
<form action="/signup" method="POST">
<p><input type="username" name="username" placeholder="邮箱地址、电话号码"></p>
<p><input type="password" name="password" placeholder="密码"></p>
<p><input type="submit" value="注册"></p>
</form>
{% endif %}
{% endblock %}
{% block body %}
{% if session['logged_in'] %}
You're logged in already! <a href="/logout">Logout</a>.
{% else %}
<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">
<script src="static/js/jquery.js"></script>
<script>
function signup() {
let username = $("#username").val();
let password = $("#password").val();
let password2 = $("#password2").val();
if (username === "" || password === "" || password2 === ""){
alert('输入不能为空!');
return false;
}
if (password !== password2) {
alert('确认密码与输入密码不一致!');
return false;
}
if (password.length < 4) {
alert('密码过于简单。(密码长度至少4位)');
return false;
}
$.post("/signup", {'username': username, 'password': password},
function (response) {
if (response.status === '0') {
alert('用户名'+username+'已经被注册。');
window.location.href = "/signup";
} else if (response.status === '1') {
alert('用户名密码验证失败。');
window.location.href = "/signup";
} else if (response.status === '2') {
let f = confirm("恭喜,你已成功注册,你的用户名是"+username+'.\n点击“确认”开始使用或点击“取消”返回首页');
if (f) {
window.location.href = '/'+username+'/userpage';
} else {
window.location.href = '/';
}
} else if (response.status === '3') {
alert(response.warn);
}
}
)
return false;
}
</script>
<p>{{ get_flashed_messages()[0] | safe }}</p>
<div class="container">
<section class="signin-heading">
<h1>Sign Up</h1>
</section>
<p><input type="username" id="username" placeholder="输入用户名" class="username"></p>
<p><input type="password" id="password" placeholder="输入密码" class="password"></p>
<p><input type="password" id="password2" placeholder="确认密码" class="password" ></p>
<button type="button" class="btn" onclick="signup()">注册</button>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,333 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes"/>
<meta name="format-detection" content="telephone=no"/>
<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'] %}
{% for css in yml['css']['item'] %}
<link href="{{ css }}" rel="stylesheet">
{% endfor %}
{% endif %}
{% if yml['js']['head'] %}
{% for js in yml['js']['head'] %}
<script src="{{ js }}"></script>
{% endfor %}
{% endif %}
<title>EnglishPal Study Room for {{ username }}</title>
<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);
}
}
.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>
<div class="container-fluid">
<p><b>English Pal for <font id="username" color="red">{{ username }}</font></b>
{% if username == admin_name %}
<a class="btn btn-secondary" href="/admin" role="button" onclick="stopRead()">管理</a>
{% endif %}
<a id="quit" class="btn btn-secondary" href="/logout" role="button" onclick="stopRead()">退出</a>
<a class="btn btn-secondary" href="/reset" role="button" onclick="stopRead()">重设密码</a>
</p>
{% for message in get_flashed_messages() %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
<button class="arrow" id="load_next_article" onclick="load_next_article();Reader.stopRead()"
title="下一篇 Next Article">⇨
</button>
<button class="arrow" id="load_pre_article" onclick="load_pre_article();Reader.stopRead()" style="display: none"
title="上一篇 Previous Article">⇦
</button>
<p><b>阅读文章并回答问题</b></p>
<div id="text-content">
<div id="found">
<div class="alert alert-success" role="alert">According to your word list, your level is <span
class="text-decoration-underline" id="user_level">{{ today_article["user_level"] }}</span> and we
have chosen an article with a difficulty level of <span class="text-decoration-underline"
id="text_level">{{ today_article["text_level"] }}</span>
for you.
</div>
<p class="text-muted" id="date">Article added on: {{ today_article["date"] }}</p><br/>
<div class="p-3 mb-2 bg-light text-dark" style="margin: 0 0.5%;"><br/>
<p class="display-6" id="article_title">{{ today_article["article_title"] }}</p><br/>
<p class="lead"><font id="article">{{ today_article["article_body"] }}</font></p><br/>
<div>
<p><small class="text-muted" id="source">{{ today_article['source'] }}</small></p><br/>
</div>
<p><b id="question">{{ today_article['question'] }}</b></p><br/>
<script type="text/javascript">
function toggle_visibility(id) { {# https://css-tricks.com/snippets/javascript/showhide-element/#}
const e = document.getElementById(id);
if (e.style.display === 'block')
e.style.display = 'none';
else
e.style.display = 'block';
}
</script>
<button onclick="toggle_visibility('answer');">ANSWER</button>
<div id="answer" style="display:none;">{{ today_article['answer'] }}</div>
<br/>
</div>
</div>
<div class="alert alert-success" role="alert" id="not_found" style="display:none;">
<p class="text-muted"><span class="badge bg-success">Notes:</span><br>No article is currently available for
you. You can try again a few times or mark new words in the passage to improve your level.</p>
</div>
<div class="alert alert-success" role="alert" id="read_all" style="display:none;">
<p class="text-muted"><span class="badge bg-success">Notes:</span><br>You've read all the articles.</p>
</div>
</div>
<input type="checkbox" id="highlightCheckbox" onclick="toggleHighlighting()"/>生词高亮
<input type="checkbox" id="readCheckbox" onclick="onReadClick()"/>大声朗读
<input type="checkbox" id="chooseCheckbox" onclick="onChooseClick()"/>划词入库
<div class="range">
<div class="field">
<div class="sliderValue">
<span id="rangeValue">1×</span>
</div>
<input type="range" id="rangeComponent" min="0.5" max="2" value="1" step="0.25"/>
</div>
</div>
<p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p>
<form method="post" action="/{{ username }}/userpage">
<textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/>
<button class="btn btn-primary btn-lg" type="submit" onclick="Reader.stopRead()">把生词加入我的生词库</button>
<button class="btn btn-primary btn-lg" type="reset" onclick="clearSelectedWords()">清除</button>
</form>
{% if session.get['thisWord'] %}
<script type="text/javascript">
//point to the anchor in the page whose id is aaa if it exists
window.onload = function () {
var element = document.getElementsByName("aaa");
if (element != null)
document.getElementsByName("aaa")[0].scrollIntoView(true);
}
</script>
{% endif %}
{% if d_len > 0 %}
<p>
<b>我的生词簿</b>
<label for="move_dynamiclly">
<input type="checkbox" name="move_dynamiclly" id="move_dynamiclly" checked>
允许动态调整顺序
</label>
</p>
<a name="aaa"></a>
<div class="word-container">
{% for x in lst3 %}
{% set word = x[0] %}
{% set freq = x[1] %}
{% if session.get('thisWord') == x[0] and session.get('time') == 1 %}
{% endif %}
<p id='p_{{ word }}' class="new-word">
<a id="word_{{ word }}" class="btn btn-light"
href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index'
role="button">{{ word }}</a>
( <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>
<a class="btn btn-info" onclick="read_word('{{ word }}')" role="button">朗读</a>
</p>
{% endfor %}
</div>
<input id="selected-words2" type="hidden" value="{{ words }}">
{% endif %}
</div>
{{ yml['footer'] | safe }}
{% if yml['js']['bottom'] %}
{% for js in yml['js']['bottom'] %}
<script src="{{ js }}"></script>
{% endfor %}
{% endif %}
<script type="text/javascript">
window.onload = function () { // 页面加载时执行
const settings = {
// initialize settings from localStorage
highlightChecked: localStorage.getItem('highlightChecked') !== 'false', // localStorage stores strings, default to true. same below
readChecked: localStorage.getItem('readChecked') !== 'false',
chooseChecked: localStorage.getItem('chooseChecked') !== 'false',
rangeValue: localStorage.getItem('rangeValue') || '1',
selectedWords: localStorage.getItem('selectedWords') || ''
};
const elements = {
highlightCheckbox: document.querySelector('#highlightCheckbox'),
readCheckbox: document.querySelector('#readCheckbox'),
chooseCheckbox: document.querySelector('#chooseCheckbox'),
rangeComponent: document.querySelector('#rangeComponent'),
rangeValueDisplay: document.querySelector('#rangeValue'),
selectedWordsInput: document.querySelector('#selected-words')
};
// 应用设置到页面元素
elements.highlightCheckbox.checked = settings.highlightChecked;
elements.readCheckbox.checked = settings.readChecked;
elements.chooseCheckbox.checked = settings.chooseChecked;
elements.rangeComponent.value = settings.rangeValue;
elements.rangeValueDisplay.textContent = `${settings.rangeValue}x`;
elements.selectedWordsInput.value = settings.selectedWords;
// 刷新页面或进入页面时判断,若不是首篇文章,则上一篇按钮可见
if (sessionStorage.getItem('pre_page_button') !== 'display' && sessionStorage.getItem('pre_page_button')) {
$('#load_pre_article').show();
}
// 事件监听器
elements.selectedWordsInput.addEventListener('input', () => {
localStorage.setItem('selectedWords', elements.selectedWordsInput.value);
});
elements.rangeComponent.addEventListener('input', () => {
const rangeValue = elements.rangeComponent.value;
elements.rangeValueDisplay.textContent = `${rangeValue}x`;
localStorage.setItem('rangeValue', rangeValue);
});
};
function clearSelectedWords() {
localStorage.removeItem('selectedWords');
document.querySelector('#selected-words').value = '';
}
function load_next_article() {
$("#load_next_article").prop("disabled", true)
$.ajax({
url: '/get_next_article/{{username}}',
dataType: 'json',
success: function (data) {
// 更新页面内容
if (data['today_article']) {
update(data['today_article']);
check_pre(data['visited_articles']);
check_next(data['result_of_generate_article']);
}
}, complete: function (xhr, status) {
$("#load_next_article").prop("disabled", false)
}
});
}
function load_pre_article() {
$.ajax({
url: '/get_pre_article/{{username}}',
dataType: 'json',
success: function (data) {
// 更新页面内容
if (data['today_article']) {
update(data['today_article']);
check_pre(data['visited_articles']);
}
}
});
}
function update(today_article) {
$('#user_level').html(today_article['user_level']);
$('#text_level').html(today_article["text_level"]);
$('#date').html('Article added on: ' + today_article["date"]);
$('#article_title').html(today_article["article_title"]);
$('#article').html(today_article["article_body"]);
$('#source').html(today_article['source']);
$('#question').html(today_article["question"]);
$('#answer').html(today_article["answer"]);
document.querySelector('#text_level').classList.add('mark'); // highlight text difficult level for 2 seconds
setTimeout(() => {
document.querySelector('#text_level').classList.remove('mark');
}, 2000);
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) {
if (result_of_generate_article == "found") {
$('#found').show();
$('#not_found').hide();
$('#read_all').hide();
} else if (result_of_generate_article == "not found") {
$('#found').hide();
$('#not_found').show();
$('#read_all').hide();
} else {
$('#found').hide();
$('#not_found').hide();
$('#read_all').show();
}
}
</script>
</body>
<style>
mark {
color: #{{ yml['highlight']['color'] }};
background-color: rgba(0, 0, 0, 0);
}
</style>
</html>

View File

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

13
app/test/conftest.py Normal file
View File

@ -0,0 +1,13 @@
import pytest
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
@pytest.fixture
def URL():
return 'http://127.0.0.1:5000' # URL of the program
@pytest.fixture
def driver():
my_driver = webdriver.Edge() # uncomment this line if you wish to run the test on your laptop
return my_driver

View File

@ -1,61 +1,61 @@
# -*- coding: utf-8 -*-
# Run the docker image using the following command:
# docker run -d -p 4444:4444 selenium/standalone-chrome
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import random, time
import string
# 调用本地chromedriver
# driver = webdriver.Chrome(executable_path="D:\ChromeDriver\chromedriver.exe")
# driver.get("http://127.0.0.1:5000/")
driver = webdriver.Remote('http://localhost:4444/wd/hub', DesiredCapabilities.FIREFOX)
driver.implicitly_wait(10)
# driver.maximize_window()
# HOME_PAGE = "http://127.0.0.1:5000/"
HOME_PAGE = 'http://121.4.94.30:91/'
def test_delete_word():
try:
driver.get(HOME_PAGE)
assert 'English Pal -' in driver.page_source
# login
elem = driver.find_element_by_link_text('登录')
elem.click()
uname = 'lanhui'
password = 'l0ve1t'
elem = driver.find_element_by_name('username')
elem.send_keys(uname)
elem = driver.find_element_by_name('password')
elem.send_keys(password)
elem = driver.find_element_by_xpath('//form[1]/p[3]/input[1]') # 找到登录按钮
elem.click()
assert 'EnglishPal Study Room for ' + uname in driver.title
# delete
elems = driver.find_element_by_class_name('new-word')
# 移动到元素elems对象的“顶端”与当前窗口的“顶部”对齐
driver.execute_script("arguments[0].scrollIntoView();", elems)
driver.save_screenshot('test_delete_pic1.png')
current_2 = elems.text
elem = driver.find_element_by_link_text("删除")
elem.click()
elems = driver.find_element_by_class_name('new-word')
driver.execute_script("arguments[0].scrollIntoView();", elems)
driver.save_screenshot('test_delete_pic2.png')
now_2 = elems.text
assert current_2 != now_2
finally:
driver.quit()
# test_delete_word()
# -*- coding: utf-8 -*-
# Run the docker image using the following command:
# docker run -d -p 4444:4444 selenium/standalone-chrome
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import random, time
import string
# 调用本地chromedriver
# driver = webdriver.Chrome(executable_path="D:\ChromeDriver\chromedriver.exe")
# driver.get("http://127.0.0.1:5000/")
driver = webdriver.Remote('http://localhost:4444/wd/hub', DesiredCapabilities.FIREFOX)
driver.implicitly_wait(10)
# driver.maximize_window()
# HOME_PAGE = "http://127.0.0.1:5000/"
HOME_PAGE = 'http://121.4.94.30:91/'
def test_delete_word():
try:
driver.get(HOME_PAGE)
assert 'English Pal -' in driver.page_source
# login
elem = driver.find_element_by_link_text('登录')
elem.click()
uname = 'lanhui'
password = 'l0ve1t'
elem = driver.find_element_by_name('username')
elem.send_keys(uname)
elem = driver.find_element_by_name('password')
elem.send_keys(password)
elem = driver.find_element_by_xpath('//form[1]/p[3]/input[1]') # 找到登录按钮
elem.click()
assert 'EnglishPal Study Room for ' + uname in driver.title
# delete
elems = driver.find_element_by_class_name('new-word')
# 移动到元素elems对象的“顶端”与当前窗口的“顶部”对齐
driver.execute_script("arguments[0].scrollIntoView();", elems)
driver.save_screenshot('test_delete_pic1.png')
current_2 = elems.text
elem = driver.find_element_by_link_text("删除")
elem.click()
elems = driver.find_element_by_class_name('new-word')
driver.execute_script("arguments[0].scrollIntoView();", elems)
driver.save_screenshot('test_delete_pic2.png')
now_2 = elems.text
assert current_2 != now_2
finally:
driver.quit()
# test_delete_word()

View File

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

View File

@ -0,0 +1,91 @@
from time import sleep
from selenium import webdriver
# 获取浏览器驱动,并且打开响应的网址
driver = webdriver.Chrome(executable_path="C:\Program Files (x86)\Google\ChromeDriver\chromedriver.exe")
HOME_PAGE = "http://127.0.0.1:5000/"
def test_word_operation():
try:
login()
unfamiliar()
familiar()
delete()
finally:
driver.quit()
def login():
driver.get(HOME_PAGE)
assert 'English Pal -' in driver.page_source
# login
elem = driver.find_element_by_link_text('登录')
elem.click()
sleep(2)
uname = 'peter'
password = 'peter'
elem = driver.find_element_by_name('username')
elem.send_keys(uname)
elem = driver.find_element_by_name('password')
elem.send_keys(password)
# find the login button
elem = driver.find_element_by_xpath('/html/body/form/p[3]/input')
elem.click()
assert 'EnglishPal Study Room for ' + uname in driver.title
def familiar():
sleep(5)
elem = driver.find_element_by_xpath('//*[@id="p_0"]/a[3]')
count = int(elem.find_element_by_xpath('//*[@id="freq_0"]').text)
loop = 3
for i in range(loop):
elem.click()
sleep(1)
new_count = int(driver.find_element_by_xpath('//*[@id="freq_0"]').text)
assert count - loop == new_count
def unfamiliar():
sleep(5)
elem = driver.find_element_by_xpath('//*[@id="p_0"]/a[4]')
count = int(elem.find_element_by_xpath('//*[@id="freq_0"]').text)
loop = 2
for i in range(loop):
elem.click()
sleep(1)
new_count = int(driver.find_element_by_xpath('//*[@id="freq_0"]').text)
assert count + loop == new_count
def delete():
sleep(3)
word = driver.find_element_by_xpath('//*[@id="word_0"]').text
elem = driver.find_element_by_xpath('//*[@id="p_0"]/a[5]')
elem.click()
sleep(5)
driver.refresh()
driver.refresh()
driver.refresh()
find_word = word in driver.page_source
assert find_word is False

190
app/user_service.py Normal file
View File

@ -0,0 +1,190 @@
from datetime import datetime
from admin_service import ADMIN_NAME
from flask import *
# from app import Yaml
# from app.Article import get_today_article, load_freq_history
# from app.WordFreq import WordFreq
# from app.wordfreqCMD import sort_in_descending_order
import Yaml
from Article import get_today_article, load_freq_history
from WordFreq import WordFreq
from wordfreqCMD import sort_in_descending_order
import pickle_idea
import pickle_idea2
# 初始化蓝图
userService = Blueprint("user_bp", __name__)
path_prefix = '/var/www/wordfreq/wordfreq/'
path_prefix = './' # comment this line in deployment
@userService.route("/get_next_article/<username>",methods=['GET','POST'])
def get_next_article(username):
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
session['old_articleID'] = session.get('articleID')
if request.method == 'GET':
visited_articles = session.get("visited_articles")
if visited_articles['article_ids'][-1] == "null": # 如果当前还是“null”则将“null”pop出来,无需index+=1
visited_articles['article_ids'].pop()
else: # 当前不为“null”直接 index+=1
visited_articles["index"] += 1
session["visited_articles"] = visited_articles
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
data = {
'visited_articles': visited_articles,
'today_article': today_article,
'result_of_generate_article': result_of_generate_article
}
else:
return 'Under construction'
return json.dumps(data)
@userService.route("/get_pre_article/<username>",methods=['GET'])
def get_pre_article(username):
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
if request.method == 'GET':
visited_articles = session.get("visited_articles")
if(visited_articles["index"]==0):
data=''
else:
visited_articles["index"] -= 1 # 上一篇index-=1
if visited_articles['article_ids'][-1] == "null": # 如果当前还是“null”则将“null”pop出来
visited_articles['article_ids'].pop()
session["visited_articles"] = visited_articles
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
data = {
'visited_articles': visited_articles,
'today_article': today_article,
'result_of_generate_article':result_of_generate_article
}
return json.dumps(data)
@userService.route("/<username>/<word>/unfamiliar", methods=['GET', 'POST'])
def unfamiliar(username, word):
'''
:param username:
:param word:
:return:
'''
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
pickle_idea.unfamiliar(user_freq_record, word)
session['thisWord'] = word # 1. put a word into session
session['time'] = 1
return "success"
@userService.route("/<username>/<word>/familiar", methods=['GET', 'POST'])
def familiar(username, word):
'''
:param username:
:param word:
:return:
'''
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
pickle_idea.familiar(user_freq_record, word)
session['thisWord'] = word # 1. put a word into session
session['time'] = 1
return "success"
@userService.route("/<username>/<word>/del", methods=['GET', 'POST'])
def deleteword(username, word):
'''
删除单词
:param username: 用户名
:param word: 单词
:return: 重定位到用户界面
'''
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
pickle_idea2.deleteRecord(user_freq_record, word)
# 模板userpage_get.html中删除单词是异步执行而flash的信息后续是同步执行的所以注释这段代码同时如果这里使用flash但不提取信息则会影响 signup.html的显示。bug复现删除单词后点击退出点击注册注册页面就会出现提示信息
# flash(f'{word} is no longer in your word list.')
return "success"
@userService.route("/<username>/userpage", methods=['GET', 'POST'])
def userpage(username):
'''
用户界面
:param username: 用户名
:return: 返回用户界面
'''
# 未登录,跳转到未登录界面
if not session.get('logged_in'):
return render_template('not_login.html')
# 用户过期
user_expiry_date = session.get('expiry_date')
if datetime.now().strftime('%Y%m%d') > user_expiry_date:
return render_template('expiry.html', expiry_date=user_expiry_date)
# 获取session里的用户名
username = session.get('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'])
f = WordFreq(content)
lst = f.get_freq()
return render_template('userpage_post.html',username=username,lst = lst, yml=Yaml.yml)
elif request.method == 'GET': # when we load a html page
d = load_freq_history(user_freq_record)
lst = pickle_idea2.dict2lst(d)
lst2 = []
for t in lst:
lst2.append((t[0], len(t[1])))
lst3 = sort_in_descending_order(lst2)
words = ''
for x in lst3:
words += x[0] + ' '
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
session['visited_articles'] = visited_articles
# 通过 today_article加载前端的显示页面
return render_template('userpage_get.html',
admin_name=ADMIN_NAME,
username=username,
session=session,
# flashed_messages=get_flashed_messages(), 仅有删除单词的时候使用到flash而删除单词是异步执行这里的信息提示是同步执行所以就没有存在的必要了
today_article=today_article,
result_of_generate_article=result_of_generate_article,
d_len=len(d),
lst3=lst3,
yml=Yaml.yml,
words=words)
@userService.route("/<username>/mark", methods=['GET', 'POST'])
def user_mark_word(username):
'''
标记单词
:param username: 用户名
:return: 重定位到用户界面
'''
username = session[username]
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
if request.method == 'POST':
# 提交标记的单词
d = load_freq_history(user_freq_record)
lst_history = pickle_idea2.dict2lst(d)
lst = []
for word in request.form.getlist('marked'):
lst.append((word, [get_time()]))
d = pickle_idea2.merge_frequency(lst, lst_history)
pickle_idea2.save_frequency_to_pickle(d, user_freq_record)
return redirect(url_for('user_bp.userpage', username=username))
else:
return 'Under construction'
def get_time():
'''
获取当前时间
:return: 当前时间
'''
return datetime.now().strftime('%Y%m%d%H%M') # upper to minutes

View File

@ -39,7 +39,7 @@ def file2str(fname):#文件转字符
def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。
special_characters = '_©~=+[]*&$%^@.,?!:;#()"“”—‘’' # 把里面的字符都去掉
special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' # 把里面的字符都去掉
for c in special_characters:
s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况
s = s.replace('--', ' ')
@ -70,7 +70,7 @@ def sort_in_ascending_order(lst):# 单词按频率降序排列
return lst2
def make_html_page(lst, fname):
def make_html_page(lst, fname): # 只是在wordfreqCMD.py中的main函数中调用所以不做修改
'''
功能把lst的信息存到fname中以html格式
'''

View File

@ -1,6 +1,11 @@
#!/bin/sh
cd /home/lanhui/englishpal
DEPLOYMENT_DIR=/home/lanhui/englishpal2/EnglishPal
cd $DEPLOYMENT_DIR
# Install dependencies
pip3 install -r requirements.txt
# Stop service
sudo docker stop EnglishPal
@ -10,9 +15,7 @@ sudo docker rm EnglishPal
sudo docker build -t englishpal .
# Run the application
sudo docker run -d --name EnglishPal -p 90:80 -v /home/lanhui/englishpal/app/static/frequency:/app/static/frequency -v /home/lanhui/englishpal/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 -v ${DEPLOYMENT_DIR}/app/static/:/app/static/ -t englishpal # for permanently saving data
# Save space. Run it after sudo docker run
sudo docker system prune -a -f

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
Flask==2.0.3
selenium==3.141.0
PyYAML~=6.0
pony==0.7.16
snowballstemmer==2.2.0
Werkzeug==2.2.2
pytest~=8.1.1