1
0
Fork 0

Compare commits

..

2 Commits

Author SHA1 Message Date
丁晟晔 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
33 changed files with 358 additions and 637 deletions

10
.gitignore vendored
View File

@ -2,20 +2,12 @@
venv/ venv/
app/__init__.py app/__init__.py
app/__pycache__/ app/__pycache__/
.DS_Store
app/.DS_Store
app/sqlite_commands.py app/sqlite_commands.py
app/static/usr/*.jpg app/static/usr/*.jpg
app/static/img/ app/static/img/
app/static/frequency/frequency_*.pickle app/static/frequency/frequency_*.pickle
app/static/frequency/frequency.p app/static/frequency/frequency.p
app/wordfreqapp.db app/static/wordfreqapp.db
app/db/wordfreqapp.db
app/static/donate-the-author.jpg app/static/donate-the-author.jpg
app/static/donate-the-author-hidden.jpg app/static/donate-the-author-hidden.jpg
app/model/__pycache__/ app/model/__pycache__/
app/test/__pycache__/
app/test/.pytest_cache/
app/test/pytest_report.html
app/test/assets
app/log.txt

View File

@ -1,5 +1,4 @@
FROM tiangolo/uwsgi-nginx-flask:python3.8-alpine FROM tiangolo/uwsgi-nginx-flask:python3.6
COPY requirements.txt /tmp COPY requirements.txt /app
COPY ./app/ /app/ RUN pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
RUN pip3 install -U pip -i https://mirrors.aliyun.com/pypi/simple/ COPY ./app /app
RUN pip3 install -r /tmp/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

4
Jenkinsfile vendored
View File

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

View File

@ -61,15 +61,15 @@ My steps for deploying English on a Ubuntu server.
All articles are stored in the `article` table in a SQLite file called All articles are stored in the `article` table in a SQLite file called
`app/db/wordfreqapp.db`. `app/static/wordfreqapp.db`.
### Adding new articles ### Adding new articles
To add articles, open and edit `app/db/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org). To add articles, open and edit `app/static/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org).
### Extending an account's expiry date ### Extending an account's expiry date
By default, an account's expiry is 30 days after first sign-up. To extend account's expiry date, open and edit `user` table in `app/db/wordfreqapp.db`. Simply update field `expiry_date`. By default, an account's expiry is 30 days after first sign-up. To extend account's expiry date, open and edit `user` table in `app/static/wordfreqapp.db`. Simply update field `expiry_date`.
### Exporting the database ### Exporting the database
@ -95,7 +95,7 @@ sqlite3 wordfreqapp.db`. Delete wordfreqapp.db first if it exists.
### Uploading wordfreqapp.db to the server ### Uploading wordfreqapp.db to the server
`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/db/` `pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/static`
@ -129,28 +129,6 @@ We welcome feedback on EnglishPal. Feedback examples:
EnglishPal's bugs and improvement suggestions are recorded in [Bugzilla](http://118.25.96.118/bugzilla/buglist.cgi?bug_status=__all__&list_id=1302&order=Importance&product=EnglishPal&query_format=specific). Send (lanhui at zjnu.edu.cn) an email message for opening a Bugzilla account or reporting a bug. EnglishPal's bugs and improvement suggestions are recorded in [Bugzilla](http://118.25.96.118/bugzilla/buglist.cgi?bug_status=__all__&list_id=1302&order=Importance&product=EnglishPal&query_format=specific). Send (lanhui at zjnu.edu.cn) an email message for opening a Bugzilla account or reporting a bug.
## End-to-end testing
We use the Selenium test framework to test our app.
In order to run the test, first we need to download a webdriver executable.
Microsoft Edge's webdriver can be downloaded from [microsoft-edge-tools-webdriver](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/). Make sure the version we download matches the version of the web browser installed on our laptop.
After extracting the downloaded zip file (e.g., edgedriver_win64.zip), rename msedgedriver.exe to MicrosoftWebDriver.exe.
Add MicrosoftWebDriver.exe's path to system's PATH variable.
Install the following dependencies too:
- pip install -U selenium==3.141.0
- pip install -U urllib3==1.26.2
Run English Pal first, then run the test using pytest as follows: pytest --html=pytest_report.html test_add_word.py
The above command will generate a HTML report file pytest_report.html after finishing executing test_add_word.py. Note: you need to install pytest-html package first: pip install pytest-html.
You may also want to use [webdriver-manager](https://pypi.org/project/webdriver-manager/) from PyPI, so that you can avoid tediously installing a web driver executable manually. However, my experience shows that webdriver-manager is too slow. For example, it took me 16 minutes to run 9 tests, while with the pre-installed web driver executable, it took less than 2 minutes.
## TODO ## TODO

View File

@ -1,6 +1,6 @@
from WordFreq import WordFreq from WordFreq import WordFreq
from wordfreqCMD import youdao_link, sort_in_descending_order from wordfreqCMD import youdao_link, sort_in_descending_order
from UseSqlite import InsertQuery, RecordQuery
import pickle_idea, pickle_idea2 import pickle_idea, pickle_idea2
import os import os
import random, glob import random, glob
@ -8,16 +8,18 @@ import hashlib
from datetime import datetime from datetime import datetime
from flask import Flask, request, redirect, render_template, url_for, session, abort, flash, get_flashed_messages from flask import Flask, request, redirect, render_template, url_for, session, abort, flash, get_flashed_messages
from difficulty import get_difficulty_level_for_user, text_difficulty_level, user_difficulty_level from difficulty import get_difficulty_level_for_user, text_difficulty_level, user_difficulty_level
from model.article import get_all_articles, get_article_by_id, get_number_of_articles
import logging
path_prefix = './' path_prefix = '/var/www/wordfreq/wordfreq/'
db_path_prefix = './db/' # comment this line in deployment path_prefix = './' # comment this line in deployment
def total_number_of_essays(): def total_number_of_essays():
return get_number_of_articles() rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
rq.instructions("SELECT * FROM article")
rq.do()
result = rq.get_results()
return len(result)
def get_article_title(s): def get_article_title(s):
@ -30,43 +32,33 @@ def get_article_body(s):
return '\n'.join(lst) return '\n'.join(lst)
#user_articlesWithoutNewWords_record 保存前端传来的用户阅读时长超过15秒且不含高亮生词的文章索引 def get_today_article(user_word_list, visited_articles):
def get_today_article(user_word_list, visited_articles,user_articlesWithoutNewWords_record): rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
if visited_articles is None: if visited_articles is None:
visited_articles = { visited_articles = {
"index" : 0, # 为 article_ids 的索引 "index" : 0, # 为 article_ids 的索引
"article_ids": [] # 之前显示文章的id列表越后越新 "article_ids": [] # 之前显示文章的id列表越后越新
} }
if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章,因此查找所有的文章 if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章,因此查找所有的文章
result = get_all_articles() rq.instructions("SELECT * FROM article")
else: # 生成阅读过的文章,因此查询指定 article_id 的文章 else: # 生成阅读过的文章,因此查询指定 article_id 的文章
if visited_articles["article_ids"][visited_articles["index"]] == 'null': # 可能因为直接刷新页面导致直接去查询了'null',因此当刷新的页面的时候,需要直接进行“上一篇”操作 if visited_articles["article_ids"][visited_articles["index"]] == 'null': # 可能因为直接刷新页面导致直接去查询了'null',因此当刷新的页面的时候,需要直接进行“上一篇”操作
visited_articles["index"] -= 1 visited_articles["index"] -= 1
visited_articles["article_ids"].pop() visited_articles["article_ids"].pop()
article_id = visited_articles["article_ids"][visited_articles["index"]] rq.instructions('SELECT * FROM article WHERE article_id=%d' % (visited_articles["article_ids"][visited_articles["index"]]))
result = get_article_by_id(article_id) rq.do()
result = rq.get_results()
random.shuffle(result) random.shuffle(result)
# Choose article according to reader's level # Choose article according to reader's level
logging.debug('* get_today_article(): start d1 = ... ') d1 = load_freq_history(path_prefix + 'static/frequency/frequency.p')
d1 = load_freq_history(user_word_list)
d2 = load_freq_history(path_prefix + 'static/words_and_tests.p') d2 = load_freq_history(path_prefix + 'static/words_and_tests.p')
logging.debug(' ... get_today_article(): get_difficulty_level_for_user() start')
d3 = get_difficulty_level_for_user(d1, d2) d3 = get_difficulty_level_for_user(d1, d2)
logging.debug(' ... get_today_article(): done')
d = None d = None
result_of_generate_article = "not found" result_of_generate_article = "not found"
d_user = load_freq_history(user_word_list) d_user = load_freq_history(user_word_list)
logging.debug('* get_today_article(): user_difficulty_level() start') user_level = user_difficulty_level(d_user, d3) # more consideration as user's behaviour is dynamic. Time factor should be considered.
articles_id_list=None
if os.path.exists(user_articlesWithoutNewWords_record) != False:
articles_id_list = pickle_idea.load_record(user_articlesWithoutNewWords_record)
#将 用户阅读时长超过15秒且不含高亮生词的文章记录 传入user_difficulty_level并据此 提高用户level
user_level = user_difficulty_level(d_user, d3,articles_id_list) # more consideration as user's behaviour is dynamic. Time factor should be considered.
logging.debug('* get_today_article(): done')
text_level = 0 text_level = 0
if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章 if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章
amount_of_visited_articles = len(visited_articles["article_ids"]) amount_of_visited_articles = len(visited_articles["article_ids"])
@ -95,10 +87,9 @@ def get_today_article(user_word_list, visited_articles,user_articlesWithoutNewWo
today_article = None today_article = None
if d: if d:
today_article = { today_article = {
"user_level": '%4.1f' % user_level, "user_level": '%4.2f' % user_level,
"text_level": '%4.1f' % text_level, "text_level": '%4.2f' % text_level,
"date": d['date'], "date": d['date'],
"article_id":d['article_id'],#该变量存储 用户阅读时长超过15秒且不含高亮生词的文章索引
"article_title": get_article_title(d['text']), "article_title": get_article_title(d['text']),
"article_body": get_article_body(d['text']), "article_body": get_article_body(d['text']),
"source": d["source"], "source": d["source"],

View File

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

87
app/UseSqlite.py Normal file
View File

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

View File

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

View File

@ -1,6 +1,5 @@
# System Library # System Library
from flask import * from flask import *
from markupsafe import escape
# Personal library # Personal library
from Yaml import yml from Yaml import yml
@ -38,22 +37,6 @@ def admin():
@adminService.route("/admin/article", methods=["GET", "POST"]) @adminService.route("/admin/article", methods=["GET", "POST"])
def article(): def article():
def _make_title_and_content(article_lst):
for article in article_lst:
text = escape(article.text) # Fix XSS vulnerability, contributed by Xu Xuan
article.title = text.split("\n")[0]
article.content = '<br/>'.join(text.split("\n")[1:])
def _update_context():
article_len = get_number_of_articles()
context["article_number"] = article_len
context["text_list"] = get_page_articles(_cur_page, _page_size)
_articles = get_page_articles(_cur_page, _page_size)
_make_title_and_content(_articles)
context["text_list"] = _articles
global _cur_page, _page_size global _cur_page, _page_size
is_admin = check_is_admin() is_admin = check_is_admin()
@ -61,15 +44,20 @@ def article():
return is_admin return is_admin
_article_number = get_number_of_articles() _article_number = get_number_of_articles()
try: try:
_page_size = min(max(1, int(request.args.get("size", 5))), _article_number) # 最小的size是1 _page_size = min(
_cur_page = min(max(1, int(request.args.get("page", 1))), _article_number // _page_size + (_article_number % _page_size > 0)) # 最小的page是1 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: except ValueError:
return "page parameters must be integer!" return "page parmas must be int!"
_articles = get_page_articles(_cur_page, _page_size) _articles = get_page_articles(_cur_page, _page_size)
_make_title_and_content(_articles) for article in _articles: # 获取每篇文章的title
article.title = article.text.split("\n")[0]
article.content = '<br/>'.join(article.text.split("\n")[1:])
context = { context = {
"article_number": _article_number, "article_number": _article_number,
@ -79,16 +67,23 @@ def article():
"username": session.get("username"), "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": if request.method == "GET":
try: try:
delete_id = int(request.args.get("delete_id", 0)) delete_id = int(request.args.get("delete_id", 0))
except: except:
return "Delete article ID must be integer!" return "Delete article ID must be int!"
if delete_id: # delete article if delete_id: # delete article
delete_article_by_id(delete_id) delete_article_by_id(delete_id)
_update_context() _update_context()
elif request.method == "POST": elif request.method == "POST":
data = request.form data = request.form
content = data.get("content", "") content = data.get("content", "")
@ -102,7 +97,6 @@ def article():
_update_context() _update_context()
title = content.split('\n')[0] title = content.split('\n')[0]
flash(f'Article added. Title: {title}') flash(f'Article added. Title: {title}')
return render_template("admin_manage_article.html", **context) return render_template("admin_manage_article.html", **context)

View File

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

View File

@ -7,7 +7,6 @@
import pickle import pickle
import math import math
from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order
import snowballstemmer import snowballstemmer
@ -19,7 +18,6 @@ def load_record(pickle_fname):
return d return d
ENGLISH_WORD_DIFFICULTY_DICT = {}
def convert_test_type_to_difficulty_level(d): def convert_test_type_to_difficulty_level(d):
""" """
对原本的单词库中的单词进行难度评级 对原本的单词库中的单词进行难度评级
@ -41,11 +39,9 @@ def convert_test_type_to_difficulty_level(d):
elif 'BBC' in d[k]: elif 'BBC' in d[k]:
result[k] = 8 result[k] = 8
global ENGLISH_WORD_DIFFICULTY_DICT
ENGLISH_WORD_DIFFICULTY_DICT = result
return result # {'apple': 4, ...} return result # {'apple': 4, ...}
def get_difficulty_level_for_user(d1, d2): def get_difficulty_level_for_user(d1, d2):
""" """
d2 来自于词库的35511个已标记单词 d2 来自于词库的35511个已标记单词
@ -53,11 +49,7 @@ def get_difficulty_level_for_user(d1, d2):
在d2的后面添加单词没有新建一个新的字典 在d2的后面添加单词没有新建一个新的字典
""" """
# TODO: convert_test_type_to_difficulty_level() should not be called every time. Each word's difficulty level should be pre-computed. # TODO: convert_test_type_to_difficulty_level() should not be called every time. Each word's difficulty level should be pre-computed.
if ENGLISH_WORD_DIFFICULTY_DICT == {}:
d2 = convert_test_type_to_difficulty_level(d2) # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...} d2 = convert_test_type_to_difficulty_level(d2) # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...}
else:
d2 = ENGLISH_WORD_DIFFICULTY_DICT
stemmer = snowballstemmer.stemmer('english') stemmer = snowballstemmer.stemmer('english')
for k in d1: # 用户的词 for k in d1: # 用户的词
@ -95,21 +87,10 @@ def revert_dict(d):
return d2 return d2
#articlesWithoutNewWords 存储用户阅读时长超过15秒且不含高亮生词的文章索引信息用户阅读完后没有添加生词的文章 def user_difficulty_level(d_user, d):
def user_difficulty_level(d_user, d,articlesWithoutNewWords):
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 count = 0
geometric = 1 geometric = 1
#传入根据用户生词计算出的用户level
def get_level_sum(level):
if articlesWithoutNewWords!=None:#若用户阅读完后没有添加生词的文章索引不为空则根据这些文章的等级适度提高用户的level
for article in articlesWithoutNewWords:
if(level<float(article.get('text_level'))):
level+=(float(article.get('text_level'))-level)/20 #根据文章等级提高用户level用户level与用户阅读完后没有添加生词的文章level 差距越大,分数提高程度越大
print(f"用户等级{level}")
print("-----------------------------------")
return level
for date in sorted(d_user2.keys(), for date in sorted(d_user2.keys(),
reverse=True): # most recently added words are more important while determining user's level reverse=True): # most recently added words are more important while determining user's level
lst = d_user2[date] # a list of words lst = d_user2[date] # a list of words
@ -127,9 +108,10 @@ def user_difficulty_level(d_user, d,articlesWithoutNewWords):
geometric = geometric * (hard) geometric = geometric * (hard)
count += 1 count += 1
if count >= 10: if count >= 10:
return get_level_sum(geometric ** (1 / count)) return geometric ** (1 / count)
return geometric ** (1 / max(count, 1))
return get_level_sum(geometric ** (1 / max(count, 1)))
def text_difficulty_level(s, d): def text_difficulty_level(s, d):
s = remove_punctuation(s) s = remove_punctuation(s)

View File

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

View File

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

View File

@ -7,7 +7,7 @@ def add_article(content, source="manual_input", level="5", question="No question
Article( Article(
text=content, text=content,
source=source, source=source,
date=datetime.now().strftime("%d %b %Y"), # format style of `5 Oct 2022` date=datetime.now().strftime("%-d %b %Y"), # format style of `5 Oct 2022`
level=level, level=level,
question=question, question=question,
) )
@ -32,17 +32,3 @@ def get_page_articles(num, size):
x x
for x in Article.select().order_by(desc(Article.article_id)).page(num, size) for x in Article.select().order_by(desc(Article.article_id)).page(num, size)
] ]
def get_all_articles():
articles = []
with db_session:
for article in Article.select():
articles.append(article.to_dict())
return articles
def get_article_by_id(article_id):
with db_session:
article = Article.get(article_id=article_id)
return [article.to_dict()]

View File

@ -9,7 +9,6 @@ js:
- ../static/js/jquery.js - ../static/js/jquery.js
- ../static/js/read.js - ../static/js/read.js
- ../static/js/word_operation.js - ../static/js/word_operation.js
- ../static/js/checkboxes.js
bottom: # 在页面加载完之后加载 bottom: # 在页面加载完之后加载
- ../static/js/fillword.js - ../static/js/fillword.js
- ../static/js/highlight.js - ../static/js/highlight.js

View File

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

View File

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

View File

@ -22,38 +22,62 @@ function getWord() {
function highLight() { function highLight() {
if (!isHighlight) return; if (!isHighlight) return;
let articleContent = document.getElementById("article").innerHTML; // innerHTML保留HTML标签来保持部分格式且适配不同的浏览器 let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml使用innerText会把原文章中所包含的<br>标签去除,导致处理后的文章内容失去了原来的格式
let pickedWords = document.getElementById("selected-words"); // words picked to the text area let pickedWords = document.getElementById("selected-words"); // words picked to the text area
let dictionaryWords = document.getElementById("selected-words2"); // words appearing in the user's new words list let dictionaryWords = document.getElementById("selected-words2"); // words appearing in the user's new words list
let allWords = dictionaryWords === null ? pickedWords.value + " " : pickedWords.value + " " + dictionaryWords.value; let allWords = ""; //初始化allWords的值避免进入判断后编译器认为allWords未初始化的问题
const list = allWords.split(" "); // 将所有的生词放入一个list中 if(dictionaryWords != null){//增加一个判断检查生词本里面是否为空如果为空allWords只添加选中的单词
let totalSet = new Set(); allWords = pickedWords.value + " " + dictionaryWords.value;
}
else{
allWords = pickedWords.value + " ";
}
const list = allWords.split(" ");//将所有的生词放入一个list中用于后续处理
for (let i = 0; i < list.length; ++i) { for (let i = 0; i < list.length; ++i) {
list[i] = list[i].replace(/(^\W*)|(\W*$)/g, ""); // 消除单词两边的非单词字符 list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); //消除单词两边的空字符
if (list[i] != "" && !totalSet.has(list[i])) { list[i] = list[i].replace('|', "");
// 返回所有匹配单词的集合, 正则表达式RegExp()中, "\b"匹配一个单词的边界, g 表示全局匹配, i 表示对大小写不敏感。 list[i] = list[i].replace('?', "");
let matches = new Set(articleContent.match(new RegExp("\\b" + list[i] + "\\b", "gi"))); if (list[i] !== "" && "<mark>".indexOf(list[i]) === -1 && "</mark>".indexOf(list[i]) === -1) {
if (matches.has("mark")) { //将文章中所有出现该单词word的地方改为"<mark>" + word + "<mark>"。 正则表达式RegExp()中,"\\b"代表单词边界匹配。
// 优先处理单词为 "mark" 的情况
totalSet = new Set(["mark", ...totalSet]); //修改代码
} let articleContent_fb = articleContent; //文章副本
totalSet = new Set([...totalSet, ...matches]); 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>");
} }
} }
// 删除所有的mark标签,防止标签发生嵌套
articleContent = articleContent.replace(/<(mark)[^>]*>/gi, "");
articleContent = articleContent.replace(/<(\/mark)[^>]*>/gi, "");
// 将文章中所有出现该单词word的地方改为"<mark>" + word + "<mark>"。
for (let word of totalSet) {
articleContent = articleContent.replace(new RegExp("\\b" + word + "\\b", "g"), "<mark>" + word + "</mark>");
} }
document.getElementById("article").innerHTML = articleContent; document.getElementById("article").innerHTML = articleContent;
} }
function cancelHighlighting() { function cancelHighlighting() {
let articleContent = document.getElementById("article").innerHTML; let articleContent = document.getElementById("article").innerText;//将原来的.innerText改为.innerHtml原因同上
articleContent = articleContent.replace(/<(mark)[^>]*>/gi, ""); let pickedWords = document.getElementById("selected-words");
articleContent = articleContent.replace(/<(\/mark)[^>]*>/gi, ""); 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; document.getElementById("article").innerHTML = articleContent;
} }

View File

@ -9,7 +9,7 @@ var Reader = (function() {
msg.rate = rate; msg.rate = rate;
msg.lang = "en-US"; msg.lang = "en-US";
msg.onboundary = ev => { msg.onboundary = ev => {
if (ev.name === "word") { if (ev.name == "word") {
current_position = ev.charIndex; current_position = ev.charIndex;
} }
} }

View File

@ -9,6 +9,7 @@ function familiar(theWord) {
let new_freq = freq - 1; let new_freq = freq - 1;
const allow_move = document.getElementById("move_dynamiclly").checked; const allow_move = document.getElementById("move_dynamiclly").checked;
if (allow_move) { if (allow_move) {
if (new_freq <= 0) { if (new_freq <= 0) {
removeWord(theWord); removeWord(theWord);
} else { } else {
@ -113,7 +114,7 @@ function removeWord(word) {
// 根据词频信息删除元素 // 根据词频信息删除元素
word = word.replace('&amp;', '&'); word = word.replace('&amp;', '&');
const element_to_remove = document.getElementById(`p_${word}`); const element_to_remove = document.getElementById(`p_${word}`);
if (element_to_remove !== null) { if (element_to_remove != null) {
element_to_remove.remove(); element_to_remove.remove();
} }
} }
@ -128,7 +129,7 @@ function renderWord(word) {
for (const current of container.children) { for (const current of container.children) {
const cur_word = parseWord(current); const cur_word = parseWord(current);
// 找到第一个词频比它小的元素,插入到这个元素前面 // 找到第一个词频比它小的元素,插入到这个元素前面
if (compareWord(cur_word, word) === -1) { if (compareWord(cur_word, word) == -1) {
container.insertBefore(new_element, current); container.insertBefore(new_element, current);
inserted = true; inserted = true;
break; break;
@ -164,11 +165,17 @@ function elementFromString(string) {
* 当first大于second时返回1 * 当first大于second时返回1
*/ */
function compareWord(first, second) { function compareWord(first, second) {
if (first.freq !== second.freq) { if (first.freq < second.freq) {
return first.freq < second.freq ? -1 : 1; return -1;
} }
if (first.word !== second.word) { if (first.freq > second.freq) {
return first.word < second.word ? -1 : 1; return 1;
}
if (first.word < second.word) {
return -1;
}
if (first.word > second.word) {
return 1;
} }
return 0; return 0;
} }

View File

@ -1,7 +1,7 @@
{% block body %} {% block body %}
{% if session['logged_in'] %} {% if session['logged_in'] %}
你已登录 <a href="/{{ session['username'] }}/userpage">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a> 你已登录 <a href="/{{ session['username'] }}">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>
{% else %} {% else %}
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" /> <meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" />
@ -32,13 +32,14 @@
<div class="container"> <div class="container">
<section class="signin-heading"> <section class="signin-heading">
<h1>Sign in</h1> <h1>Sign In</h1>
</section> </section>
<input type="text" placeholder="用户名" class="username" id="username"> <input type="text" placeholder="用户名" class="username" id="username">
<input type="password" placeholder="密码" class="password" id="password"> <input type="password" placeholder="密码" class="password" id="password">
<button type="button" class="btn" onclick="login()">登录</button> <button type="button" class="btn" onclick="login()">登录</button>
<a class="signup" href="/signup">注册</a> <a class="signup" href="/signup">注册</a>
</div> </div>
{% endif %} {% endif %}

View File

@ -44,7 +44,6 @@
<a href="http://youdao.com/w/eng/{{x[0]}}/#keyfrom=dict2.index">{{x[0]}}</a> {{x[1]}} <a href="http://youdao.com/w/eng/{{x[0]}}/#keyfrom=dict2.index">{{x[0]}}</a> {{x[1]}}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<p class="text-muted">Version: 20230810</p>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> <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> </div>
{{ yml['footer'] | safe }} {{ yml['footer'] | safe }}

View File

@ -53,7 +53,7 @@ You're logged in already! <a href="/logout">Logout</a>.
<div class="container"> <div class="container">
<section class="signin-heading"> <section class="signin-heading">
<h1>Sign up</h1> <h1>Sign Up</h1>
</section> </section>
<p><input type="username" id="username" placeholder="输入用户名" class="username"></p> <p><input type="username" id="username" placeholder="输入用户名" class="username"></p>

View File

@ -5,8 +5,6 @@
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes"/> 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"/> <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 }} {{ yml['header'] | safe }}
{% if yml['css']['item'] %} {% if yml['css']['item'] %}
@ -34,24 +32,6 @@
40%, 60% { transform: translate3d(+4px, 0, 0); } 40%, 60% { transform: translate3d(+4px, 0, 0); }
50% { 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> </style>
</head> </head>
<body> <body>
@ -65,28 +45,22 @@
<a class="btn btn-secondary" href="/reset" role="button" onclick="stopRead()">重设密码</a> <a class="btn btn-secondary" href="/reset" role="button" onclick="stopRead()">重设密码</a>
</p> </p>
{% for message in get_flashed_messages() %} {# {% for message in flashed_messages %}#} {# 根据user_service.userpage,取消了参数flashed_messages因此注释了这段代码 #}
<div class="alert alert-warning alert-dismissible fade show" role="alert"> {# <div class="alert alert-warning" role="alert">Congratulations! {{ message }}</div>#}
{{ message }} {# {% endfor %}#}
<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="btn btn-success" id="load_next_article" onclick="load_next_article()"> 下一篇 Next Article </button>
<button class="arrow" id="load_pre_article" onclick="load_pre_article();Reader.stopRead()" style="display: none" title="上一篇 Previous Article"></button> <button class="btn btn-success" id="load_pre_article" onclick="load_pre_article()" > 上一篇 Previous Article </button>
<p><b>阅读文章并回答问题</b></p> <p><b>阅读文章并回答问题</b></p>
<div id="text-content"> <div id="text-content">
<div id="found"> <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> <div class="alert alert-success" role="alert">According to your word list, your level is <span class="badge bg-success" id="user-level">{{ today_article["user_level"] }}</span> and we have chosen an article with a difficulty level of <span class="badge bg-success" id="text_level">{{ today_article["text_level"] }}</span> for you.</div>
<p class="text-muted" id="date">Article added on: {{ today_article["date"] }}</p><br/> <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/> <div class="p-3 mb-2 bg-light text-dark"><br/>
<p class="display-6" id="article_title">{{ today_article["article_title"] }}</p><br/> <p class="display-5" id="article_title">{{ today_article["article_title"] }}</p><br/>
<p class="lead"><font id="article">{{ today_article["article_body"] }}</font></p><br/> <p class="lead"><font id="article" size=2>{{ today_article["article_body"] }}</font></p><br/>
<div>
<p><small class="text-muted" id="source">{{ today_article['source'] }}</small></p><br/> <p><small class="text-muted" id="source">{{ today_article['source'] }}</small></p><br/>
</div>
<p><b id="question">{{ today_article['question'] }}</b></p><br/> <p><b id="question">{{ today_article['question'] }}</b></p><br/>
<script type="text/javascript"> <script type="text/javascript">
function toggle_visibility(id) { {# https://css-tricks.com/snippets/javascript/showhide-element/#} function toggle_visibility(id) { {# https://css-tricks.com/snippets/javascript/showhide-element/#}
@ -123,8 +97,8 @@
<p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p> <p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p>
<form method="post" action="/{{ username }}/userpage"> <form method="post" action="/{{ username }}/userpage">
<textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/> <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> <input type="submit" value="把生词加入我的生词库"/>
<button class="btn btn-primary btn-lg" type="reset">清除</button> <input type="reset" value="清除"/>
</form> </form>
{% if session.get['thisWord'] %} {% if session.get['thisWord'] %}
<script type="text/javascript"> <script type="text/javascript">
@ -173,33 +147,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<script type="text/javascript"> <script type="text/javascript">
window.onload = function () { // 页面加载时执行
// 刷新页面或进入页面时判断,若不是首篇文章,则上一篇按钮可见
if(sessionStorage.getItem('pre_page_button')!="display" && sessionStorage.getItem('pre_page_button')){
$('#load_pre_article').show();
}
startTime=Date.now();//记录用户页面的初始时间
if(window.performance.navigation.type!=1){//将第一次加载页面时的文章id,level保存到本地
sessionStorage.setItem('article_id','{{today_article['article_id']}}');
sessionStorage.setItem('text_level','{{today_article['text_level']}}');
}
//
};
function load_next_article(){ function load_next_article(){
//在进入下一篇之前处理
endTime=Date.now();//记录用户点击下一篇文章时的时间
spendtime=endTime-startTime;//计算出用户浏览该页面的时长,单位毫秒
if(spendtime/1000>15){//若浏览时长大于15说明用户认真阅读了该篇文章时长小于15代表用户不感兴趣跳过不做任何处理
if(!checkMarkedWords()){//检查该篇文章是否有生词高亮若没有将文章id,level发送到后台保存至文件中在计算用户level时根据文件中保存的文章等级计算
$.ajax({
url: '/submit_article_id_without_new_words/{{username}}/'+sessionStorage.getItem('article_id')+'/'+sessionStorage.getItem('text_level'),
dataType: 'json',
success: function(data) {}
});
}
}
$.ajax({ $.ajax({
url: '/get_next_article/{{username}}', url: '/get_next_article/{{username}}',
dataType: 'json', dataType: 'json',
@ -212,21 +160,8 @@
} }
} }
}); });
startTime=Date.now();//在页面加载完下一篇文章,记录起始时间
} }
function load_pre_article(){ function load_pre_article(){
//在进入上一篇之前处理
endTime=Date.now();//记录用户点击上一篇文章时的时间
spendtime=endTime-startTime;//计算出用户浏览该文章的时长,单位毫秒
if(spendtime/1000>15){
if(!checkMarkedWords()){
$.ajax({
url: '/submit_article_id_without_new_words/{{username}}/'+sessionStorage.getItem('article_id')+'/'+sessionStorage.getItem('text_level'),
dataType: 'json',
success: function(data) {}
});
}
}
$.ajax({ $.ajax({
url: '/get_pre_article/{{username}}', url: '/get_pre_article/{{username}}',
dataType: 'json', dataType: 'json',
@ -238,10 +173,9 @@
} }
} }
}); });
startTime=Date.now();//在页面加载完下一篇文章,记录起始时间
} }
function update(today_article){ function update(today_article){
$('#user_level').html(today_article['user_level']); $('#user-level').html(today_article['user_level']);
$('#text_level').html(today_article["text_level"]); $('#text_level').html(today_article["text_level"]);
$('#date').html('Article added on: '+today_article["date"]); $('#date').html('Article added on: '+today_article["date"]);
$('#article_title').html(today_article["article_title"]); $('#article_title').html(today_article["article_title"]);
@ -249,42 +183,13 @@
$('#source').html(today_article['source']); $('#source').html(today_article['source']);
$('#question').html(today_article["question"]); $('#question').html(today_article["question"]);
$('#answer').html(today_article["answer"]); $('#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);
sessionStorage.setItem('article_id',today_article["article_id"]);//将更新后的文章id,level保存到本地
sessionStorage.setItem('text_level',today_article['text_level']);
} }
function checkMarkedWords(){//检查文章中是否有高亮生词
let article_title= document.getElementById("article_title").innerHTML;//从页面中获取文章名和内容
let article_body=document.getElementById("article").innerHTML;
let article=article_title+" "+article_body;
let pickedWords = document.getElementById("selected-words"); // words picked to the text area
let dictionaryWords = document.getElementById("selected-words2"); // words appearing in the user's new words list
let allWords = dictionaryWords === null ? pickedWords.value + " " : pickedWords.value + " " + dictionaryWords.value;
const list = allWords.split(" "); // 将所有的生词放入一个list中
for (let i = 0; i < list.length; ++i) {
list[i] = list[i].replace(/(^\W*)|(\W*$)/g, "");
}
for (let i=1;i<list.length-1;i++){//list
if(article.search(list[i])!==-1){
return true;
}
}
return false;
}
//
<!-- 检查是否存在上一篇或下一篇,不存在则对应按钮隐藏--> <!-- 检查是否存在上一篇或下一篇,不存在则对应按钮隐藏-->
function check_pre(visited_articles){ function check_pre(visited_articles){
if((visited_articles=='')||(visited_articles['index']<=0)){ if((visited_articles=='')||(visited_articles['index']<=0)){
$('#load_pre_article').hide(); $('#load_pre_article').hide();
sessionStorage.setItem('pre_page_button', 'display')
}else{ }else{
$('#load_pre_article').show(); $('#load_pre_article').show();
sessionStorage.setItem('pre_page_button', 'show')
} }
} }
function check_next(result_of_generate_article){ function check_next(result_of_generate_article){

View File

@ -20,13 +20,9 @@
<title>EnglishPal Study Room for {{username}}</title> <title>EnglishPal Study Room for {{username}}</title>
</head> </head>
<body> <body>
<div class="container-fluid"> <p>取消勾选认识的单词</p>
<p class="mt-md-3">
<input type="button" id="btn-cancel-selection" value="取消勾选" onclick="toggleCheckboxSelection(false)" />
<input type="button" id="btn-selection" value="全部勾选" onclick="toggleCheckboxSelection(true)" />
</p>
<form method="post" action="/{{username}}/mark"> <form method="post" action="/{{username}}/mark">
<button type="submit" name="add-btn" class="btn btn-outline-primary btn-lg">加入我的生词簿</button> <input type="submit" name="add-btn" value="加入我的生词簿"/>
{% for x in lst %} {% for x in lst %}
{% set word = x[0]%} {% set word = x[0]%}
<p> <p>
@ -45,6 +41,5 @@
<script src="{{ js }}" ></script> <script src="{{ js }}" ></script>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div>
</body> </body>
</html> </html>

View File

@ -1,9 +1,6 @@
import pytest import pytest
import sqlite3
import time
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from pathlib import Path
@pytest.fixture @pytest.fixture
def URL(): def URL():
@ -12,24 +9,5 @@ def URL():
@pytest.fixture @pytest.fixture
def driver(): def driver():
return webdriver.Edge() # follow the "End-to-end testing" section in README.md to install the web driver executable my_driver = webdriver.Edge() # uncomment this line if you wish to run the test on your laptop
return my_driver
@pytest.fixture
def restore_sqlite_database():
'''
Automatically restore SQLite database file app/db/wordfreqapp.db
using SQL statements from app/static/wordfreqapp.sql
'''
con = sqlite3.connect('../db/wordfreqapp.db')
with con:
con.executescript('DROP TABLE IF EXISTS user;')
con.executescript('DROP TABLE IF EXISTS article;')
con.executescript(open('../static/wordfreqapp.sql', encoding='utf8').read())
con.close()
@pytest.fixture(autouse=True)
def restart_englishpal(restore_sqlite_database):
(Path(__file__).parent / '../main.py').touch()
time.sleep(1)

View File

@ -1,33 +0,0 @@
import uuid
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import UnexpectedAlertPresentException, NoAlertPresentException
def signup(URL, driver):
username = 'TestUser' + str(uuid.uuid1()).split('-')[0].title()
password = '[Abc+123]'
driver.get(URL)
elem = driver.find_element_by_link_text('注册')
elem.click()
elem = driver.find_element_by_id('username')
elem.send_keys(username)
elem = driver.find_element_by_id('password')
elem.send_keys(password)
elem = driver.find_element_by_id('password2')
elem.send_keys(password)
elem = driver.find_element_by_class_name('btn') # 找到"注册"按钮
elem.click()
try:
WebDriverWait(driver, 1).until(EC.alert_is_present())
driver.switch_to.alert.accept()
except (UnexpectedAlertPresentException, NoAlertPresentException):
pass
return username, password

View File

@ -1,23 +1,68 @@
import time # -*- coding: utf-8 -*-
from helper import signup # Run the docker image using the following command:
# docker run -d -p 4444:4444 selenium/standalone-chrome
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import random, time
import string
driver = webdriver.Remote('http://localhost:4444/wd/hub', DesiredCapabilities.FIREFOX)
driver.implicitly_wait(10)
HOME_PAGE = 'http://121.4.94.30:91/'
def test_add_word(URL, driver): def has_punctuation(s):
return [c for c in s if c in string.punctuation] != []
def test_add_word():
try: try:
username, password = signup(URL, driver) # sign up a new account and automatically log in driver.get(HOME_PAGE)
time.sleep(1) assert 'English Pal -' in driver.page_source
# login
elem = driver.find_element_by_link_text('登录')
elem.click()
uname = 'lanhui'
password = 'l0ve1t'
elem = driver.find_element_by_name('username')
elem.send_keys(uname)
elem = driver.find_element_by_name('password')
elem.send_keys(password)
elem = driver.find_element_by_xpath('//form[1]/p[3]/input[1]') # 找到登录按钮
elem.click()
assert 'EnglishPal Study Room for ' + uname in driver.title
# get essay content
elem = driver.find_element_by_id('text-content')
essay_content = elem.text
# enter the word in the text area
elem = driver.find_element_by_id('selected-words') elem = driver.find_element_by_id('selected-words')
word = 'devour' word = random.choice(essay_content.split())
while 'font>' in word or 'br>' in word or 'p>' in word or len(word) < 6 or has_punctuation(word):
word = random.choice(essay_content.split())
elem.send_keys(word) elem.send_keys(word)
elem = driver.find_element_by_xpath('//form[1]//button[1]') # 找到"把生词加入我的生词库"按钮 elem = driver.find_element_by_xpath('//form[1]//input[1]') # 找到get所有词频按钮
elem.click() elem.click()
elem = driver.find_element_by_name('add-btn') # 找到"加入我的生词簿"按钮 elems = driver.find_elements_by_xpath("//input[@type='checkbox']")
for elem in elems:
if elem.get_attribute('name') == 'marked':
elem.click() elem.click()
elem = driver.find_element_by_name('add-btn') # 找到加入我的生词簿按钮
elem.click()
driver.refresh()
driver.refresh()
driver.refresh()
elems = driver.find_elements_by_xpath("//p[@class='new-word']/a") elems = driver.find_elements_by_xpath("//p[@class='new-word']/a")
found = 0 found = 0

View File

@ -1,114 +0,0 @@
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import UnexpectedAlertPresentException, NoAlertPresentException, \
ElementClickInterceptedException, NoSuchElementException, TimeoutException
from helper import signup
def is_textarea_empty(driver):
# 获取<textarea>元素的内容,判断是否加入了生词
textarea_element = driver.find_element(By.ID, 'selected-words')
content = textarea_element.get_attribute("value")
return content is None or content.strip() == ""
def get_user_level(driver):
return float(driver.find_element(By.ID, 'user_level').text)
def scroll(total_time, driver):
try:
half_time = (total_time - 2) / 2 # 分成两半,一半时间滚到底,一半时间滚回顶,模拟阅读时间
# 滚动到底部
start_time = time.time()
end_time = start_time + half_time
while time.time() < end_time:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 确保滚动更平滑
time.sleep(1)
# 滚动回顶部
start_time = time.time()
end_time = start_time + half_time
while time.time() < end_time:
driver.execute_script("window.scrollTo(0, -document.body.scrollHeight);")
time.sleep(1)
except ElementClickInterceptedException:
print("加载出错")
def read_test_with_time(driver, total_time):
# 获取点击前的user_level和text_level
before_user_level = get_user_level(driver)
before_text_level = float(driver.find_element(By.ID, 'text_level').text)
# 检查是否加入了生词簿
check = is_textarea_empty(driver)
scroll(total_time, driver)
# 定位“下一页”按钮
next_button = driver.find_element(By.ID, 'load_next_article')
# 模拟点击
next_button.click()
# 关闭弹窗
try:
WebDriverWait(driver, 1).until(EC.alert_is_present())
driver.switch_to.alert.accept()
except (UnexpectedAlertPresentException, NoAlertPresentException):
pass
# 等待页面加载完毕,正确地获取更新后的值
time.sleep(2)
# 获取点击后的user_level
after_user_level = get_user_level(driver)
# 等待3秒方便观察测试
time.sleep(3)
return before_user_level, after_user_level, before_text_level, check
def test_score_with_enough_time(driver, URL):
try:
# 登录并跳转到测试主页面
signup(URL, driver)
# 测试阅读到足够时间的等级变化
before_user_level, after_user_level, before_text_level, check = read_test_with_time(driver, 18)
# print(check, before_user_level, after_user_level, before_text_level)
if before_user_level < before_text_level and check:
assert before_user_level < after_user_level, 'The user level dose not increase after reading an article with a high level and clicking to navigate away without adding any new vocabulary. '
except (NoSuchElementException, TimeoutException) as e:
print("Error occurs: " + str(e))
finally:
driver.quit()
def test_score_without_enough_time(driver, URL):
try:
# 登录并跳转到测试主页面
signup(URL, driver)
# 测试阅读时间不足够的等级变化
before_user_level, after_user_level, before_text_level, check = read_test_with_time(driver, 6)
# print(check, before_user_level, after_user_level, before_text_level)
if before_user_level < before_text_level and check:
assert before_user_level == after_user_level, 'After the user quickly skipped through the article, the user level does not remain unchanged. '
except (NoSuchElementException, TimeoutException) as e:
print("Error occurs: " + str(e))
finally:
driver.quit()

View File

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

View File

@ -1,5 +1,3 @@
import pickle
import os.path
from datetime import datetime from datetime import datetime
from admin_service import ADMIN_NAME from admin_service import ADMIN_NAME
from flask import * from flask import *
@ -17,9 +15,6 @@ from wordfreqCMD import sort_in_descending_order
import pickle_idea import pickle_idea
import pickle_idea2 import pickle_idea2
import logging
logging.basicConfig(filename='log.txt', format='%(asctime)s %(message)s', level=logging.DEBUG)
# 初始化蓝图 # 初始化蓝图
userService = Blueprint("user_bp", __name__) userService = Blueprint("user_bp", __name__)
@ -28,8 +23,6 @@ path_prefix = './' # comment this line in deployment
@userService.route("/get_next_article/<username>",methods=['GET','POST']) @userService.route("/get_next_article/<username>",methods=['GET','POST'])
def get_next_article(username): def get_next_article(username):
user_articlesWithoutNewWords_record = path_prefix + 'static/frequency/' + 'ArticlesWithoutNewWords_%s.pickle' % (username)
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username) user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
session['old_articleID'] = session.get('articleID') session['old_articleID'] = session.get('articleID')
if request.method == 'GET': if request.method == 'GET':
@ -39,9 +32,7 @@ def get_next_article(username):
else: # 当前不为“null”直接 index+=1 else: # 当前不为“null”直接 index+=1
visited_articles["index"] += 1 visited_articles["index"] += 1
session["visited_articles"] = visited_articles session["visited_articles"] = visited_articles
logging.debug('/get_next_article: start calling get_today_arcile()') visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'),user_articlesWithoutNewWords_record)
logging.debug('/get_next_arcile: done.')
data = { data = {
'visited_articles': visited_articles, 'visited_articles': visited_articles,
'today_article': today_article, 'today_article': today_article,
@ -51,37 +42,8 @@ def get_next_article(username):
return 'Under construction' return 'Under construction'
return json.dumps(data) return json.dumps(data)
#保存前端传来 用户阅读时长超过15秒且不含高亮生词的文章用户阅读完后没有添加生词的文章的id,level
@userService.route("/submit_article_id_without_new_words/<username>/<article_id>/<text_level>",methods=['GET'])
def set_article_id_without_new_words(username,article_id,text_level):
user_articles_without_new_words_record_path = path_prefix + 'static/frequency/' + 'ArticlesWithoutNewWords_%s.pickle' % (username)
if os.path.exists(user_articles_without_new_words_record_path)==False: #若不存在 存储用户阅读时长超过15秒且不含高亮生词的文章索引信息的文件 则新建
f = open(user_articles_without_new_words_record_path, 'wb')
list=[]
pickle.dump(list,f)
f.close()
fp=open(user_articles_without_new_words_record_path,'rb')
user_article_id_record=pickle.load(fp)
fp.close()
dic = {}
dic['article_id'] = article_id
dic['text_level'] = text_level
if dic not in user_article_id_record: #若已存在记录(即这篇文章之前被阅读过且没有添加生词,且已加入索引信息记录文件中),则不再重复加入,避免重复刷分
user_article_id_record.append(dic)
print(f"编号为{article_id}的文章阅读时间大于15秒且未有生词记录于ArticlesWithoutNewWords_<username>.pickle")
print(user_article_id_record)
f=open(user_articles_without_new_words_record_path,'wb')
pickle.dump(user_article_id_record,f)
f.close()
return "success"
@userService.route("/get_pre_article/<username>",methods=['GET']) @userService.route("/get_pre_article/<username>",methods=['GET'])
def get_pre_article(username): def get_pre_article(username):
user_articlesWithoutNewWords_record = path_prefix + 'static/frequency/' + 'ArticlesWithoutNewWords_%s.pickle' % (
username)
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username) user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
if request.method == 'GET': if request.method == 'GET':
visited_articles = session.get("visited_articles") visited_articles = session.get("visited_articles")
@ -92,7 +54,7 @@ def get_pre_article(username):
if visited_articles['article_ids'][-1] == "null": # 如果当前还是“null”则将“null”pop出来 if visited_articles['article_ids'][-1] == "null": # 如果当前还是“null”则将“null”pop出来
visited_articles['article_ids'].pop() visited_articles['article_ids'].pop()
session["visited_articles"] = visited_articles session["visited_articles"] = visited_articles
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'),user_articlesWithoutNewWords_record) visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
data = { data = {
'visited_articles': visited_articles, 'visited_articles': visited_articles,
'today_article': today_article, 'today_article': today_article,
@ -163,12 +125,11 @@ def userpage(username):
# 获取session里的用户名 # 获取session里的用户名
username = session.get('username') username = session.get('username')
user_articlesWithoutNewWords_record = path_prefix + 'static/frequency/' + 'ArticlesWithoutNewWords_%s.pickle' % (
username)
user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username) user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
if request.method == 'POST': # when we submit a form if request.method == 'POST': # when we submit a form
content = request.form['content'] content = escape(request.form['content'])
f = WordFreq(content) f = WordFreq(content)
lst = f.get_freq() lst = f.get_freq()
return render_template('userpage_post.html',username=username,lst = lst, yml=Yaml.yml) return render_template('userpage_post.html',username=username,lst = lst, yml=Yaml.yml)
@ -183,7 +144,7 @@ def userpage(username):
words = '' words = ''
for x in lst3: for x in lst3:
words += x[0] + ' ' words += x[0] + ' '
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'),user_articlesWithoutNewWords_record) visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
session['visited_articles'] = visited_articles session['visited_articles'] = visited_articles
# 通过 today_article加载前端的显示页面 # 通过 today_article加载前端的显示页面
return render_template('userpage_get.html', return render_template('userpage_get.html',
@ -215,11 +176,7 @@ def user_mark_word(username):
for word in request.form.getlist('marked'): for word in request.form.getlist('marked'):
lst.append((word, [get_time()])) lst.append((word, [get_time()]))
d = pickle_idea2.merge_frequency(lst, lst_history) d = pickle_idea2.merge_frequency(lst, lst_history)
if len(lst_history) > 999:
flash('You have way too many words in your difficult-words book. Delete some first.')
else:
pickle_idea2.save_frequency_to_pickle(d, user_freq_record) pickle_idea2.save_frequency_to_pickle(d, user_freq_record)
flash('Added %s.' % (', '.join(request.form.getlist('marked'))))
return redirect(url_for('user_bp.userpage', username=username)) return redirect(url_for('user_bp.userpage', username=username))
else: else:
return 'Under construction' return 'Under construction'

View File

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

View File

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