forked from mrlan/EnglishPal
Compare commits
2 Commits
Bug585-zha
...
Bug391-Liu
Author | SHA1 | Date |
---|---|---|
|
46ddf063cf | |
|
dafe1717eb |
|
@ -2,20 +2,11 @@
|
|||
venv/
|
||||
app/__init__.py
|
||||
app/__pycache__/
|
||||
.DS_Store
|
||||
app/.DS_Store
|
||||
app/sqlite_commands.py
|
||||
app/static/usr/*.jpg
|
||||
app/static/img/
|
||||
app/static/frequency/frequency_*.pickle
|
||||
app/static/frequency/frequency.p
|
||||
app/wordfreqapp.db
|
||||
app/db/wordfreqapp.db
|
||||
app/static/wordfreqapp.db
|
||||
app/static/donate-the-author.jpg
|
||||
app/static/donate-the-author-hidden.jpg
|
||||
app/model/__pycache__/
|
||||
app/test/__pycache__/
|
||||
app/test/.pytest_cache/
|
||||
app/test/pytest_report.html
|
||||
app/test/assets
|
||||
app/log.txt
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
FROM tiangolo/uwsgi-nginx-flask:python3.8-alpine
|
||||
COPY requirements.txt /tmp
|
||||
COPY ./app/ /app/
|
||||
RUN pip3 install -U pip -i https://mirrors.aliyun.com/pypi/simple/
|
||||
RUN pip3 install -r /tmp/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
|
||||
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
|
||||
|
|
|
@ -10,8 +10,8 @@ pipeline {
|
|||
stages {
|
||||
stage('MakeDatabasefile') {
|
||||
steps {
|
||||
sh 'touch ./app/wordfreqapp.db && rm -f ./app/wordfreqapp.db'
|
||||
sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/wordfreqapp.db'
|
||||
sh 'touch ./app/static/wordfreqapp.db && rm -f ./app/static/wordfreqapp.db'
|
||||
sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/static/wordfreqapp.db'
|
||||
}
|
||||
}
|
||||
stage('BuildIt') {
|
||||
|
|
98
README.md
98
README.md
|
@ -11,14 +11,15 @@ Hui Lan <hui.lan@cantab.net>
|
|||
|
||||
|
||||
EnglishPal allows the user to build his list of new English words
|
||||
picked from articles selected for him to read according his vocabulary level. EnglishPal will determine a user's vocabulary level based on his picked words. After that, it will recommend articles for him to read, in order to booster his English vocabulary furthermore.
|
||||
picked from articles selected for him according his vocabulary level.
|
||||
|
||||
|
||||
## Run on your own laptop
|
||||
## Run it on a local machine
|
||||
|
||||
|
||||
`python3 main.py`
|
||||
|
||||
Make sure you have put the SQLite database file in the path `app/static` (see below).
|
||||
Make sure you have the SQLite database file in `app/static` (see below).
|
||||
|
||||
|
||||
## Run it as a Docker container
|
||||
|
@ -28,32 +29,32 @@ Assuming that docker has been installed and that you are a sudo user (i.e., sudo
|
|||
|
||||
`sudo ./build.sh`
|
||||
|
||||
Open your favourite Internet browser and enter this URL address: `http://ip-address:90`. Note: you must update the variable `DEPLOYMENT_DIR` in `build.sh`.
|
||||
Open your favourite Internet browser and enter this URL address: `http://ip-address:90`.
|
||||
|
||||
### Explanation on the commands in build.sh
|
||||
|
||||
My steps for deploying English on a Ubuntu server.
|
||||
My steps for deploying English on the server.
|
||||
|
||||
- ssh to ubuntu@118.*.*.118
|
||||
|
||||
- cd to `/home/lanhui/englishpal2/EnglishPal`
|
||||
- cd to /home/lanhui/englishpal2/EnglishPal
|
||||
|
||||
- Stop all docker service: `sudo service docker restart`. If you know the docker container ID, then the above command is an overkill. Use the following command instead: `sudo docker stop ContainerID`. You could get all container IDs with the following command: `sudo docker ps`
|
||||
|
||||
- Rebuild container. Run the following command to rebuild a docker image each time after the source code gets updated: `sudo docker build -t englishpal .`
|
||||
- Rebuild container. Run the following command to rebuild a docker image after the code gets updated: `sudo docker build -t englishpal .`
|
||||
|
||||
- Run the application: `sudo docker run -d -p 90:80 -v /home/lanhui/englishpal2/EnglishPal/app/static/frequency:/app/static/frequency -t englishpal`. If you use `sudo docker run -d -p 90:80 -t englishpal`, data will be lost after terminating the program. If you want to automatically restart the docker image after each system reboot, add the option `--restart=always` after `docker run`.
|
||||
- Run the application: `sudo docker run -d -p 90:80 -v /home/lanhui/englishpal2/EnglishPal/app/static/frequency:/app/static/frequency -t englishpal`. If you use `sudo docker run -d -p 90:80 -t englishpal`, data will be lost after terminating the program.
|
||||
|
||||
- Save disk space: `sudo docker system prune -a -f`
|
||||
|
||||
`build.sh` contains all the above commands. Run "sudo ./build.sh" to rebuild and start the web application.
|
||||
- Save space: `sudo docker system prune -a -f`
|
||||
|
||||
|
||||
#### Other useful docker commands
|
||||
### Other useful docker commands
|
||||
|
||||
- `sudo docker ps -a`
|
||||
|
||||
- `sudo docker logs image_name`, where `image_name` could be obtained from `sudo docker ps`.
|
||||
- `sudo docker logs image_name`, where image_name could be obtained from `sudo docker ps`.
|
||||
|
||||
`build.sh` contains all the above commands. Run "sudo ./build.sh" to rebuild and run the web application.
|
||||
|
||||
|
||||
|
||||
|
@ -61,15 +62,11 @@ My steps for deploying English on a Ubuntu server.
|
|||
|
||||
|
||||
All articles are stored in the `article` table in a SQLite file called
|
||||
`app/db/wordfreqapp.db`.
|
||||
`app/static/wordfreqapp.db`.
|
||||
|
||||
### Adding new articles
|
||||
|
||||
To add articles, open and edit `app/db/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org).
|
||||
|
||||
### Extending an account's expiry date
|
||||
|
||||
By default, an account's expiry is 30 days after first sign-up. To extend account's expiry date, open and edit `user` table in `app/db/wordfreqapp.db`. Simply update field `expiry_date`.
|
||||
To add articles, open and edit `app/static/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org).
|
||||
|
||||
### Exporting the database
|
||||
|
||||
|
@ -95,31 +92,33 @@ sqlite3 wordfreqapp.db`. Delete wordfreqapp.db first if it exists.
|
|||
### Uploading wordfreqapp.db to the server
|
||||
|
||||
|
||||
`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/db/`
|
||||
`pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal/app/static`
|
||||
|
||||
|
||||
|
||||
## Feedback
|
||||
|
||||
We welcome feedback on EnglishPal. Feedback examples:
|
||||
We welcome feedback on EnglishPal.
|
||||
|
||||
### Feedback 1
|
||||
|
||||
- "Need a phone app. I use phone a lot. You cannot ask students to use computers."
|
||||
### Respondent 1
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
- “成为会员”改成“注册”
|
||||
“成为会员”改成“注册”
|
||||
|
||||
- “登出”改成“退出”
|
||||
“登出”改成“退出”
|
||||
|
||||
- “收集生词吧”改成“生词收集栏”
|
||||
“收集生词吧”改成“生词收集栏”
|
||||
|
||||
- 不要自动显示下一篇
|
||||
“不要自动显示下一篇”
|
||||
|
||||
- 需要有“上一篇”、“下一篇”按钮。
|
||||
需要有“上一篇”、“下一篇”
|
||||
|
||||
|
||||
|
||||
|
@ -129,28 +128,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.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -160,7 +137,7 @@ You may also want to use [webdriver-manager](https://pypi.org/project/webdriver-
|
|||
- Usability testing
|
||||
|
||||
|
||||
## Improvements made by contributors (incomplete list)
|
||||
## Improvements made by contributors
|
||||
|
||||
|
||||
### 朱文绮
|
||||
|
@ -182,6 +159,7 @@ too many words that they already know, on the other hand, it can
|
|||
reduce unnecessary memory occupied by the database, in addition, it
|
||||
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
|
||||
|
||||
|
||||
### 占健豪
|
||||
|
@ -203,16 +181,4 @@ Demo video link: https://b23.tv/QuB77m
|
|||
Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=215
|
||||
|
||||
|
||||
|
||||
|
||||
### 丁锐
|
||||
|
||||
修复了以下漏洞
|
||||
|
||||
漏洞:新用户在创建账号时,不需要输入确定密码也可以注册成功,并且新账户可以正常使用。
|
||||
|
||||
Bug report: http://118.25.96.118/bugzilla/show_bug.cgi?id=489
|
||||
|
||||
|
||||
*Last modified on 2023-01-30*
|
||||
|
||||
*Last modified on 2021-10-17*
|
136
app/Article.py
136
app/Article.py
|
@ -1,43 +1,25 @@
|
|||
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
|
||||
from model.article import get_all_articles, get_article_by_id, get_number_of_articles
|
||||
import logging
|
||||
import re
|
||||
path_prefix = './'
|
||||
db_path_prefix = './db/' # comment this line in deployment
|
||||
oxford_words_path='./db/oxford_words.txt'
|
||||
from difficulty import get_difficulty_level, text_difficulty_level, user_difficulty_level
|
||||
|
||||
def count_oxford_words(text, oxford_words):
|
||||
words = re.findall(r'\b\w+\b', text.lower())
|
||||
total_words = len(words)
|
||||
oxford_word_count = sum(1 for word in words if word in oxford_words)
|
||||
return oxford_word_count, total_words
|
||||
|
||||
def calculate_ratio(oxford_word_count, total_words):
|
||||
if total_words == 0:
|
||||
return 0
|
||||
return oxford_word_count / total_words
|
||||
path_prefix = '/var/www/wordfreq/wordfreq/'
|
||||
path_prefix = './' # comment this line in deployment
|
||||
|
||||
def load_oxford_words(file_path):
|
||||
oxford_words = {}
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
for line in file:
|
||||
parts = line.strip().split()
|
||||
word = parts[0]
|
||||
pos = parts[1]
|
||||
level = parts[2]
|
||||
oxford_words[word] = {'pos': pos, 'level': level}
|
||||
return oxford_words
|
||||
|
||||
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):
|
||||
|
@ -50,80 +32,43 @@ def get_article_body(s):
|
|||
return '\n'.join(lst)
|
||||
|
||||
|
||||
def get_today_article(user_word_list, visited_articles):
|
||||
if visited_articles is None:
|
||||
visited_articles = {
|
||||
"index" : 0, # 为 article_ids 的索引
|
||||
"article_ids": [] # 之前显示文章的id列表,越后越新
|
||||
}
|
||||
if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章,因此查找所有的文章
|
||||
result = get_all_articles()
|
||||
else: # 生成阅读过的文章,因此查询指定 article_id 的文章
|
||||
if visited_articles["article_ids"][visited_articles["index"]] == 'null': # 可能因为直接刷新页面导致直接去查询了'null',因此当刷新的页面的时候,需要直接进行“上一篇”操作
|
||||
visited_articles["index"] -= 1
|
||||
visited_articles["article_ids"].pop()
|
||||
article_id = visited_articles["article_ids"][visited_articles["index"]]
|
||||
result = get_article_by_id(article_id)
|
||||
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
|
||||
logging.debug('* get_today_article(): start d1 = ... ')
|
||||
d1 = load_freq_history(user_word_list)
|
||||
d1 = load_freq_history(path_prefix + 'static/frequency/frequency.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)
|
||||
logging.debug(' ... get_today_article(): done')
|
||||
|
||||
d = None
|
||||
result_of_generate_article = "not found"
|
||||
d3 = get_difficulty_level(d1, d2)
|
||||
|
||||
d = {}
|
||||
d_user = load_freq_history(user_word_list)
|
||||
logging.debug('* get_today_article(): user_difficulty_level() start')
|
||||
user_level = user_difficulty_level(d_user, d3) # more consideration as user's behaviour is dynamic. Time factor should be considered.
|
||||
logging.debug('* get_today_article(): done')
|
||||
text_level = 0
|
||||
if visited_articles["index"] > len(visited_articles["article_ids"])-1: # 生成新的文章
|
||||
amount_of_visited_articles = len(visited_articles["article_ids"])
|
||||
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"
|
||||
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)
|
||||
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 within_range(text_level, user_level, (8.0 - user_level) * factor):
|
||||
d = reading
|
||||
break
|
||||
|
||||
article_date = d['date']
|
||||
article_title = get_article_title(d['text'])
|
||||
article_body = get_article_body(d['text'])
|
||||
question_part = get_question_part(d['question'])
|
||||
answer_part = get_answer_part(d['question'])
|
||||
return user_level,text_level,article_date,article_title,article_body,question_part,answer_part
|
||||
|
||||
today_article = None
|
||||
if d:
|
||||
oxford_words = load_oxford_words(oxford_words_path)
|
||||
oxford_word_count, total_words = count_oxford_words(d['text'],oxford_words)
|
||||
ratio = calculate_ratio(oxford_word_count,total_words)
|
||||
today_article = {
|
||||
"user_level": '%4.1f' % user_level,
|
||||
"text_level": '%4.1f' % text_level,
|
||||
"date": d['date'],
|
||||
"article_title": get_article_title(d['text']),
|
||||
"article_body": get_article_body(d['text']),
|
||||
"source": d["source"],
|
||||
"question": get_question_part(d['question']),
|
||||
"answer": get_answer_part(d['question']),
|
||||
"ratio" : ratio
|
||||
}
|
||||
|
||||
return visited_articles, today_article, result_of_generate_article
|
||||
|
||||
|
||||
def load_freq_history(path):
|
||||
|
@ -150,7 +95,7 @@ def get_question_part(s):
|
|||
flag = 0
|
||||
elif flag == 1:
|
||||
result.append(line)
|
||||
return '\n'.join(result)
|
||||
return result
|
||||
|
||||
|
||||
def get_answer_part(s):
|
||||
|
@ -163,4 +108,5 @@ def get_answer_part(s):
|
|||
flag = 1
|
||||
elif flag == 1:
|
||||
result.append(line)
|
||||
return '\n'.join(result)
|
||||
# https://css-tricks.com/snippets/javascript/showhide-element/
|
||||
return result
|
136
app/Login.py
136
app/Login.py
|
@ -1,43 +1,39 @@
|
|||
import hashlib
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
import unicodedata
|
||||
|
||||
|
||||
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
|
||||
from datetime import datetime
|
||||
from UseSqlite import InsertQuery, RecordQuery
|
||||
|
||||
path_prefix = '/var/www/wordfreq/wordfreq/'
|
||||
path_prefix = './' # comment this line in deployment
|
||||
|
||||
|
||||
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
|
||||
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
|
||||
password = md5(username + password)
|
||||
rq.instructions_with_parameters("SELECT * FROM user WHERE name=:username AND password=:password", dict(
|
||||
username=username, password=password)) # the named style https://docs.python.org/3/library/sqlite3.html
|
||||
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 = (datetime.now() + timedelta(days=30)).strftime('%Y%m%d') # will expire after 30 days
|
||||
expiry_date = '20221230'
|
||||
# 将用户名和密码一起加密,以免暴露不同用户的相同密码
|
||||
password = md5(username + password)
|
||||
insert_user(username=username, password=password, start_date=start_date, expiry_date=expiry_date)
|
||||
rq = InsertQuery(path_prefix + 'static/wordfreqapp.db')
|
||||
rq.instructions_with_parameters("INSERT INTO user VALUES (:username, :password, :start_date, :expiry_date)", dict(
|
||||
username=username, password=password, start_date=start_date, expiry_date=expiry_date))
|
||||
rq.do_with_parameters()
|
||||
|
||||
|
||||
def check_username_availability(username):
|
||||
existed_user = get_user_by_username(username)
|
||||
return existed_user is None
|
||||
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
|
||||
rq.instructions_with_parameters(
|
||||
"SELECT * FROM user WHERE name=:username", dict(username=username))
|
||||
rq.do_with_parameters()
|
||||
result = rq.get_results()
|
||||
return result == []
|
||||
|
||||
|
||||
def change_password(username, old_password, new_password):
|
||||
|
@ -49,79 +45,33 @@ def change_password(username, old_password, new_password):
|
|||
:return: 修改成功:True 否则:False
|
||||
'''
|
||||
if not verify_user(username, old_password): # 旧密码错误
|
||||
return {'error':'Old password is wrong.', 'username':username}
|
||||
return False
|
||||
# 将用户名和密码一起加密,以免暴露不同用户的相同密码
|
||||
if new_password == old_password: #新旧密码一致
|
||||
return {'error':'New password cannot be the same as the old password.', 'username':username}
|
||||
update_password_by_username(username, new_password)
|
||||
return {'success':'Password changed', 'username':username}
|
||||
password = md5(username + new_password)
|
||||
rq = InsertQuery(path_prefix + 'static/wordfreqapp.db')
|
||||
rq.instructions_with_parameters("UPDATE user SET password=:password WHERE name=:username", dict(
|
||||
password=password, username=username))
|
||||
rq.do_with_parameters()
|
||||
return True
|
||||
|
||||
|
||||
def get_expiry_date(username):
|
||||
user = get_user_by_username(username)
|
||||
if user is None:
|
||||
return '20191024'
|
||||
rq = RecordQuery(path_prefix + 'static/wordfreqapp.db')
|
||||
rq.instructions_with_parameters(
|
||||
"SELECT expiry_date FROM user WHERE name=:username", dict(username=username))
|
||||
rq.do_with_parameters()
|
||||
result = rq.get_results()
|
||||
if len(result) > 0:
|
||||
return result[0]['expiry_date']
|
||||
else:
|
||||
return user.expiry_date
|
||||
return '20191024'
|
||||
|
||||
|
||||
class UserName:
|
||||
def __init__(self, username):
|
||||
self.username = username
|
||||
|
||||
def contains_chinese(self):
|
||||
for char in self.username:
|
||||
# Check if the character is in the CJK (Chinese, Japanese, Korean) Unicode block
|
||||
if unicodedata.name(char).startswith('CJK UNIFIED IDEOGRAPH'):
|
||||
return True
|
||||
return False
|
||||
|
||||
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.'
|
||||
if self.contains_chinese():
|
||||
return 'Chinese characters are not allowed in the user name.'
|
||||
return 'OK'
|
||||
|
||||
|
||||
class Password:
|
||||
def __init__(self, password):
|
||||
self.password = password
|
||||
|
||||
def contains_chinese(self):
|
||||
for char in self.password:
|
||||
# Check if the character is in the CJK (Chinese, Japanese, Korean) Unicode block
|
||||
if unicodedata.name(char).startswith('CJK UNIFIED IDEOGRAPH'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def validate(self):
|
||||
if len(self.password) < 4:
|
||||
return 'Password must be at least 4 characters long.'
|
||||
if ' ' in self.password:
|
||||
return 'Password cannot contain spaces.'
|
||||
if self.contains_chinese():
|
||||
return 'Chinese characters are not allowed in the password.'
|
||||
return 'OK'
|
||||
|
||||
|
||||
class WarningMessage:
|
||||
def __init__(self, s, type='username'):
|
||||
self.s = s
|
||||
self.type = type
|
||||
|
||||
def __str__(self):
|
||||
if self.type == 'username':
|
||||
return UserName(self.s).validate()
|
||||
if self.type == 'password':
|
||||
return Password(self.s).validate()
|
||||
def md5(s):
|
||||
'''
|
||||
MD5摘要
|
||||
:param str: 字符串
|
||||
:return: 经MD5以后的字符串
|
||||
'''
|
||||
h = hashlib.md5(s.encode(encoding='utf-8'))
|
||||
return h.hexdigest()
|
||||
|
|
|
@ -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())
|
|
@ -1,10 +1,10 @@
|
|||
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
|
||||
|
||||
# 初始化蓝图
|
||||
accountService = Blueprint("accountService", __name__)
|
||||
|
||||
|
||||
### Sign-up, login, logout ###
|
||||
@accountService.route("/signup", methods=['GET', 'POST'])
|
||||
def signup():
|
||||
|
@ -19,15 +19,13 @@ def signup():
|
|||
# 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'})
|
||||
flash('用户名 %s 已经被注册。' % (username))
|
||||
return render_template('signup.html')
|
||||
elif len(password.strip()) < 4: # 密码过短
|
||||
return '密码过于简单。'
|
||||
else: # 添加账户信息
|
||||
add_user(username, password)
|
||||
verified = verify_user(username, password)
|
||||
|
@ -37,10 +35,11 @@ def signup():
|
|||
session[username] = username
|
||||
session['username'] = username
|
||||
session['expiry_date'] = get_expiry_date(username)
|
||||
session['visited_articles'] = None
|
||||
return jsonify({'status': '2'})
|
||||
session['articleID'] = None
|
||||
return '<p>恭喜,你已成功注册, 你的用户名是 <a href="%s">%s</a>。</p>\
|
||||
<p><a href="/%s">开始使用</a> <a href="/">返回首页</a><p/>' % (username, username, username)
|
||||
else:
|
||||
return jsonify({'status': '1'})
|
||||
return '用户名密码验证失败。'
|
||||
|
||||
|
||||
@accountService.route("/login", methods=['GET', 'POST'])
|
||||
|
@ -51,74 +50,43 @@ def login():
|
|||
'''
|
||||
if request.method == 'GET':
|
||||
# GET请求
|
||||
return render_template('login.html')
|
||||
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':
|
||||
# POST方法用于判断登录是否成功
|
||||
# check database and verify user
|
||||
username = escape(request.form['username'])
|
||||
password = escape(request.form['password'])
|
||||
verified = verify_user(username, password)
|
||||
#读black.txt文件判断用户是否在黑名单中
|
||||
with open('black.txt') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if username == line:
|
||||
return jsonify({'status': '5'})
|
||||
with open('black.txt', 'a+') as f:
|
||||
f.seek(0)
|
||||
lines = f.readlines()
|
||||
line=[]
|
||||
for i in lines:
|
||||
line.append(i.strip('\n'))
|
||||
#读black.txt文件判断用户是否在黑名单中
|
||||
if verified and username not in line: #TODO: 一个用户名是另外一个用户名的子串怎么办?
|
||||
# 登录成功,写入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
|
||||
f.close()
|
||||
return jsonify({'status': '1'})
|
||||
elif verified==0 and password!='黑名单':
|
||||
#输入错误密码次数小于5次
|
||||
return jsonify({'status': '0'})
|
||||
else:
|
||||
#输入错误密码次数达到5次
|
||||
with open('black.txt', 'a+') as f:
|
||||
f.seek(0)
|
||||
lines = f.readlines()
|
||||
line = []
|
||||
for i in lines:
|
||||
line.append(i.strip('\n'))
|
||||
if username in line:
|
||||
return jsonify({'status': '5'})
|
||||
else:
|
||||
f.write(username)
|
||||
f.write('\n')
|
||||
return jsonify({'status': '5'})
|
||||
|
||||
|
||||
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['articleID'] = None
|
||||
return redirect(url_for('user_bp.userpage', username=username))
|
||||
else:
|
||||
return '无法通过验证。'
|
||||
|
||||
|
||||
@accountService.route("/logout", methods=['GET', 'POST'])
|
||||
# def logout():
|
||||
# '''
|
||||
# 登出
|
||||
# :return: 重定位到主界面
|
||||
# '''
|
||||
# # 将session标记为登出状态
|
||||
# session['logged_in'] = False
|
||||
# return redirect(url_for('mainpage'))
|
||||
|
||||
# 使用session.clear()替代部分字段删除.确保完全退出
|
||||
def logout():
|
||||
session.clear() # 彻底清除会话
|
||||
'''
|
||||
登出
|
||||
:return: 重定位到主界面
|
||||
'''
|
||||
# 将session标记为登出状态
|
||||
session['logged_in'] = False
|
||||
return redirect(url_for('mainpage'))
|
||||
|
||||
|
||||
|
||||
@accountService.route("/reset", methods=['GET', 'POST'])
|
||||
def reset():
|
||||
'''
|
||||
|
@ -138,7 +106,24 @@ def reset():
|
|||
# POST请求用于提交修改后信息
|
||||
old_password = escape(request.form['old-password'])
|
||||
new_password = escape(request.form['new-password'])
|
||||
result = change_password(username, old_password, new_password)
|
||||
return jsonify(result)
|
||||
flag = change_password(username, old_password, new_password) # flag表示是否修改成功
|
||||
if flag:
|
||||
session['logged_in'] = False
|
||||
return \
|
||||
'''
|
||||
<script>
|
||||
alert('密码修改成功,请重新登录。');
|
||||
window.location.href="/login";
|
||||
</script>
|
||||
|
||||
'''
|
||||
|
||||
else:
|
||||
return \
|
||||
'''
|
||||
<script>
|
||||
alert('密码修改失败');
|
||||
window.location.href="/reset";
|
||||
</script>
|
||||
|
||||
'''
|
|
@ -1,154 +0,0 @@
|
|||
# System Library
|
||||
from flask import *
|
||||
from markupsafe import escape
|
||||
|
||||
# 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():
|
||||
|
||||
def _make_title_and_content(article_lst):
|
||||
for article in article_lst:
|
||||
text = escape(article.text) # Fix XSS vulnerability, contributed by Xu Xuan
|
||||
article.title = text.split("\n")[0]
|
||||
article.content = '<br/>'.join(text.split("\n")[1:])
|
||||
|
||||
|
||||
def _update_context():
|
||||
article_len = get_number_of_articles()
|
||||
context["article_number"] = article_len
|
||||
context["text_list"] = get_page_articles(_cur_page, _page_size)
|
||||
_articles = get_page_articles(_cur_page, _page_size)
|
||||
_make_title_and_content(_articles)
|
||||
context["text_list"] = _articles
|
||||
|
||||
global _cur_page, _page_size
|
||||
|
||||
is_admin = check_is_admin()
|
||||
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 parameters must be integer!"
|
||||
|
||||
_articles = get_page_articles(_cur_page, _page_size)
|
||||
_make_title_and_content(_articles)
|
||||
|
||||
context = {
|
||||
"article_number": _article_number,
|
||||
"text_list": _articles,
|
||||
"page_size": _page_size,
|
||||
"cur_page": _cur_page,
|
||||
"username": session.get("username"),
|
||||
}
|
||||
|
||||
if request.method == "POST":
|
||||
data = request.form
|
||||
|
||||
if "delete_id" in data:
|
||||
try:
|
||||
delete_id = int(data["delete_id"]) # 转成int型
|
||||
delete_article_by_id(delete_id) # 根据id删除article
|
||||
flash(f'Article ID {delete_id} deleted successfully.') # 刷新页首提示语
|
||||
_update_context()
|
||||
except ValueError:
|
||||
flash('Invalid article ID for deletion.') # 刷新页首提示语
|
||||
|
||||
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)
|
||||
title = content.split('\n')[0]
|
||||
flash(f'Article added. Title: {title}')
|
||||
_update_context() # 这行应在flash之后 否则会发生新建的文章即点即删
|
||||
|
||||
return render_template("admin_manage_article.html", **context)
|
||||
|
||||
#引入 flask_wtf.csrf.CSRFProtect 防止跨站请求伪造。
|
||||
# @adminService.route("/admin/user", methods=["POST"])
|
||||
# def update_user():
|
||||
# # 添加CSRF保护(需配合Flask-WTF或Flask-SeaSurf)
|
||||
# if not validate_csrf(request.form.get("csrf_token")):
|
||||
# return "Invalid CSRF token", 403
|
||||
|
||||
@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
|
|
@ -1,31 +0,0 @@
|
|||
from flask import *
|
||||
from flask_httpauth import HTTPTokenAuth
|
||||
from Article import load_freq_history
|
||||
|
||||
path_prefix = '/var/www/wordfreq/wordfreq/'
|
||||
path_prefix = './' # comment this line in deployment
|
||||
|
||||
apiService = Blueprint('site',__name__)
|
||||
|
||||
auth = HTTPTokenAuth(scheme='Bearer')
|
||||
|
||||
tokens = {
|
||||
"token": "token",
|
||||
"secret-token": "lanhui" # token, username
|
||||
}
|
||||
|
||||
|
||||
@auth.verify_token
|
||||
def verify_token(token):
|
||||
if token in tokens:
|
||||
return tokens[token]
|
||||
|
||||
|
||||
@apiService.route('/api/mywords') # HTTPie usage: http -A bearer -a secret-token http://127.0.0.1:5000/api/mywords
|
||||
@auth.login_required
|
||||
def show():
|
||||
username = auth.current_user()
|
||||
word_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username)
|
||||
d = load_freq_history(word_freq_record)
|
||||
return jsonify(d)
|
||||
|
|
@ -1 +0,0 @@
|
|||
hsy
|
|
@ -1 +0,0 @@
|
|||
Put wordfreqapp.db here
|
File diff suppressed because it is too large
Load Diff
|
@ -7,8 +7,7 @@
|
|||
|
||||
import pickle
|
||||
import math
|
||||
from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order, map_percentages_to_levels
|
||||
import snowballstemmer
|
||||
from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order
|
||||
|
||||
|
||||
def load_record(pickle_fname):
|
||||
|
@ -18,58 +17,41 @@ def load_record(pickle_fname):
|
|||
return d
|
||||
|
||||
|
||||
ENGLISH_WORD_DIFFICULTY_DICT = {}
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
level = min(level, 8)
|
||||
return level
|
||||
|
||||
global ENGLISH_WORD_DIFFICULTY_DICT
|
||||
ENGLISH_WORD_DIFFICULTY_DICT = result
|
||||
|
||||
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.
|
||||
if ENGLISH_WORD_DIFFICULTY_DICT == {}:
|
||||
d2 = convert_test_type_to_difficulty_level(d2) # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...}
|
||||
else:
|
||||
d2 = ENGLISH_WORD_DIFFICULTY_DICT
|
||||
|
||||
stemmer = snowballstemmer.stemmer('english')
|
||||
|
||||
for k in d1: # 用户的词
|
||||
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
|
||||
return d
|
||||
|
||||
|
||||
|
||||
def revert_dict(d):
|
||||
'''
|
||||
|
@ -80,13 +62,12 @@ def revert_dict(d):
|
|||
for k in d:
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
|
@ -94,73 +75,43 @@ def revert_dict(d):
|
|||
return d2
|
||||
|
||||
|
||||
def user_difficulty_level(d_user, d, calc_func=0):
|
||||
'''
|
||||
two ways to calculate difficulty_level
|
||||
set calc_func!=0 to use sqrt, otherwise use weighted average
|
||||
'''
|
||||
if calc_func != 0:
|
||||
# calculation function 1: sqrt
|
||||
d_user2 = revert_dict(d_user) # key is date, and value is a list of words added in that date
|
||||
geometric = 0
|
||||
count = 0
|
||||
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)
|
||||
for t in lst3:
|
||||
word = t[0]
|
||||
hard = t[1]
|
||||
# print('WORD %s HARD %4.2f' % (word, hard))
|
||||
geometric = geometric + math.log(hard)
|
||||
count += 1
|
||||
return math.exp(geometric / max(count, 1))
|
||||
|
||||
# calculation function 2: weighted average
|
||||
d_user2 = revert_dict(d_user) # key is date, and value is a list of words added in that date
|
||||
count = {} # number of all kinds of words
|
||||
percentages = {} # percentages of all kinds of difficulties
|
||||
total = 0 # total words
|
||||
for date in d_user2.keys():
|
||||
lst = d_user2[date] # a list of words
|
||||
for word in lst:
|
||||
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
|
||||
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:
|
||||
if word in d:
|
||||
if d[word] not in count:
|
||||
count[d[word]] = 0
|
||||
count[d[word]] += 1
|
||||
total += 1
|
||||
lst2.append((word, d[word]))
|
||||
|
||||
if total == 0:
|
||||
return 1
|
||||
for k in count.keys():
|
||||
percentages[k] = count[k] / total
|
||||
weight = map_percentages_to_levels(percentages)
|
||||
sum = 0
|
||||
for k in weight.keys():
|
||||
sum += weight[k] * k
|
||||
return sum
|
||||
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))
|
||||
geometric = geometric * (hard)
|
||||
count += 1
|
||||
if count >= 10:
|
||||
return geometric**(1/count)
|
||||
|
||||
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)
|
||||
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
|
||||
lst = [] # a list of tuples, each tuple being (word, difficulty level)
|
||||
for x in L:
|
||||
word = x[0]
|
||||
if word not in stop_words and word in d:
|
||||
if 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:
|
||||
|
@ -168,20 +119,24 @@ 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)
|
||||
if count >= 20: # we look for n most difficult words
|
||||
return geometric**(1/count)
|
||||
|
||||
return geometric**(1/max(count,1))
|
||||
|
||||
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_for_user(d1, d2)
|
||||
|
||||
d3 = get_difficulty_level(d1, d2)
|
||||
|
||||
s = '''
|
||||
South Lawn
|
||||
|
@ -242,6 +197,7 @@ 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)
|
||||
|
||||
|
@ -262,6 +218,7 @@ 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. We’ll have a few questions after grace. And, if you would, Ben, please do the honors.
|
||||
|
||||
|
@ -276,11 +233,17 @@ 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))
|
||||
|
||||
|
||||
|
||||
|
|
Binary file not shown.
61
app/main.py
61
app/main.py
|
@ -1,33 +1,27 @@
|
|||
#! /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 abort, jsonify
|
||||
from markupsafe import escape
|
||||
from collections import Counter
|
||||
|
||||
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
|
||||
from api_service import apiService
|
||||
import os
|
||||
from translate import *
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.urandom(32)
|
||||
app.secret_key = 'lunch.time!'
|
||||
|
||||
# 将蓝图注册到Lab app
|
||||
app.register_blueprint(userService)
|
||||
app.register_blueprint(accountService)
|
||||
app.register_blueprint(adminService)
|
||||
app.register_blueprint(apiService)
|
||||
|
||||
path_prefix = '/var/www/wordfreq/wordfreq/'
|
||||
path_prefix = './' # comment this line in deployment
|
||||
|
||||
|
||||
def get_random_image(path):
|
||||
'''
|
||||
返回随机图
|
||||
|
@ -44,7 +38,8 @@ def get_random_ads():
|
|||
返回随机广告
|
||||
:return: 一个广告(包含HTML标签)
|
||||
'''
|
||||
return random.choice(['个性化分析精准提升', '你的专有单词本', '智能捕捉阅读弱点,针对性提高你的阅读水平'])
|
||||
ads = random.choice(['个性化分析精准提升', '你的专有单词本', '智能捕捉阅读弱点,针对性提高你的阅读水平'])
|
||||
return ads + '。 <a href="/signup">试试</a>吧!'
|
||||
|
||||
|
||||
def appears_in_test(word, d):
|
||||
|
@ -60,11 +55,6 @@ def appears_in_test(word, d):
|
|||
return ','.join(d[word])
|
||||
|
||||
|
||||
def good_word(word):
|
||||
return len(word) < len('Pneumonoultramicroscopicsilicovolcanoconiosis') \
|
||||
and Counter(word).most_common(1)[0][1] <= 4
|
||||
|
||||
|
||||
@app.route("/mark", methods=['GET', 'POST'])
|
||||
def mark_word():
|
||||
'''
|
||||
|
@ -90,23 +80,10 @@ def mainpage():
|
|||
根据GET或POST方法来返回不同的主界面
|
||||
:return: 主界面
|
||||
'''
|
||||
|
||||
article_text = get_all_articles()
|
||||
texts = [item['text'] for item in article_text]
|
||||
oxford_words = load_oxford_words(oxford_words_path)
|
||||
|
||||
# 提取所有单词
|
||||
all_words = []
|
||||
for text in texts:
|
||||
words = re.findall(r'\b\w+\b', text.lower())
|
||||
all_words.extend(words)
|
||||
oxford_word_count = sum(1 for word in all_words if word in oxford_words)
|
||||
ratio = calculate_ratio(oxford_word_count, len(all_words))
|
||||
|
||||
if request.method == 'POST': # when we submit a form
|
||||
content = escape(request.form['content'])
|
||||
content = request.form['content']
|
||||
f = WordFreq(content)
|
||||
lst = [ t for t in f.get_freq() if good_word(t[0]) ] # only keep normal words
|
||||
lst = f.get_freq()
|
||||
# save history
|
||||
d = load_freq_history(path_prefix + 'static/frequency/frequency.p')
|
||||
lst_history = pickle_idea.dict2lst(d)
|
||||
|
@ -120,23 +97,9 @@ def mainpage():
|
|||
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,
|
||||
ratio = ratio)
|
||||
return render_template('mainpage_get.html', random_ads=random_ads, number_of_essays=number_of_essays,
|
||||
d_len=d_len, lst=lst, yml=Yaml.yml)
|
||||
|
||||
@app.route("/translate", methods=['POST'])
|
||||
def translate_word():
|
||||
data = request.get_json()
|
||||
word = data.get('word', '')
|
||||
from_lang = data.get('from_lang', 'en') # 假设默认源语言是英语
|
||||
to_lang = data.get('to_lang', 'zh') # 假设默认目标语言是中文
|
||||
result = translate(word, from_lang, to_lang)
|
||||
return jsonify({'translation': result})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
from pony.orm import *
|
||||
|
||||
db = Database()
|
||||
db.bind("sqlite", "../db/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
|
|
@ -1,48 +0,0 @@
|
|||
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)
|
||||
]
|
||||
|
||||
|
||||
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()]
|
|
@ -1,30 +0,0 @@
|
|||
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
|
|
@ -6,7 +6,6 @@
|
|||
# Purpose: dictionary & pickle as a simple means of database.
|
||||
# Task: incorporate the functions into wordfreqCMD.py such that it will also show cumulative frequency.
|
||||
|
||||
import os
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
|
||||
|
@ -56,13 +55,11 @@ def save_frequency_to_pickle(d, pickle_fname):
|
|||
f.close()
|
||||
|
||||
def unfamiliar(path,word):
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
with open(path,"rb") as f:
|
||||
dic = pickle.load(f)
|
||||
dic[word] += [datetime.now().strftime('%Y%m%d%H%M')]
|
||||
with open(path,"wb") as fp:
|
||||
pickle.dump(dic,fp)
|
||||
f = open(path,"rb")
|
||||
dic = pickle.load(f)
|
||||
dic[word] += [datetime.now().strftime('%Y%m%d%H%M')]
|
||||
fp = open(path,"wb")
|
||||
pickle.dump(dic,fp)
|
||||
|
||||
def familiar(path,word):
|
||||
f = open(path,"rb")
|
||||
|
|
|
@ -64,15 +64,15 @@ def load_record(pickle_fname):
|
|||
|
||||
def save_frequency_to_pickle(d, pickle_fname):
|
||||
f = open(pickle_fname, 'wb')
|
||||
exclusion_lst = ['one', 'no', 'has', 'had', 'do', 'that', 'have', 'by', 'not', 'but', 'we', 'this', 'my', 'him', 'so', 'or', 'as', 'are', 'it', 'from', 'with', 'be', 'can', 'for', 'an', 'if', 'who', 'whom', 'whose', 'which', 'the', 'to', 'a', 'of', 'and', 'you', 'i', 'he', 'she', 'they', 'me', 'was', 'were', 'is', 'in', 'at', 'on', 'their', 'his', 'her', 's', 'said', 'all', 'did', 'been', 'w']
|
||||
d2 = {}
|
||||
for k in d:
|
||||
if not k in exclusion_lst and not k.isnumeric() and not len(k) < 2:
|
||||
d2[k] = list(sorted(d[k])) # 原先这里是d2[k] = list(sorted(set(d[k])))
|
||||
d2[k] = list(sorted(set(d[k])))
|
||||
pickle.dump(d2, f)
|
||||
f.close()
|
||||
|
||||
|
||||
exclusion_lst = ['one', 'no', 'has', 'had', 'do', 'that', 'have', 'by', 'not', 'but', 'we', 'this', 'my', 'him', 'so', 'or', 'as', 'are', 'it', 'from', 'with', 'be', 'can', 'for', 'an', 'if', 'who', 'whom', 'whose', 'which', 'the', 'to', 'a', 'of', 'and', 'you', 'i', 'he', 'she', 'they', 'me', 'was', 'were', 'is', 'in', 'at', 'on', 'their', 'his', 'her', 's', 'said', 'all', 'did', 'been', 'w']
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
# 全局引入的css文件地址
|
||||
css:
|
||||
item:
|
||||
- ../static/css/bootstrap.css
|
||||
- ../static/css/highlighted.css
|
||||
- static/css/bootstrap.css
|
||||
|
||||
# 全局引入的js文件地址
|
||||
js:
|
||||
head: # 在页面加载之前加载
|
||||
- ../static/js/jquery.js
|
||||
- ../static/js/read.js
|
||||
- ../static/js/word_operation.js
|
||||
- ../static/js/checkboxes.js
|
||||
# - static/js/APlayer.js
|
||||
# - static/js/Meting.js
|
||||
bottom: # 在页面加载完之后加载
|
||||
- ../static/js/fillword.js
|
||||
- ../static/js/highlight.js
|
||||
- static/js/fillword.js
|
||||
- static/js/highlight.js
|
||||
|
||||
# 高亮样式,目前仅支持修改颜色
|
||||
highlight:
|
||||
|
|
|
@ -417,7 +417,7 @@ progress {
|
|||
}
|
||||
|
||||
.lead {
|
||||
font-size: 2rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 300
|
||||
}
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/* 按钮(上一篇,下一篇)样式 */
|
||||
.pagination {
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #007BFF; /* 按钮背景颜色 */
|
||||
border: none;
|
||||
color: #FFF; /* 按钮文字颜色 */
|
||||
padding: 5px 12px;
|
||||
font-size: 20px;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.arrow i {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
background-color: #0056b3; /* 按钮悬停时的背景颜色 */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 为首页时按钮(Pre Article)的背景颜色 */
|
||||
.gray-background {
|
||||
background-color: #6c757d !important;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
.highlighted {
|
||||
color: red;
|
||||
font-weight: normal;
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
/*样式应用于login、signup、reset三个页面*/
|
||||
|
||||
.container {
|
||||
background-color: #FFFFFF;
|
||||
width: 400px;
|
||||
height: 500px;
|
||||
margin: 7em auto;
|
||||
border-radius: 1.5em;
|
||||
box-shadow: 0px 11px 35px 2px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
/*增加一个类reset-heading*/
|
||||
.signin-heading, .reset-heading {
|
||||
padding-top: 5px;
|
||||
color: #8C55AA;
|
||||
font-family: 'Ubuntu', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 23px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*增加2个类.old-password和.new-password*/
|
||||
.username, .email, .password, .re-password, .old-password, .new-password,.re-new-password {
|
||||
width: 76%;
|
||||
color: rgb(38, 50, 56);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
background: rgba(136, 126, 126, 0.04);
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid rgba(124, 16, 97, 0.02);
|
||||
margin-bottom: 50px;
|
||||
margin-left: 46px;
|
||||
text-align: center;
|
||||
margin-bottom: 27px;
|
||||
font-family: 'Ubuntu', sans-serif;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 50%;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid #8C55AA;
|
||||
margin-bottom: 50px;
|
||||
margin-left: 90px;
|
||||
padding: 10px 20px;
|
||||
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
|
||||
background: #8C55AA;
|
||||
transition: .5s;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.signup {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
left: 65%;
|
||||
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
list-style: none;
|
||||
font-weight: bold;
|
||||
font-family: 'ink free';
|
||||
|
||||
}
|
||||
|
||||
.main_menu a {
|
||||
color: #fff;
|
||||
font-size: 300px;
|
||||
}
|
||||
|
||||
li :hover {
|
||||
color: #8C55AA;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'ink free';
|
||||
|
||||
}
|
||||
|
||||
.main_menu h1 {
|
||||
color: #fff;
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
|
||||
#slider {
|
||||
margin: 20px auto;
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
background-color: #dae2d0;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
#slider_bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background-color: #7AC23C;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#label {
|
||||
width: 46px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
border: 1px solid #cccccc;
|
||||
background: #fff;
|
||||
z-index: 3;
|
||||
cursor: move;
|
||||
color: #ff9e77;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
#labelTip {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 13px;
|
||||
font-family: 'Microsoft Yahei', serif;
|
||||
color: #787878;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
This folder holds users' vocabulary files.
|
||||
Each file ends with .pickle.
|
||||
For example, mrlan.pickle is the vocabulary file for user mrlan.
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
function toggleCheckboxSelection(checkStatus) {
|
||||
// used in userpage_post.html
|
||||
const checkBoxes = document.getElementsByName('marked');
|
||||
checkBoxes.forEach((checkbox) => { checkbox.checked = checkStatus;} );
|
||||
}
|
|
@ -1,52 +1,29 @@
|
|||
let isRead = localStorage.getItem('readChecked') !== 'false'; // default to true
|
||||
let isChoose = localStorage.getItem('chooseChecked') !== 'false';
|
||||
isRead = true;
|
||||
isChoose = true;
|
||||
var reader = window.speechSynthesis; // 全局定义朗读者,以便朗读和暂停
|
||||
|
||||
function getWord() {
|
||||
return window.getSelection ? window.getSelection() : document.selection.createRange().text;
|
||||
function getWord(){
|
||||
var word = window.getSelection?window.getSelection():document.selection.createRange().text;
|
||||
return word;
|
||||
}
|
||||
|
||||
function fillInWord() {
|
||||
let word = getWord();
|
||||
if (isRead) Reader.read(word, inputSlider.value);
|
||||
if (!isChoose) {
|
||||
if(isHighlight){
|
||||
const element = document.getElementById("selected-words3");
|
||||
element.value = element.value + " " + word;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const element = document.getElementById("selected-words");
|
||||
localStorage.setItem('nowWords', element.value);
|
||||
element.value = element.value + " " + word;
|
||||
localStorage.setItem('selectedWords', element.value);
|
||||
function fillinWord(){
|
||||
var word = getWord();
|
||||
if (isRead) read(word);
|
||||
if (!isChoose) return;
|
||||
var element = document.getElementById("selected-words");
|
||||
element.value = element.value + " " + word;
|
||||
}
|
||||
|
||||
if (document.getElementById("text-content")) {
|
||||
document.getElementById("text-content").addEventListener("click", fillInWord, false);
|
||||
document.getElementById("text-content").addEventListener("click", fillinWord, false);
|
||||
function read(s){
|
||||
var msg = new SpeechSynthesisUtterance(s);
|
||||
reader.speak(msg);
|
||||
}
|
||||
|
||||
const sliderValue = document.getElementById("rangeValue");
|
||||
const inputSlider = document.getElementById("rangeComponent");
|
||||
|
||||
if (inputSlider) {
|
||||
inputSlider.oninput = () => {
|
||||
let value = inputSlider.value;
|
||||
sliderValue.textContent = value + '×';
|
||||
};
|
||||
}
|
||||
|
||||
function onReadClick() {
|
||||
function onReadClick(){
|
||||
isRead = !isRead;
|
||||
localStorage.setItem('readChecked', isRead);
|
||||
if(!isRead){
|
||||
reader.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
function onChooseClick() {
|
||||
function onChooseClick(){
|
||||
isChoose = !isChoose;
|
||||
localStorage.setItem('chooseChecked', isChoose);
|
||||
}
|
||||
|
||||
// 如果网页刷新,停止播放声音
|
||||
if (performance.getEntriesByType("navigation")[0].type == "reload") {
|
||||
Reader.stopRead();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,196 +1,95 @@
|
|||
let isHighlight = localStorage.getItem('highlightChecked') !== 'false'; // default to true
|
||||
var 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);
|
||||
cancel_highLight();
|
||||
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() {
|
||||
if (document.getElementById("text-content")) {
|
||||
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 replaceWords(str, word) {
|
||||
let count = 0;
|
||||
|
||||
const regex = new RegExp(`(^|\\s)${word}(?=\\s|$)`, 'g');
|
||||
|
||||
let result = str.replace(regex, (match, p1) => {
|
||||
count++;
|
||||
// p1 保留前导空格(如果有),仅第一个匹配保留,后续匹配替换为空字符串
|
||||
return count === 1 ? match : p1;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function countWords(str, word) {
|
||||
// 使用正则表达式匹配目标单词的整个单词边界情况,包括前后空格、行首和行尾
|
||||
const regex = new RegExp(`(^|\\s)${word}(?=\\s|$)`, 'g');
|
||||
let match;
|
||||
let count = 0;
|
||||
|
||||
// 迭代匹配所有符合条件的单词
|
||||
while ((match = regex.exec(str)) !== null) {
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
//用于替换单词
|
||||
function replaceAllWords(str, word, replacement) {
|
||||
const regex = new RegExp(`(^|\\s)${word}(?=\\s|$)`, 'gi');
|
||||
let result = str.replace(regex, (match, p1) => {
|
||||
return p1 + replacement;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getWord() {
|
||||
return window.getSelection ? window.getSelection().toString() : document.selection.createRange().text;
|
||||
}
|
||||
|
||||
function highLight() {
|
||||
if (!isHighlight) return;
|
||||
let word = (getWord() + "").trim().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
||||
let articleContent = document.getElementById("article").innerHTML; // innerHTML保留HTML标签来保持部分格式,且适配不同的浏览器
|
||||
let pickedWords = document.getElementById("selected-words"); // words picked to the text area
|
||||
let dictionaryWords = document.getElementById("selected-words2"); // words appearing in the user's new words list
|
||||
let allWords = dictionaryWords === null ? pickedWords.value + " " : pickedWords.value + " " + dictionaryWords.value;
|
||||
let highlightWords = document.getElementById("selected-words3");
|
||||
allWords = highlightWords == null ? allWords : allWords + " " + highlightWords.value;
|
||||
const list = allWords.split(" "); // 将所有的生词放入一个list中
|
||||
if(word !== null && word !== "" && word !== " "){
|
||||
if(localStorage.getItem("nowWords").indexOf(word) !== -1 || localStorage.getItem("nowWords").indexOf(word.toLowerCase()) !== -1){
|
||||
articleContent = articleContent.replace(new RegExp('<span class="highlighted">' + word + '</span>', "g"), word);
|
||||
|
||||
let count=countWords(pickedWords.value,word)
|
||||
let currentWords=localStorage.getItem("nowWords")+" "+word
|
||||
localStorage.setItem("nowWords",currentWords)
|
||||
//
|
||||
if(count>0){
|
||||
if(count==1){
|
||||
localStorage.setItem("nowWords",replaceWords(currentWords,word))
|
||||
}else{
|
||||
localStorage.setItem("nowWords",replaceAllWords(currentWords,word,""))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pickedWords.value = localStorage.getItem("nowWords")
|
||||
document.getElementById("article").innerHTML = articleContent;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let totalSet = new Set();
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
list[i] = list[i].replace(/(^\W*)|(\W*$)/g, ""); // 消除单词两边的非单词字符
|
||||
list[i] = list[i].replace('|', "");
|
||||
list[i] = list[i].replace('?', "");
|
||||
if (list[i] != "" && !totalSet.has(list[i])) {
|
||||
// 返回所有匹配单词的集合, 正则表达式RegExp()中, "\b"匹配一个单词的边界, g 表示全局匹配, i 表示对大小写不敏感。
|
||||
let matches = new Set(articleContent.match(new RegExp("\\b" + list[i] + "\\b", "gi")));
|
||||
totalSet = new Set([...totalSet, ...matches]);
|
||||
}
|
||||
}
|
||||
// 删除所有的"<span class='highlighted'>"标签,防止标签发生嵌套
|
||||
articleContent = articleContent.replace(new RegExp('<span class="highlighted">',"gi"), "")
|
||||
articleContent = articleContent.replace(new RegExp("</span>","gi"), "");
|
||||
// 将文章中所有出现该单词word的地方改为:"<span class='highlighted'>" + word + "</span>"。
|
||||
for (let word of totalSet) {
|
||||
articleContent = articleContent.replace(new RegExp("\\b" + word + "\\b", "g"), "<span class='highlighted'>" + word + "</span>");
|
||||
}
|
||||
document.getElementById("article").innerHTML = articleContent;
|
||||
addClickEventToHighlightedWords();
|
||||
}
|
||||
|
||||
function cancelHighlighting() {
|
||||
let articleContent = document.getElementById("article").innerHTML;
|
||||
articleContent = articleContent.replace(new RegExp('<span class="highlighted">',"gi"), "")
|
||||
articleContent = articleContent.replace(new RegExp("</span>","gi"), "");
|
||||
document.getElementById("article").innerHTML = articleContent;
|
||||
}
|
||||
|
||||
function fillInWord() {
|
||||
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 fillInWord2() {
|
||||
cancelHighlighting();
|
||||
function getWord() {
|
||||
var word = window.getSelection ? window.getSelection() : document.selection.createRange().text;
|
||||
return word;
|
||||
}
|
||||
|
||||
function toggleHighlighting() {
|
||||
function highLight() {
|
||||
if(!isHighlight) return;
|
||||
var txt = document.getElementById("article").innerText;
|
||||
var sel_word1 = document.getElementById("selected-words");
|
||||
var sel_word2 = document.getElementById("selected-words2");
|
||||
if (sel_word1 != null) {
|
||||
var list = sel_word1.value.split(" ");
|
||||
for (var i = 0; i < list.length; ++i) {
|
||||
list[i] = list[i].replace(/(^\s*)|(\s*$)/g, "");
|
||||
if (list[i] != "" && "<mark>".indexOf(list[i]) == -1 && "</mark>".indexOf(list[i]) == -1) {
|
||||
txt = txt.replace(new RegExp(list[i], "g"), "<mark>" + list[i] + "</mark>");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sel_word2 != null) {
|
||||
var list2 = sel_word2.value.split(" ");
|
||||
for (var i = 0; i < list2.length; ++i) {
|
||||
list2[i] = list2[i].replace(/(^\s*)|(\s*$)/g, "");
|
||||
if (list2[i] != "" && "<mark>".indexOf(list2[i]) == -1 && "</mark>".indexOf(list2[i]) == -1) {
|
||||
txt = txt.replace(new RegExp(list2[i], "g"), "<mark>" + list2[i] + "</mark>");
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById("article").innerHTML = txt;
|
||||
}
|
||||
|
||||
function cancel_highLight() {
|
||||
var txt = document.getElementById("article").innerText;
|
||||
var sel_word1 = document.getElementById("selected-words");
|
||||
var sel_word2 = document.getElementById("selected-words2");
|
||||
if (sel_word1 != null) {
|
||||
var list = sel_word1.value.split(" ");
|
||||
for (var i = 0; i < list.length; ++i) {
|
||||
list[i] = list[i].replace(/(^\s*)|(\s*$)/g, "");
|
||||
if (list[i] != "") {
|
||||
txt = txt.replace("<mark>" + list[i] + "</mark>", "list[i]");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sel_word2 != null) {
|
||||
var list2 = sel_word1.value.split(" ");
|
||||
for (var i = 0; i < list2.length; ++i) {
|
||||
var list2 = sel_word2.value.split(" ");
|
||||
list2[i] = list2[i].replace(/(^\s*)|(\s*$)/g, "");
|
||||
if (list2[i] != "") {
|
||||
txt = txt.replace("<mark>" + list[i] + "</mark>", "list[i]");
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById("article").innerHTML = txt;
|
||||
}
|
||||
|
||||
function fillinWord() {
|
||||
highLight();
|
||||
}
|
||||
|
||||
function fillinWord2() {
|
||||
cancel_highLight();
|
||||
}
|
||||
|
||||
function ChangeHighlight() {
|
||||
if (isHighlight) {
|
||||
isHighlight = false;
|
||||
cancelHighlighting();
|
||||
cancel_highLight();
|
||||
} else {
|
||||
isHighlight = true;
|
||||
highLight();
|
||||
|
||||
}
|
||||
localStorage.setItem('highlightChecked', isHighlight);
|
||||
}
|
||||
|
||||
function showWordMeaning(event) {
|
||||
const word = event.target.innerText.trim().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "").toLowerCase();
|
||||
const apiUrl = '/translate';
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const tooltipX = rect.left + window.scrollX;
|
||||
const tooltipY = rect.top + window.scrollY + rect.height;
|
||||
// 发送POST请求
|
||||
fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ word: word }), // 发送的JSON数据
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json(); // 解析JSON响应
|
||||
})
|
||||
.then(data => {
|
||||
// 假设data.translation是翻译结果
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
if (!tooltip) {
|
||||
console.error('Tooltip element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip.textContent = data.translation || '没有找到该单词的中文意思';
|
||||
tooltip.style.left = `${tooltipX}px`;
|
||||
tooltip.style.top = `${tooltipY}px`;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.position = 'absolute';
|
||||
tooltip.style.background = 'yellow';
|
||||
|
||||
// 可以在这里添加点击事件监听器来隐藏tooltip,但注意避免内存泄漏
|
||||
document.addEventListener('click', function handler(e) {
|
||||
if (!tooltip.contains(e.target)) {
|
||||
tooltip.style.display = 'none';
|
||||
document.removeEventListener('click', handler);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with your fetch operation:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function addClickEventToHighlightedWords() {
|
||||
const highlightedWords = document.querySelectorAll('.highlighted');
|
||||
highlightedWords.forEach(word => {
|
||||
word.addEventListener('click', showWordMeaning);
|
||||
});
|
||||
}
|
||||
|
||||
showBtnHandler();
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,200 +0,0 @@
|
|||
/**
|
||||
* jquery plugin -- jquery.slideunlock.js
|
||||
* Description: a slideunlock plugin based on jQuery
|
||||
* Version: 1.1
|
||||
* Author: Dong Yuhao
|
||||
* created: March 27, 2016
|
||||
*/
|
||||
|
||||
;(function ($,window,document,undefined) {
|
||||
function SliderUnlock(elm, options, success){
|
||||
var me = this;
|
||||
var $elm = me.checkElm(elm) ? $(elm) : $;
|
||||
success = me.checkFn(success) ? success : function(){};
|
||||
|
||||
var opts = {
|
||||
successLabelTip: "Successfully Verified",
|
||||
duration: 200,
|
||||
swipestart: false,
|
||||
min: 0,
|
||||
max: $elm.width(),
|
||||
index: 0,
|
||||
isOk: false,
|
||||
lableIndex: 0
|
||||
};
|
||||
|
||||
opts = $.extend(opts, options||{});
|
||||
|
||||
//$elm
|
||||
me.elm = $elm;
|
||||
//opts
|
||||
me.opts = opts;
|
||||
//是否开始滑动
|
||||
me.swipestart = opts.swipestart;
|
||||
//最小值
|
||||
me.min = opts.min;
|
||||
//最大值
|
||||
me.max = opts.max;
|
||||
//当前滑动条所处的位置
|
||||
me.index = opts.index;
|
||||
//是否滑动成功
|
||||
me.isOk = opts.isOk;
|
||||
//滑块宽度
|
||||
me.labelWidth = me.elm.find('#label').width();
|
||||
//滑块背景
|
||||
me.sliderBg = me.elm.find('#slider_bg');
|
||||
//鼠标在滑动按钮的位置
|
||||
me.lableIndex = opts.lableIndex;
|
||||
//success
|
||||
me.success = success;
|
||||
}
|
||||
|
||||
SliderUnlock.prototype.init = function () {
|
||||
var me = this;
|
||||
|
||||
me.updateView();
|
||||
me.elm.find("#label").on("mousedown", function (event) {
|
||||
var e = event || window.event;
|
||||
me.lableIndex = e.clientX - this.offsetLeft;
|
||||
me.handerIn();
|
||||
}).on("mousemove", function (event) {
|
||||
me.handerMove(event);
|
||||
}).on("mouseup", function (event) {
|
||||
me.handerOut();
|
||||
}).on("mouseout", function (event) {
|
||||
me.handerOut();
|
||||
}).on("touchstart", function (event) {
|
||||
var e = event || window.event;
|
||||
me.lableIndex = e.originalEvent.touches[0].pageX - this.offsetLeft;
|
||||
me.handerIn();
|
||||
}).on("touchmove", function (event) {
|
||||
me.handerMove(event, "mobile");
|
||||
}).on("touchend", function (event) {
|
||||
me.handerOut();
|
||||
});
|
||||
};
|
||||
SliderUnlock.prototype.getIsOk = function() {
|
||||
return this.isOk;
|
||||
};
|
||||
|
||||
/**
|
||||
* 鼠标/手指接触滑动按钮
|
||||
*/
|
||||
SliderUnlock.prototype.handerIn = function () {
|
||||
var me = this;
|
||||
me.swipestart = true;
|
||||
me.min = 0;
|
||||
me.max = me.elm.width();
|
||||
};
|
||||
|
||||
/**
|
||||
* 鼠标/手指移出
|
||||
*/
|
||||
SliderUnlock.prototype.handerOut = function () {
|
||||
var me = this;
|
||||
//停止
|
||||
me.swipestart = false;
|
||||
//me.move();
|
||||
if (me.index < me.max) {
|
||||
me.reset();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 鼠标/手指移动
|
||||
* @param event
|
||||
* @param type
|
||||
*/
|
||||
SliderUnlock.prototype.handerMove = function (event, type) {
|
||||
var me = this;
|
||||
if (me.swipestart) {
|
||||
event.preventDefault();
|
||||
event = event || window.event;
|
||||
if (type == "mobile") {
|
||||
me.index = event.originalEvent.touches[0].pageX - me.lableIndex;
|
||||
} else {
|
||||
me.index = event.clientX - me.lableIndex;
|
||||
}
|
||||
me.move();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 鼠标/手指移动过程
|
||||
*/
|
||||
SliderUnlock.prototype.move = function () {
|
||||
var me = this;
|
||||
if ((me.index + me.labelWidth) >= me.max) {
|
||||
me.index = me.max - me.labelWidth -2;
|
||||
//停止
|
||||
me.swipestart = false;
|
||||
//解锁
|
||||
me.isOk = true;
|
||||
}
|
||||
if (me.index < 0) {
|
||||
me.index = me.min;
|
||||
//未解锁
|
||||
me.isOk = false;
|
||||
}
|
||||
if (me.index+me.labelWidth+2 == me.max && me.max > 0 && me.isOk) {
|
||||
//解锁默认操作
|
||||
$('#label').unbind().next('#labelTip').
|
||||
text(me.opts.successLabelTip).css({'color': '#fff'});
|
||||
|
||||
me.success();
|
||||
}
|
||||
me.updateView();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 更新视图
|
||||
*/
|
||||
SliderUnlock.prototype.updateView = function () {
|
||||
var me = this;
|
||||
|
||||
me.sliderBg.css('width', me.index);
|
||||
me.elm.find("#label").css("left", me.index + "px")
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置slide的起点
|
||||
*/
|
||||
SliderUnlock.prototype.reset = function () {
|
||||
var me = this;
|
||||
|
||||
me.index = 0;
|
||||
me.sliderBg .animate({'width':0},me.opts.duration);
|
||||
me.elm.find("#label").animate({left: me.index}, me.opts.duration)
|
||||
.next("#lableTip").animate({opacity: 1}, me.opts.duration);
|
||||
me.updateView();
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测元素是否存在
|
||||
* @param elm
|
||||
* @returns {boolean}
|
||||
*/
|
||||
SliderUnlock.prototype.checkElm = function (elm) {
|
||||
if($(elm).length > 0){
|
||||
return true;
|
||||
}else{
|
||||
throw "this element does not exist.";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测传入参数是否是function
|
||||
* @param fn
|
||||
* @returns {boolean}
|
||||
*/
|
||||
SliderUnlock.prototype.checkFn = function (fn) {
|
||||
if(typeof fn === "function"){
|
||||
return true;
|
||||
}else{
|
||||
throw "the param is not a function.";
|
||||
}
|
||||
};
|
||||
|
||||
window['SliderUnlock'] = SliderUnlock;
|
||||
})(jQuery, window, document);
|
|
@ -1,20 +0,0 @@
|
|||
function containsDigitsLettersSpecialCharacters(s) {
|
||||
let resultD = 0, resultL = 0, resultS = 0;
|
||||
|
||||
// Digit test
|
||||
'0123456789'.split('').forEach((x) => {
|
||||
if (s.includes(x))
|
||||
resultD = 1;
|
||||
});
|
||||
|
||||
// Letter test
|
||||
resultL = /[a-z]/i.test(s);
|
||||
|
||||
// Special charater test
|
||||
'+-*/,.:;/\[]<>$%&()!?^~'.split('').forEach((x) => {
|
||||
if (s.includes(x))
|
||||
resultS = 1;
|
||||
});
|
||||
|
||||
return resultD + resultL + resultS == 3;
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
var Reader = (function() {
|
||||
let reader = window.speechSynthesis;
|
||||
let current_position = 0;
|
||||
let original_position = 0;
|
||||
let to_speak = "";
|
||||
let current_rate = 1; // 添加这一行,设置默认速率为 1
|
||||
|
||||
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 updateRate(rate) {
|
||||
// 停止当前的朗读
|
||||
stopRead();
|
||||
|
||||
// 更新当前速率
|
||||
current_rate = rate;
|
||||
|
||||
// 重新开始朗读
|
||||
read(to_speak, current_rate);
|
||||
}
|
||||
|
||||
function stopRead() {
|
||||
reader.cancel();
|
||||
}
|
||||
|
||||
return {
|
||||
read: read,
|
||||
stopRead: stopRead,
|
||||
updateRate: updateRate // 添加这一行,将 updateRate 方法暴露出去
|
||||
};
|
||||
}) ();
|
|
@ -1,246 +0,0 @@
|
|||
function familiar(theWord) {
|
||||
let username = $("#username").text();
|
||||
let word = document.getElementById(`word_${theWord}`).innerText;
|
||||
let freq = document.getElementById(`freq_${theWord}`).innerText;
|
||||
console.log(theWord);
|
||||
console.log(word);
|
||||
$.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 = document.getElementById(`word_${theWord}`).innerText;
|
||||
let freq = document.getElementById(`freq_${theWord}`).innerText;
|
||||
console.log(theWord);
|
||||
console.log(word);
|
||||
$.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('&', '&');
|
||||
$.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();
|
||||
}
|
||||
// remove highlighting for the word
|
||||
let highlightedWords = document.querySelectorAll('.highlighted');
|
||||
for (let x of highlightedWords) {
|
||||
if (x.innerHTML == word)
|
||||
x.replaceWith(x.innerHTML);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 // 获取词汇值;
|
||||
let freqId = `freq_${word}`;
|
||||
freqId = CSS.escape(freqId); // for fixing bug 580, escape the apostrophe in the word
|
||||
const freq = Number.parseInt(element.querySelector("#"+freqId).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>
|
||||
<a class="btn btn-primary" onclick="addNote('{{ word }}'); saveNote('{{ word }}')" role="button">笔记</a> <!-- Modify to call addNote and then saveNote -->
|
||||
<input type="text" id="note_{{ word }}" class="note-input" placeholder="输入笔记内容" style="display:none;" oninput="saveNote('{{ word }}')"> <!-- Added oninput event -->
|
||||
</p>`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除某一词频元素
|
||||
* 此处word为词频元素对应的单词
|
||||
*/
|
||||
function removeWord(word) {
|
||||
// 根据词频信息删除元素
|
||||
word = word.replace('&', '&');
|
||||
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 first.freq < second.freq ? -1 : 1;
|
||||
}
|
||||
if (first.word !== second.word) {
|
||||
return first.word < second.word ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 生词csv导出 */
|
||||
function exportToCSV() {
|
||||
let csvContent = "data:text/csv;charset=utf-8,Word,Frequency\n";
|
||||
let rows = document.querySelectorAll(".new-word");
|
||||
|
||||
rows.forEach(row => {
|
||||
let word = row.querySelector("a.btn-light").innerText;
|
||||
let freq = row.querySelector("a[title]").innerText;
|
||||
csvContent += word + "," + freq + "\n";
|
||||
});
|
||||
|
||||
let encodedUri = encodeURI(csvContent);
|
||||
let link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", "word_list.csv");
|
||||
document.body.appendChild(link);
|
||||
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 随机选取 10 个单词学习
|
||||
*/
|
||||
function random_select_word(word) {
|
||||
|
||||
// 获取所有带有 "word-container" 类的 <p> 标签
|
||||
const container = document.querySelector('.word-container');
|
||||
|
||||
console.log("container",container)
|
||||
|
||||
// 获取所有带有"new-word"类的<p>标签
|
||||
let wordContainers = container.querySelectorAll('.new-word');
|
||||
|
||||
// 检查是否存在带有"new-word"类的<p>标签
|
||||
if (wordContainers.length > 0) {
|
||||
// 将NodeList转换为数组
|
||||
let wordContainersArray = [...wordContainers];
|
||||
|
||||
// 随机打乱数组,乱序
|
||||
for (let i = wordContainersArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[wordContainersArray[i], wordContainersArray[j]] = [wordContainersArray[j], wordContainersArray[i]];
|
||||
}
|
||||
|
||||
wordContainersArray.forEach((p, index) => {
|
||||
if (index < 10) {
|
||||
p.style.display = 'block';
|
||||
} else {
|
||||
p.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,55 +0,0 @@
|
|||
<!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>
|
|
@ -1,109 +0,0 @@
|
|||
<!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>
|
||||
function confirmDeletion(articleId, articleTitle) {
|
||||
return confirm(`确认删除文章 "${articleTitle}" (ID: ${articleId}) 吗?`);
|
||||
}
|
||||
</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;">
|
||||
{% 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="格式:
 QUESTION
 What?

 ANSWER
 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">
|
||||
<form action="/admin/article" method="post" style="display: inline;">
|
||||
<input type="hidden" name="delete_id" value="{{ text.article_id }}">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirmDeletion('{{ text.article_id }}', '{{ text.title }}')">删除</button>
|
||||
</form>
|
||||
<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>
|
|
@ -1,99 +0,0 @@
|
|||
<!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>
|
|
@ -5,7 +5,7 @@
|
|||
<title>账号过期</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>您的账号过期(过期日 {{expiry_date}})。</p>
|
||||
<p>您的账号{{ username }}过期。</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>
|
||||
|
|
|
@ -1,109 +1,21 @@
|
|||
{% block body %}
|
||||
{% if session['logged_in'] %}
|
||||
|
||||
你已登录 <a href="/{{ session['username'] }}/userpage">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>。
|
||||
You're logged in already!
|
||||
|
||||
{% 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>
|
||||
let blackList = [];
|
||||
|
||||
<!--function getBlack() {-->
|
||||
<!-- const fs = require('fs');-->
|
||||
<!-- global.blackFile = fs.readFileSync('black', 'utf8');-->
|
||||
<!-- const blackListTemp = blackFile.split('\n');-->
|
||||
<!-- global.blackList = blackListTemp.map(line => line.trim()).filter(line => line !== '');-->
|
||||
<!--}-->
|
||||
|
||||
function putUserIntoBlack(usernameTemp) {
|
||||
|
||||
blackList.push(usernameTemp);
|
||||
}
|
||||
|
||||
function ifUsernameInBlack(usernameTemp) {
|
||||
return blackList.includes(usernameTemp);
|
||||
}
|
||||
|
||||
count=0
|
||||
function login()
|
||||
{
|
||||
let username = $("#username").val();
|
||||
let password = $("#password").val();
|
||||
if (username === "" || password === ""){
|
||||
alert('输入不能为空!');
|
||||
return false;
|
||||
}
|
||||
if (password.includes(' ')) {
|
||||
alert('输入不能包含空格!');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$.post
|
||||
(
|
||||
"/login", {'username': username, 'password': password},
|
||||
|
||||
function (response)
|
||||
{
|
||||
|
||||
if(response.status === '5')
|
||||
{
|
||||
alert('已被加入黑名单,请联系管理员!');
|
||||
}
|
||||
else{
|
||||
if(!ifUsernameInBlack(username))
|
||||
{
|
||||
if (response.status === '0')
|
||||
{
|
||||
if(count<5)
|
||||
{
|
||||
alert('无法通过验证。');
|
||||
<!--window.location.href = "/login";-->
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
<!--输入错误密码次数超过5次-->
|
||||
alert('密码输入错误超过五次,已被加入黑名单!');
|
||||
putUserIntoBlack(username);
|
||||
console.log(ifUsernameInBlack(username));
|
||||
response.status=5;
|
||||
$("#password").val('黑名单');
|
||||
}
|
||||
}
|
||||
else if (response.status === '1')
|
||||
{
|
||||
window.location.href = "/"+username+"/userpage";
|
||||
}
|
||||
}
|
||||
else if(ifUsernameInBlack(username))
|
||||
{
|
||||
alert('已被加入黑名单!');
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
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>
|
||||
|
||||
<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 %}
|
||||
|
||||
|
|
|
@ -23,20 +23,17 @@
|
|||
<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 %}
|
||||
<a href="/{{session['username']}}">{{session['username']}}</a></p>
|
||||
{% 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>
|
||||
<p><b>{{random_ads|safe}}</b></p>
|
||||
{% endif %}
|
||||
<div class="alert alert-success" role="alert">共有文章 <span class="badge bg-success"> {{ number_of_essays }} </span> 篇,覆盖 <span class="badge bg-success"> {{ (ratio * 100) | int }}% </span> 的 Oxford5000 单词</div>
|
||||
<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" id="article" rows="10" cols="120"></textarea><br/>
|
||||
<textarea name="content" rows="10" cols="120"></textarea><br/>
|
||||
<input type="submit" value="get文章中的词频"/>
|
||||
<input type="reset" value="清除" onclick="clearArticle()"/>
|
||||
<input type="reset" value="清除"/>
|
||||
</form>
|
||||
{% if d_len > 0 %}
|
||||
<p><b>最常见的词</b></p>
|
||||
|
@ -44,7 +41,6 @@
|
|||
<a href="http://youdao.com/w/eng/{{x[0]}}/#keyfrom=dict2.index">{{x[0]}}</a> {{x[1]}}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<p class="text-muted">Version: 20240618</p>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
</div>
|
||||
{{ yml['footer'] | safe }}
|
||||
|
@ -53,22 +49,5 @@
|
|||
<script src="{{ js }}" ></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<script type="text/javascript">
|
||||
// IIFE, avoid polluting the global scope
|
||||
(function() {
|
||||
const articleInput = document.querySelector('#article');
|
||||
articleInput.value = localStorage.getItem('article') || '';
|
||||
|
||||
articleInput.addEventListener('input', function() {
|
||||
localStorage.setItem('article', articleInput.value);
|
||||
});
|
||||
|
||||
window.clearArticle = function() {
|
||||
localStorage.removeItem('article');
|
||||
articleInput.value = '';
|
||||
};
|
||||
})();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>单词词频</title>
|
||||
<title>Title</title>
|
||||
|
||||
{{ yml['header'] | safe }}
|
||||
{% if yml['css']['item'] %}
|
||||
|
|
|
@ -1,60 +1,14 @@
|
|||
{% 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 src="static/js/password.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 (old_password.includes(' ') || new_password.includes(' ')) {
|
||||
alert('输入不能包含空格!');
|
||||
return false;
|
||||
}
|
||||
if (new_password !== re_new_password) {
|
||||
alert('新密码不匹配,请重新输入');
|
||||
return false;
|
||||
}
|
||||
if (new_password.length < 4) {
|
||||
alert('密码过于简单。(密码长度至少4位)');
|
||||
return false;
|
||||
}
|
||||
if (!containsDigitsLettersSpecialCharacters(new_password)) {
|
||||
alert('密码过于简单。(密码要包括数字,字母,特殊符号)');
|
||||
return false;
|
||||
}
|
||||
$.post("/reset", {'old-password': old_password, 'new-password': new_password},
|
||||
function (response) {
|
||||
console.log(response);
|
||||
if ('success' in response) {
|
||||
alert('密码修改成功。');
|
||||
} else if ('error' in response) {
|
||||
alert(`密码修改失败 ${response.error}`);
|
||||
}
|
||||
window.location.href = `/${response.username}/userpage`;
|
||||
}
|
||||
)
|
||||
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 %}
|
||||
<html>
|
||||
<body>
|
||||
<form action="/reset" method='POST'>
|
||||
旧密码:
|
||||
<input type="password" name="old-password" />
|
||||
<br/>
|
||||
新密码:
|
||||
<input type="password" name="new-password" />
|
||||
<br/>
|
||||
<input type="submit" name="submit" value="提交" />
|
||||
<input type="button" name="submit" value="放弃修改" onclick="window.location.href='/{{ username }}'"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -1,112 +1,19 @@
|
|||
{% block body %}
|
||||
{% if session['logged_in'] %}
|
||||
You're logged in already! <a href="/logout">Logout</a>.
|
||||
|
||||
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">
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE-edge,chrome=1">
|
||||
<link href="static/css/slide-unlock.css" rel="stylesheet">
|
||||
<script src="static/js/password.js"></script>
|
||||
<script src="static/js/jquery.js"></script>
|
||||
<script src="static/js/jquery.slideunlock.js"></script>
|
||||
<script>
|
||||
var slider
|
||||
let username,password,password2
|
||||
$(document).ready(function() {
|
||||
slider = new SliderUnlock("#slider", {
|
||||
successLabelTip: "验证成功"
|
||||
}, function() {
|
||||
<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>
|
||||
|
||||
});
|
||||
slider.init(); // 初始化滑块解锁功能
|
||||
});
|
||||
|
||||
function signup(){
|
||||
// 发起 AJAX 请求来处理注册
|
||||
username = $("#username").val().trim();
|
||||
password = $("#password").val().trim();
|
||||
password2 = $("#password2").val().trim();
|
||||
|
||||
// 基本表单验证
|
||||
if (username === "" || password === "" || password2 === "") {
|
||||
alert('输入不能为空!');
|
||||
return false;
|
||||
}
|
||||
if (password.includes(' ') || password2.includes(' ')) {
|
||||
alert('输入不能包含空格!');
|
||||
return false;
|
||||
}
|
||||
if (password !== password2) {
|
||||
alert('确认密码与输入密码不一致!');
|
||||
return false;
|
||||
}
|
||||
if (password.length < 4) {
|
||||
alert('密码过于简单。(密码长度至少4位)');
|
||||
return false;
|
||||
}
|
||||
if (!containsDigitsLettersSpecialCharacters(password)) {
|
||||
alert('密码过于简单。(密码要包括数字,字母,特殊符号)');
|
||||
return false;
|
||||
}
|
||||
is_ok = slider.getIsOk();
|
||||
if(!is_ok){
|
||||
alert('没有滑动验证');
|
||||
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') {
|
||||
var 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>
|
||||
|
||||
<form>
|
||||
<p><input type="text" 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>
|
||||
|
||||
<div id="slider">
|
||||
<div id="slider_bg"></div>
|
||||
<span id="label">>></span> <span id="labelTip">-----滑动验证你是不是人类</span>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn" onclick="signup()">注册</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Bind click event to the signup button
|
||||
$(".btn").click(function() {
|
||||
// Trigger slider unlock
|
||||
var slider = new SliderUnlock("#slider");
|
||||
slider.isOk();
|
||||
});
|
||||
</script>
|
||||
<p>Sign up here.</p>
|
||||
|
||||
<form action="/signup" method="POST">
|
||||
<p><input type="username" name="username" placeholder="邮箱地址、电话号码" required="required"></p>
|
||||
<p><input type="password" name="password" placeholder="密码"></p>
|
||||
<p><input type="submit" value="注册"></p>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<!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">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<link rel="stylesheet" href="../static/css/button.css">
|
||||
<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'] %}
|
||||
|
@ -23,125 +19,68 @@
|
|||
{% 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><b>English Pal for <font color="red">{{ username }}</font></b>
|
||||
<a class="btn btn-secondary" href="/logout" role="button">退出</a>
|
||||
<a class="btn btn-secondary" href="/reset" role="button">重设密码</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 %}
|
||||
|
||||
<div class="pagination">
|
||||
<button class="arrow" id="load_pre_article" onclick="load_pre_article();Reader.stopRead()" title="Previous Article">
|
||||
<i class="fas fa-chevron-left"></i> 上一篇
|
||||
</button>
|
||||
<button class="arrow" id="load_next_article" onclick="load_next_article();Reader.stopRead()" title="Next Article">
|
||||
下一篇 <i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
{{ flashed_messages|safe }}
|
||||
|
||||
<p><a class="btn btn-success" href="/{{ username }}/reset" role="button"> 下一篇 Next Article </a></p>
|
||||
<p><b>阅读文章并回答问题</b></p>
|
||||
</div>
|
||||
|
||||
<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. The Oxford word coverage is <span class="text-decoration-underline" id="ratio">{{ (today_article["ratio"] * 100) | int }}%.</span></div>
|
||||
<p class="text-muted" id="date">Article added on: {{ today_article["date"] }}</p><br/>
|
||||
|
||||
<button onclick="saveArticle()" >标记文章</button>
|
||||
<select id="saved_articles_dropdown">
|
||||
<!-- 这里将显示已经保存的文章 -->
|
||||
<option></option>
|
||||
</select>
|
||||
|
||||
<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 id="tooltip"></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 id="text-content">
|
||||
<div class="alert alert-success" role="alert">
|
||||
According to your word list, your level is
|
||||
<span class="badge bg-success">{{ user_level }}</span>
|
||||
and we have chosen an article with a difficulty level of
|
||||
<span class="badge bg-success">{{ text_level }}</span>
|
||||
for you.
|
||||
</div>
|
||||
<p class="text-muted">Article added on: {{ article_date }}</p>
|
||||
<div class="p-3 mb-2 bg-light text-dark">
|
||||
<p class="display-3">{{ article_title }}</p>
|
||||
<p class="lead"><font id="article" size=2>{{ article_body }}</font></p>
|
||||
<p><b>
|
||||
{% for x in question_part %}
|
||||
{{ x }}
|
||||
</br>
|
||||
{% endfor %}
|
||||
</b></p>
|
||||
<script type="text/javascript">
|
||||
|
||||
<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>
|
||||
function toggle_visibility(id) {
|
||||
var e = document.getElementById(id);
|
||||
if(e.style.display == 'block')
|
||||
e.style.display = 'none';
|
||||
else
|
||||
e.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</br>
|
||||
<button onclick="toggle_visibility('answer')">ANSWER</button></br>
|
||||
<div id="answer" style="display:none;">
|
||||
{% for x in answer_part %}
|
||||
{{ x }}
|
||||
</br>
|
||||
{% endfor %}
|
||||
</div></br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<input type="checkbox" onclick="ChangeHighlight()" checked/>生词高亮
|
||||
<input type="checkbox" onclick="onReadClick()" checked/>大声朗读
|
||||
<input type="checkbox" onclick="onChooseClick()" checked/>划词入库
|
||||
|
||||
<p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p>
|
||||
<form method="post" action="/{{ username }}/userpage">
|
||||
<form method="post" action="/{{ username }}">
|
||||
<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>
|
||||
<input type="submit" value="把生词加入我的生词库"/>
|
||||
<input type="reset" value="清除"/>
|
||||
</form>
|
||||
{% if session.get['thisWord'] %}
|
||||
<script type="text/javascript">
|
||||
|
@ -155,41 +94,42 @@
|
|||
{% endif %}
|
||||
|
||||
{% if d_len > 0 %}
|
||||
<p>
|
||||
<p><b>我的生词簿</b></p>
|
||||
{% for x in lst3 %}
|
||||
{% set word = x[0] %}
|
||||
|
||||
<b>我的生词簿</b>
|
||||
<label for="move_dynamiclly">
|
||||
<input type="checkbox" name="move_dynamiclly" id="move_dynamiclly" checked>
|
||||
允许动态调整顺序
|
||||
</label>
|
||||
<br>
|
||||
<a class="btn btn-primary btn-lg" onclick="random_select_word('{{ word }}')" role="button">随机选取10个</a>
|
||||
<a class="btn btn-primary btn-lg" onclick="location.reload();" role="button">显示所有生词</a>
|
||||
</p>
|
||||
<!--添加导出按钮-->
|
||||
<button class="btn btn-primary" onclick="exportToCSV()">导出</button>
|
||||
<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>
|
||||
<a class="btn btn-primary" onclick="addNote('{{ word }}'); saveNote('{{ word }}')" role="button">笔记</a> <!-- Modify to call addNote and then saveNote -->
|
||||
<input type="text" id="note_{{ word }}" class="note-input" placeholder="输入笔记内容" style="display:none;" oninput="saveNote('{{ word }}')"> <!-- Added oninput event -->
|
||||
{% set freq = x[1] %}
|
||||
{% if session.get('thisWord') == x[0] and session.get('time') == 1 %}
|
||||
<a name="aaa"></a>
|
||||
{% endif %}
|
||||
{% if freq > 1 %}
|
||||
<p class="new-word">
|
||||
<a class="btn btn-light" href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index'
|
||||
role="button">{{ word }}</a>
|
||||
(
|
||||
<a title="{{ word }}">{{ freq }}</a>
|
||||
)
|
||||
|
||||
<a class="btn btn-success" href={{ username }}/{{ word }}/familiar role="button">熟悉</a>
|
||||
<a class="btn btn-warning" href={{ username }}/{{ word }}/unfamiliar role="button">不熟悉</a>
|
||||
<a class="btn btn-danger" href={{ username }}/{{ word }}/del role="button">删除</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="new-word">
|
||||
<a class="btn btn-light" href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index'
|
||||
role="button">{{ word }}</a>
|
||||
(
|
||||
<a title="{{ word }}">{{ freq }}</a>
|
||||
)
|
||||
<a class="btn btn-success" href={{ username }}/{{ word }}/familiar role="button">熟悉</a>
|
||||
<a class="btn btn-warning" href={{ username }}/{{ word }}/unfamiliar role="button">不熟悉</a>
|
||||
<a class="btn btn-danger" href={{ username }}/{{ word }}/del role="button">删除</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index'>{{ word }}</a>{{ freq }}
|
||||
{% endfor %}
|
||||
<input id="selected-words2" type="hidden" value="{{ words }}">
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ yml['footer'] | safe }}
|
||||
|
@ -198,243 +138,12 @@
|
|||
<script src="{{ js }}"></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
// Function to show/hide note input and load saved note content from localStorage
|
||||
function addNote(word) {
|
||||
var noteInput = document.getElementById("note_" + word);
|
||||
var savedNote = localStorage.getItem(word); // Get the saved note from localStorage
|
||||
if (savedNote) {
|
||||
noteInput.value = savedNote; // Set the saved note if it exists
|
||||
}
|
||||
noteInput.style.display = (noteInput.style.display === 'none') ? 'inline-block' : 'none'; // Toggle display
|
||||
}
|
||||
|
||||
// Example function to save the note to localStorage
|
||||
function saveNote(word) {
|
||||
var noteContent = document.getElementById("note_" + word).value;
|
||||
localStorage.setItem(word, noteContent); // Save the note content in localStorage
|
||||
console.log('Note saved for ' + word + ': ' + noteContent); // Log for debugging purposes
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<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').addClass('gray-background');
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
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(){
|
||||
$.ajax({
|
||||
url: '/get_next_article/{{username}}',
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
// 更新页面内容
|
||||
if(data['today_article']){
|
||||
// answer不可见
|
||||
const e = document.getElementById('answer');
|
||||
e.style.display = 'none';
|
||||
update(data['today_article']);
|
||||
check_pre(data['visited_articles']);
|
||||
check_next(data['result_of_generate_article']);
|
||||
toggleHighlighting();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function load_pre_article(){
|
||||
$.ajax({
|
||||
url: '/get_pre_article/{{username}}',
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
// 更新页面内容
|
||||
if(data['today_article']){
|
||||
// answer不可见
|
||||
const e = document.getElementById('answer');
|
||||
e.style.display = 'none';
|
||||
update(data['today_article']);
|
||||
check_pre(data['visited_articles']);
|
||||
toggleHighlighting();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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"]);
|
||||
$('#ratio').html(Math.round(today_article["ratio"] * 100) + '%');
|
||||
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').addClass('gray-background'); // 设置为灰色
|
||||
sessionStorage.setItem('pre_page_button', 'display')
|
||||
}else{
|
||||
$('#load_pre_article').removeClass('gray-background'); // 设置为正常蓝色
|
||||
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();
|
||||
}
|
||||
}
|
||||
function saveArticle() {
|
||||
const article = {
|
||||
user_level: document.getElementById('user_level').innerText,
|
||||
text_level: document.getElementById('text_level').innerText,
|
||||
date: document.getElementById('date').innerText.replace('Article added on: ', ''),
|
||||
article_title: document.getElementById('article_title').innerText,
|
||||
article_body: document.getElementById('article').innerText,
|
||||
source: document.getElementById('source').innerText,
|
||||
question: document.getElementById('question').innerText,
|
||||
answer: document.getElementById('answer').innerText
|
||||
};
|
||||
|
||||
const articleJSON = JSON.stringify(article);
|
||||
const articleTitle = article.article_title;
|
||||
const savedArticlesDropdown = document.getElementById('saved_articles_dropdown');
|
||||
|
||||
var option = document.createElement('option');
|
||||
option.text = articleTitle;
|
||||
option.value = articleJSON; // 存储序列化的JSON字符串
|
||||
option.title = article.article_title;
|
||||
savedArticlesDropdown.appendChild(option);
|
||||
localStorage.setItem(articleTitle, articleJSON); // 以文章标题为键,序列化的JSON字符串为值存储
|
||||
}
|
||||
function loadSelectedArticle() {
|
||||
const selectedOption = document.getElementById('saved_articles_dropdown');
|
||||
const selectedTitle = selectedOption.options[selectedOption.selectedIndex].text;
|
||||
const articleJSON = localStorage.getItem(selectedTitle);
|
||||
|
||||
if (articleJSON) {
|
||||
const today_article = JSON.parse(articleJSON); // 解析JSON字符串为对象
|
||||
update(today_article); // 使用解析出的对象更新页面
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
const savedArticlesDropdown = document.getElementById('saved_articles_dropdown');
|
||||
savedArticlesDropdown.addEventListener('change', loadSelectedArticle);
|
||||
|
||||
// 先清空dropdown,以防有多余的选项或重新加载页面时出现重复
|
||||
savedArticlesDropdown.innerHTML = '';
|
||||
|
||||
// 获取localStorage中最后一个(最新)的键值对
|
||||
let latestKey, latestValue;
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
const value = localStorage.getItem(key);
|
||||
if (!latestKey) { // 第一次迭代时设置最新文章
|
||||
latestKey = key;
|
||||
latestValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 首先添加最新保存的文章到下拉菜单
|
||||
|
||||
if (latestKey && latestValue) {
|
||||
var latestOption = document.createElement('option');
|
||||
latestOption.text = latestKey;
|
||||
latestOption.value = latestValue;
|
||||
latestOption.title = latestValue;
|
||||
savedArticlesDropdown.appendChild(latestOption);
|
||||
}
|
||||
|
||||
// 接着遍历其余文章并添加到下拉菜单
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
const value = localStorage.getItem(key);
|
||||
// 确保不重复添加最新文章
|
||||
if (key !== latestKey && key !== 'selectedWords') {
|
||||
var option = document.createElement('option');
|
||||
option.text = key;
|
||||
option.value = value;
|
||||
option.title = value;
|
||||
savedArticlesDropdown.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
savedArticlesDropdown.selectedIndex = -1;
|
||||
}
|
||||
|
||||
document.getElementById('rangeComponent').addEventListener('input', function() {
|
||||
var rate = this.value;
|
||||
Reader.updateRate(rate);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
<style>
|
||||
mark {
|
||||
color: red;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
color: #{{ yml['highlight']['color'] }};
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
</style>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -1,52 +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" />
|
||||
<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 %}
|
||||
{{ 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>
|
||||
<div class="container-fluid">
|
||||
<p class="mt-md-3">
|
||||
<input type="button" id="btn-cancel-selection" value="取消勾选" onclick="toggleCheckboxSelection(false)" />
|
||||
<input type="button" id="btn-selection" value="全部勾选" onclick="toggleCheckboxSelection(true)" />
|
||||
</p>
|
||||
<form method="post" action="/{{username}}/mark">
|
||||
<button type="submit" name="add-btn" class="btn btn-outline-primary btn-lg" onclick="clearSelectedWords()">加入我的生词簿</button>
|
||||
{% 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>
|
||||
<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}}>
|
||||
</p>
|
||||
|
||||
{% endfor %}
|
||||
</form>
|
||||
{{ yml['footer'] | safe }}
|
||||
{% if yml['js']['bottom'] %}
|
||||
{% for js in yml['js']['bottom'] %}
|
||||
<script src="{{ js }}" ></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>window.history.replaceState(null, null, window.location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{% endfor %}
|
||||
</form>
|
||||
{{ yml['footer'] | safe }}
|
||||
{% if yml['js']['bottom'] %}
|
||||
{% for js in yml['js']['bottom'] %}
|
||||
<script src="{{ js }}" ></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +1,6 @@
|
|||
import pytest
|
||||
import sqlite3
|
||||
import time
|
||||
from selenium import webdriver
|
||||
|
||||
from pathlib import Path
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
|
||||
@pytest.fixture
|
||||
def URL():
|
||||
|
@ -12,24 +9,5 @@ def URL():
|
|||
|
||||
@pytest.fixture
|
||||
def driver():
|
||||
return webdriver.Edge() # follow the "End-to-end testing" section in README.md to install the web driver executable
|
||||
|
||||
|
||||
@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)
|
||||
my_driver = webdriver.Edge() # uncomment this line if you wish to run the test on your laptop
|
||||
return my_driver
|
||||
|
|
|
@ -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
|
|
@ -1,31 +1,76 @@
|
|||
import time
|
||||
from helper import signup
|
||||
# -*- 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
|
||||
|
||||
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:
|
||||
username, password = signup(URL, driver) # sign up a new account and automatically log in
|
||||
time.sleep(1)
|
||||
|
||||
# enter the word in the text area
|
||||
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
|
||||
|
||||
# get essay content
|
||||
elem = driver.find_element_by_id('text-content')
|
||||
essay_content = elem.text
|
||||
|
||||
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 = driver.find_element_by_xpath('//form[1]//button[1]') # 找到"把生词加入我的生词库"按钮
|
||||
elem.click()
|
||||
|
||||
elem = driver.find_element_by_name('add-btn') # 找到"加入我的生词簿"按钮
|
||||
elem = driver.find_element_by_xpath('//form[1]//input[1]') # 找到get所有词频按钮
|
||||
elem.click()
|
||||
|
||||
elems = driver.find_elements_by_xpath("//input[@type='checkbox']")
|
||||
for elem in elems:
|
||||
if elem.get_attribute('name') == 'marked':
|
||||
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")
|
||||
|
||||
|
||||
found = 0
|
||||
for elem in elems:
|
||||
if word in elem.text:
|
||||
found = 1
|
||||
break
|
||||
|
||||
|
||||
assert found == 1
|
||||
finally:
|
||||
finally:
|
||||
driver.quit()
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import pytest
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException
|
||||
|
||||
|
||||
# 测试登录页面输入密码包含空格的情况
|
||||
def test_login_password_with_space(driver, URL):
|
||||
try:
|
||||
driver.get(URL+"/login")
|
||||
|
||||
# 输入用户名
|
||||
username_elem = driver.find_element_by_id('username')
|
||||
username_elem.send_keys("test_user")
|
||||
|
||||
# 输入包含空格的密码
|
||||
password_elem = driver.find_element_by_id('password')
|
||||
password_elem.send_keys("password with space")
|
||||
|
||||
# 提交登录表单
|
||||
elem = driver.find_element_by_class_name('btn') # 找到提交按钮
|
||||
elem.click()
|
||||
|
||||
# 显式等待直到警告框出现
|
||||
WebDriverWait(driver, 10).until(EC.alert_is_present())
|
||||
|
||||
# 检查是否弹出警告框
|
||||
alert = driver.switch_to.alert
|
||||
assert "输入不能包含空格!" in alert.text
|
||||
except (NoSuchElementException, TimeoutException) as e:
|
||||
pytest.fail("页面元素未找到或超时: {}".format(e))
|
||||
|
||||
|
||||
# 测试注册页面输入密码包含空格的情况
|
||||
|
||||
def test_signup_password_with_space(driver, URL):
|
||||
try:
|
||||
driver.get(URL+"/signup")
|
||||
|
||||
# 输入用户名
|
||||
username_elem = driver.find_element_by_id('username')
|
||||
username_elem.send_keys("new_user")
|
||||
|
||||
# 输入包含空格的密码
|
||||
password_elem = driver.find_element_by_id('password')
|
||||
password_elem.send_keys("password with space")
|
||||
|
||||
# 再次输入密码
|
||||
password2_elem = driver.find_element_by_id('password2')
|
||||
password2_elem.send_keys("password with space")
|
||||
|
||||
# 提交注册表单
|
||||
elem = driver.find_element_by_class_name('btn') # 找到提交按钮
|
||||
elem.click()
|
||||
|
||||
# 显式等待直到警告框出现
|
||||
WebDriverWait(driver, 10).until(EC.alert_is_present())
|
||||
|
||||
# 检查是否弹出警告框
|
||||
alert = driver.switch_to.alert
|
||||
assert "输入不能包含空格!" in alert.text
|
||||
except (NoSuchElementException, TimeoutException) as e:
|
||||
pytest.fail("页面元素未找到或超时: {}".format(e))
|
||||
|
||||
|
||||
|
||||
# 测试重设密码页面输入新密码包含空格的情况
|
||||
|
||||
def test_reset_password_with_space(driver, URL):
|
||||
try:
|
||||
driver.get(URL+"/reset")
|
||||
|
||||
# 输入用户名
|
||||
username_elem = driver.find_element_by_id('username')
|
||||
username_elem.send_keys("test_user")
|
||||
|
||||
# 输入包含空格的密码
|
||||
password_elem = driver.find_element_by_id('password')
|
||||
password_elem.send_keys("password with space")
|
||||
|
||||
# 提交重设密码表单
|
||||
elem = driver.find_element_by_class_name('btn') # 找到提交按钮
|
||||
elem.click()
|
||||
|
||||
# 显式等待直到警告框出现
|
||||
WebDriverWait(driver, 10).until(EC.alert_is_present())
|
||||
|
||||
# 检查是否弹出警告框
|
||||
alert = driver.switch_to.alert
|
||||
assert "输入不能包含空格!" in alert.text
|
||||
except (NoSuchElementException, TimeoutException) as e:
|
||||
pytest.fail("页面元素未找到或超时: {}".format(e))
|
|
@ -1,88 +0,0 @@
|
|||
from selenium.webdriver.common.alert import Alert
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
|
||||
# 对用户名不能为中文进行测试
|
||||
def test_register_username_with_chinese(driver, URL):
|
||||
try:
|
||||
driver.get(URL + "/signup")
|
||||
|
||||
# 等待用户名输入框出现
|
||||
username_elem = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, 'username'))
|
||||
)
|
||||
username_elem.send_keys("测试用户") # 输入中文用户名
|
||||
|
||||
# 等待密码输入框出现
|
||||
password_elem = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, 'password'))
|
||||
)
|
||||
password_elem.send_keys("validPassword123") # 输入有效密码
|
||||
|
||||
# 等待确认密码输入框出现
|
||||
password2_elem = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, 'password2'))
|
||||
)
|
||||
password2_elem.send_keys("validPassword123") # 输入有效确认密码
|
||||
|
||||
# 等待注册按钮出现并点击
|
||||
signup_button = WebDriverWait(driver, 10).until(
|
||||
EC.element_to_be_clickable((By.XPATH, '//button[@onclick="signup()"]'))
|
||||
)
|
||||
signup_button.click()
|
||||
|
||||
# 等待警告框出现并接受
|
||||
WebDriverWait(driver, 10).until(EC.alert_is_present())
|
||||
alert = driver.switch_to.alert
|
||||
alert_text = alert.text
|
||||
print(f"警告文本: {alert_text}")
|
||||
assert alert_text == "Chinese characters are not allowed in the user name." # 根据实际的警告文本进行断言
|
||||
alert.accept()
|
||||
|
||||
except Exception as e:
|
||||
print(f"发生错误: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# 对注册时密码不能是中文进行测试
|
||||
def test_register_password_with_chinese(driver, URL):
|
||||
try:
|
||||
driver.get(URL + "/signup")
|
||||
|
||||
# 等待用户名输入框出现
|
||||
username_elem = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, 'username'))
|
||||
)
|
||||
username_elem.send_keys("validUsername123") # 输入有效用户名
|
||||
|
||||
# 等待密码输入框出现
|
||||
password_elem = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, 'password'))
|
||||
)
|
||||
password_elem.send_keys("测试密码") # 输入中文密码
|
||||
|
||||
# 等待确认密码输入框出现
|
||||
password2_elem = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, 'password2'))
|
||||
)
|
||||
password2_elem.send_keys("测试密码") # 输入中文确认密码
|
||||
|
||||
# 等待注册按钮出现并点击
|
||||
signup_button = WebDriverWait(driver, 10).until(
|
||||
EC.element_to_be_clickable((By.XPATH, '//button[@onclick="signup()"]'))
|
||||
)
|
||||
signup_button.click()
|
||||
|
||||
# 等待警告框出现并接受
|
||||
WebDriverWait(driver, 10).until(EC.alert_is_present())
|
||||
alert = driver.switch_to.alert
|
||||
alert_text = alert.text
|
||||
print(f"警告文本: {alert_text}")
|
||||
assert alert_text == "Chinese characters are not allowed in the password." # 根据实际的警告文本进行断言
|
||||
alert.accept()
|
||||
|
||||
except Exception as e:
|
||||
print(f"发生错误: {e}")
|
||||
raise
|
|
@ -1,45 +0,0 @@
|
|||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
import logging
|
||||
|
||||
from helper import signup
|
||||
|
||||
def login(driver, home, uname, password):
|
||||
driver.get(home)
|
||||
WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, '登录'))).click()
|
||||
driver.find_element(By.ID, 'username').send_keys(uname)
|
||||
driver.find_element(By.ID, 'password').send_keys(password)
|
||||
driver.find_element(By.XPATH, '//button[text()="登录"]').click()
|
||||
WebDriverWait(driver, 10).until(EC.title_is(f"EnglishPal Study Room for {uname}"))
|
||||
|
||||
def logout(driver):
|
||||
WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, '退出'))).click()
|
||||
|
||||
# 标记文章
|
||||
def collect_article(driver):
|
||||
driver.find_element(By.XPATH, '//button[text()="标记文章"]').click()
|
||||
|
||||
def test_collect_article(driver, URL):
|
||||
try:
|
||||
username, password = signup(URL, driver)
|
||||
title = driver.find_element(By.ID, 'article_title').text
|
||||
article = driver.find_element(By.ID, 'article').text
|
||||
|
||||
collect_article(driver)
|
||||
collected_title = driver.execute_script('return localStorage.getItem("articleTitle");')
|
||||
assert title == collected_title, "Unable to add the article to your collection."
|
||||
|
||||
# 退出登录
|
||||
logout(driver)
|
||||
|
||||
# 再次登录并检查收藏状态
|
||||
login(driver, URL, username, password)
|
||||
rechecked_title = driver.execute_script('return localStorage.getItem("articleTitle");')
|
||||
assert title == rechecked_title, "Collected article not found after re-login."
|
||||
|
||||
except Exception as e:
|
||||
# 输出异常信息
|
||||
logging.error(e)
|
||||
finally:
|
||||
driver.quit()
|
|
@ -1,55 +0,0 @@
|
|||
import random
|
||||
import string
|
||||
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 helper import signup
|
||||
|
||||
|
||||
def has_punctuation(s):
|
||||
return any(c in string.punctuation for c in s)
|
||||
|
||||
|
||||
def login(driver, home, uname, password):
|
||||
driver.get(home)
|
||||
WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, '登录'))).click()
|
||||
driver.find_element(By.ID, 'username').send_keys(uname)
|
||||
driver.find_element(By.ID, 'password').send_keys(password)
|
||||
driver.find_element(By.XPATH, '//button[text()="登录"]').click()
|
||||
WebDriverWait(driver, 10).until(EC.title_is(f"EnglishPal Study Room for {uname}"))
|
||||
|
||||
|
||||
def select_valid_word(driver):
|
||||
elem = driver.find_element(By.ID, 'text-content')
|
||||
essay_content = elem.text
|
||||
valid_word = random.choice([word for word in essay_content.split() if len(word) >= 6 and not has_punctuation(
|
||||
word) and 'font>' not in word and 'br>' not in word and 'p>' not in word])
|
||||
driver.find_element(By.ID, 'selected-words').send_keys(valid_word)
|
||||
return valid_word
|
||||
|
||||
|
||||
def test_save_selected_word(driver, URL):
|
||||
try:
|
||||
username, password = signup(URL, driver)
|
||||
word = select_valid_word(driver)
|
||||
stored_words = driver.execute_script('return localStorage.getItem("selectedWords");')
|
||||
assert word == stored_words, "Selected word not saved to localStorage correctly"
|
||||
# 退出并重新登录以检查存储的单词
|
||||
driver.find_element(By.LINK_TEXT, '退出').click()
|
||||
driver.execute_script("window.open('');window.close();")
|
||||
|
||||
# 等待一会儿,让浏览器有足够的时间关闭标签页
|
||||
WebDriverWait(driver, 2)
|
||||
|
||||
# 重新打开一个新的标签页
|
||||
driver.execute_script("window.open('');")
|
||||
driver.switch_to.window(driver.window_handles[-1]) # 切换到新打开的标签页
|
||||
|
||||
login(driver, URL, username, password)
|
||||
textarea_content = driver.find_element(By.ID, 'selected-words').get_attribute('value')
|
||||
assert word == textarea_content, "Selected word not preserved after re-login"
|
||||
finally:
|
||||
driver.quit()
|
|
@ -1,44 +0,0 @@
|
|||
import random
|
||||
import string
|
||||
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.webdriver.common.action_chains import ActionChains
|
||||
|
||||
from helper import signup
|
||||
|
||||
def has_punctuation(s):
|
||||
return any(c in string.punctuation for c in s)
|
||||
|
||||
def select_one(driver):
|
||||
elem = driver.find_element(By.ID, 'article')
|
||||
essay_content = elem.text
|
||||
valid_word = random.choice([word for word in essay_content.split() if len(word) >= 6 and not has_punctuation(
|
||||
word) and 'font>' not in word and 'br>' not in word and 'p>' not in word])
|
||||
driver.find_element(By.ID, 'selected-words').send_keys(valid_word)
|
||||
driver.find_element(By.ID, 'article').click()
|
||||
return valid_word
|
||||
|
||||
def select_two(driver):
|
||||
word = driver.find_element(By.CLASS_NAME, 'highlighted')
|
||||
|
||||
# 创建ActionChains对象
|
||||
actions = ActionChains(driver)
|
||||
actions.move_to_element(word)
|
||||
|
||||
# 模拟鼠标按下并拖动以选择文本
|
||||
actions.double_click()
|
||||
actions.perform()
|
||||
|
||||
|
||||
def test_selected_second_word(driver, URL):
|
||||
try:
|
||||
signup(URL, driver)
|
||||
selected_words = select_one(driver);
|
||||
assert selected_words.strip() != "", "选中的单词被放置框中"
|
||||
select_two(driver)
|
||||
selected_second_words = driver.find_element(By.ID, 'selected-words').get_attribute('value')
|
||||
assert selected_second_words.strip() == "", "选中的单词被删除"
|
||||
finally:
|
||||
driver.quit()
|
|
@ -1,39 +0,0 @@
|
|||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from helper import signup
|
||||
|
||||
|
||||
def test_highlight(driver, URL):
|
||||
try:
|
||||
# 打开网页
|
||||
driver.get(URL)
|
||||
driver.maximize_window()
|
||||
|
||||
# 注册
|
||||
signup(URL, driver)
|
||||
|
||||
# 取消勾选“划词入库按钮”
|
||||
highlight_checkbox = driver.find_element_by_id("chooseCheckbox")
|
||||
driver.execute_script("arguments[0].click();", highlight_checkbox)
|
||||
|
||||
article = driver.find_element_by_id("article")
|
||||
|
||||
# 创建 ActionChains 对象
|
||||
actions = ActionChains(driver)
|
||||
|
||||
# 移动鼠标到起点位置
|
||||
actions.move_to_element(article)
|
||||
# actions.move_to_element_with_offset(article, 50, 100)
|
||||
# 按下鼠标左键
|
||||
actions.click_and_hold()
|
||||
# 拖动鼠标到结束位置
|
||||
actions.move_by_offset(400,50)
|
||||
# 释放鼠标左键
|
||||
actions.release()
|
||||
# 执行操作链
|
||||
actions.perform()
|
||||
# time.sleep(10)
|
||||
|
||||
assert driver.find_elements_by_class_name("highlighted") is not None
|
||||
finally:
|
||||
# 测试结束后关闭浏览器
|
||||
driver.quit()
|
|
@ -1,37 +0,0 @@
|
|||
import time
|
||||
import pytest
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.alert import Alert
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from helper import signup
|
||||
|
||||
def test_bug551(driver, URL):
|
||||
driver.maximize_window()
|
||||
driver.get(URL)
|
||||
|
||||
username, password = signup(URL, driver)
|
||||
|
||||
article = driver.find_element(By.ID, 'article')
|
||||
actions = ActionChains(driver)
|
||||
|
||||
actions.move_to_element(article)
|
||||
actions.click_and_hold()
|
||||
actions.move_by_offset(450, 200)
|
||||
actions.release()
|
||||
actions.perform()
|
||||
|
||||
# 获取选中高亮部分的单词的元素
|
||||
highlighted_words = driver.find_elements(By.CLASS_NAME, 'highlighted')
|
||||
|
||||
# 验证选中部分的单词是否同时应用了需求样式
|
||||
expected_font_weight = "400"
|
||||
|
||||
for word in highlighted_words:
|
||||
font_weight = word.value_of_css_property("font-weight")
|
||||
assert font_weight == expected_font_weight, f"选中部分的单词的字体样式错误"
|
||||
|
||||
time.sleep(5)
|
||||
driver.quit()
|
|
@ -1,58 +0,0 @@
|
|||
from selenium import webdriver
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import logging
|
||||
import time
|
||||
import pytest
|
||||
|
||||
@pytest.mark.parametrize("test_input,expected",
|
||||
[("‘test1’", "test1"),
|
||||
("'test2'", "test2"),
|
||||
("“test3”", "test3"),
|
||||
("it's", "it's"),
|
||||
("hello,I'm linshan", ["hello","i'm","linshan"]),
|
||||
("Happy New Year!?", ["happy","new","year"]),
|
||||
("My favorite book is 《Harry Potter》。", ["potter","harry","my","favorite","book","is"])])
|
||||
def test_bug553_LinShan(test_input,expected, driver, URL):
|
||||
try:
|
||||
# 打开对应地址的网页
|
||||
driver.get(URL)
|
||||
|
||||
# 浏览器最大窗口化
|
||||
driver.maximize_window()
|
||||
|
||||
# 判断网页源代码中是否有English Pal -文字
|
||||
assert 'English Pal -' in driver.page_source
|
||||
|
||||
# 将测试的数据输入到主页的textarea里
|
||||
driver.find_element_by_xpath("//textarea[@name='content']").send_keys(Keys.CONTROL, "a")
|
||||
driver.find_element_by_xpath("//textarea[@name='content']").send_keys(test_input)
|
||||
time.sleep(1)
|
||||
|
||||
# 点击按钮获取单词
|
||||
driver.find_element_by_xpath("//input[@value='get文章中的词频']").click()
|
||||
time.sleep(1)
|
||||
|
||||
# 获取筛选后的单词
|
||||
words = driver.find_elements_by_xpath("//p/a")
|
||||
|
||||
# 遍历获取到的单词,并判断单词与预期的相同
|
||||
for word in words:
|
||||
# 判断单词是否在预期结果中
|
||||
assert word.text in expected
|
||||
|
||||
# 返回上一页网页
|
||||
driver.find_element_by_xpath("//input[@value='确定并返回']").click()
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
# 输出异常信息
|
||||
logging.error(e)
|
||||
# 关闭浏览器
|
||||
driver.quit()
|
||||
finally:
|
||||
driver.quit()
|
|
@ -1,43 +0,0 @@
|
|||
import time
|
||||
import pytest
|
||||
import uuid
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver import ActionChains
|
||||
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, NoSuchElementException, \
|
||||
TimeoutException
|
||||
from conftest import URL
|
||||
driver = webdriver.Chrome()
|
||||
def test_bug555():
|
||||
try:
|
||||
driver.maximize_window()
|
||||
base_url = "http://127.0.0.1:5000"
|
||||
driver.get(base_url)
|
||||
article = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, 'article')))
|
||||
perform_actions_on_article(driver, article)
|
||||
|
||||
next_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, 'load_next_article')))
|
||||
next_button.click()
|
||||
print("Clicked next article button.")
|
||||
|
||||
prev_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, 'load_pre_article')))
|
||||
prev_button.click()
|
||||
print("Clicked previous article button.")
|
||||
|
||||
except (TimeoutException, NoSuchElementException) as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
finally:
|
||||
driver.quit()
|
||||
print("Driver closed.")
|
||||
|
||||
def perform_actions_on_article(driver, article):
|
||||
actions = ActionChains(driver)
|
||||
actions.move_to_element(article)
|
||||
actions.click_and_hold()
|
||||
actions.move_by_offset(450, 200)
|
||||
actions.release()
|
||||
actions.perform()
|
||||
print("Performed actions on article.")
|
|
@ -1,27 +0,0 @@
|
|||
import random
|
||||
import string
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
|
||||
def test_bug561_LiangZiyue(driver, URL):
|
||||
try:
|
||||
driver.get(home)
|
||||
WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, '登录'))).click()
|
||||
driver.find_element(By.ID, 'username').send_keys("wrr")
|
||||
driver.find_element(By.ID, 'password').send_keys("1234")
|
||||
driver.find_element(By.XPATH, '//button[text()="登录"]').click()
|
||||
ele = driver.find_element(By.XPATH,'//font[@id="article"]')
|
||||
driver.execute_script('arguments[0].scrollIntoView();',ele)
|
||||
action = ActionChains(driver)
|
||||
action.click_and_hold(ele)
|
||||
action.move_by_offset(0,500)
|
||||
action.perform()
|
||||
next_ele = driver.find_element(By.ID,'//button[@id="load_next_article"]')
|
||||
driver.execute_script('arguments[0].scrollIntoView();',next_ele)
|
||||
next_ele.click()
|
||||
driver.execute_script('arguments[0].scrollIntoView();',ele)
|
||||
ele.click()
|
||||
finally:
|
||||
driver.quit()
|
|
@ -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()
|
|
@ -1,91 +0,0 @@
|
|||
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
|
|
@ -1,94 +0,0 @@
|
|||
# Run this test script on the command line:
|
||||
# pytest test_vocabulary.py
|
||||
#
|
||||
# Last modified by Mr Lan Hui on 2025-03-05
|
||||
|
||||
from vocabulary import UserVocabularyLevel, ArticleVocabularyLevel
|
||||
|
||||
|
||||
def test_article_level_empty_content():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('')
|
||||
assert article.level == 0
|
||||
|
||||
def test_article_level_punctuation_only():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel(',')
|
||||
assert article.level == 0
|
||||
|
||||
def test_article_level_digit_only():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('1')
|
||||
assert article.level == 0
|
||||
|
||||
def test_article_level_single_word():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('source')
|
||||
assert 2 <= article.level <= 4
|
||||
|
||||
def test_article_level_subset_vs_superset():
|
||||
''' Boundary case test '''
|
||||
article1 = ArticleVocabularyLevel('source')
|
||||
article2 = ArticleVocabularyLevel('open source')
|
||||
assert article1.level < article2.level
|
||||
|
||||
def test_article_level_multiple_words():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('Producing Open Source Software - How to Run a Successful Free Software Project')
|
||||
assert 3 <= article.level <= 5
|
||||
|
||||
def test_article_level_short_paragraph():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('At parties, people no longer give me a blank stare when I tell them I work in open source software. "Oh, yes — like Linux?" they say. I nod eagerly in agreement. "Yes, exactly! That\'s what I do." It\'s nice not to be completely fringe anymore. In the past, the next question was usually fairly predictable: "How do you make money doing that?" To answer, I\'d summarize the economics of free software: that there are organizations in whose interest it is to have certain software exist, but that they don\'t need to sell copies, they just want to make sure the software is available and maintained, as a tool instead of as a rentable monopoly.')
|
||||
assert 4 <= article.level <= 6
|
||||
|
||||
def test_article_level_medium_paragraph():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('In considering the Origin of Species, it is quite conceivable that a naturalist, reflecting on the mutual affinities of organic beings, on their embryological relations, their geographical distribution, geological succession, and other such facts, might come to the conclusion that each species had not been independently created, but had descended, like varieties, from other species. Nevertheless, such a conclusion, even if well founded, would be unsatisfactory, until it could be shown how the innumerable species inhabiting this world have been modified, so as to acquire that perfection of structure and coadaptation which most justly excites our admiration. Naturalists continually refer to external conditions, such as climate, food, etc., as the only possible cause of variation. In one very limited sense, as we shall hereafter see, this may be true; but it is preposterous to attribute to mere external conditions, the structure, for instance, of the woodpecker, with its feet, tail, beak, and tongue, so admirably adapted to catch insects under the bark of trees. In the case of the misseltoe, which draws its nourishment from certain trees, which has seeds that must be transported by certain birds, and which has flowers with separate sexes absolutely requiring the agency of certain insects to bring pollen from one flower to the other, it is equally preposterous to account for the structure of this parasite, with its relations to several distinct organic beings, by the effects of external conditions, or of habit, or of the volition of the plant itself.')
|
||||
assert 5 <= article.level <= 7
|
||||
|
||||
def test_article_level_long_paragraph():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('These several facts accord well with my theory. I believe in no fixed law of development, causing all the inhabitants of a country to change abruptly, or simultaneously, or to an equal degree. The process of modification must be extremely slow. The variability of each species is quite independent of that of all others. Whether such variability be taken advantage of by natural selection, and whether the variations be accumulated to a greater or lesser amount, thus causing a greater or lesser amount of modification in the varying species, depends on many complex contingencies,—on the variability being of a beneficial nature, on the power of intercrossing, on the rate of breeding, on the slowly changing physical conditions of the country, and more especially on the nature of the other inhabitants with which the varying species comes into competition. Hence it is by no means surprising that one species should retain the same identical form much longer than others; or, if changing, that it should change less. We see the same fact in geographical distribution; for instance, in the land-shells and coleopterous insects of Madeira having come to differ considerably from their nearest allies on the continent of Europe, whereas the marine shells and birds have remained unaltered. We can perhaps understand the apparently quicker rate of change in terrestrial and in more highly organised productions compared with marine and lower productions, by the more complex relations of the higher beings to their organic and inorganic conditions of life, as explained in a former chapter. When many of the inhabitants of a country have become modified and improved, we can understand, on the principle of competition, and on that of the many all-important relations of organism to organism, that any form which does not become in some degree modified and improved, will be liable to be exterminated. Hence we can see why all the species in the same region do at last, if we look to wide enough intervals of time, become modified; for those which do not change will become extinct.')
|
||||
assert 6 <= article.level <= 8
|
||||
|
||||
def test_user_level_empty_dictionary():
|
||||
''' Boundary case test '''
|
||||
user = UserVocabularyLevel({})
|
||||
assert user.level == 0
|
||||
|
||||
def test_user_level_one_simple_word():
|
||||
''' Boundary case test '''
|
||||
user = UserVocabularyLevel({'simple':['202408050930']})
|
||||
assert 0 < user.level <= 4
|
||||
|
||||
def test_user_level_invalid_word():
|
||||
''' Boundary case test '''
|
||||
user = UserVocabularyLevel({'xyz':['202408050930']})
|
||||
assert user.level == 0
|
||||
|
||||
def test_user_level_one_hard_word():
|
||||
''' Boundary case test '''
|
||||
user = UserVocabularyLevel({'pasture':['202408050930']})
|
||||
assert 5 <= user.level <= 8
|
||||
|
||||
def test_user_level_multiple_words():
|
||||
''' Boundary case test '''
|
||||
user = UserVocabularyLevel(
|
||||
{'sessile': ['202408050930'], 'putrid': ['202408050930'], 'prodigal': ['202408050930'], 'presumptuous': ['202408050930'], 'prehension': ['202408050930'], 'pied': ['202408050930'], 'pedunculated': ['202408050930'], 'pasture': ['202408050930'], 'parturition': ['202408050930'], 'ovigerous': ['202408050930'], 'ova': ['202408050930'], 'orifice': ['202408050930'], 'obliterate': ['202408050930'], 'niggard': ['202408050930'], 'neuter': ['202408050930'], 'locomotion': ['202408050930'], 'lineal': ['202408050930'], 'glottis': ['202408050930'], 'frivolous': ['202408050930'], 'frena': ['202408050930'], 'flotation': ['202408050930'], 'ductus': ['202408050930'], 'dorsal': ['202408050930'], 'dearth': ['202408050930'], 'crustacean': ['202408050930'], 'cornea': ['202408050930'], 'contrivance': ['202408050930'], 'collateral': ['202408050930'], 'cirriped': ['202408050930'], 'canon': ['202408050930'], 'branchiae': ['202408050930'], 'auditory': ['202408050930'], 'articulata': ['202408050930'], 'alimentary': ['202408050930'], 'adduce': ['202408050930'], 'aberration': ['202408050930']}
|
||||
)
|
||||
assert 6 <= user.level <= 8
|
||||
|
||||
def test_user_level_consider_only_most_recent_words_difficult_words_most_recent():
|
||||
''' Consider only the most recent three words '''
|
||||
user = UserVocabularyLevel(
|
||||
{'pasture':['202408050930'], 'putrid': ['202408040000'], 'frivolous':['202408030000'], 'simple':['202408020000'], 'apple':['202408010000']}
|
||||
)
|
||||
assert 5 <= user.level <= 8
|
||||
|
||||
def test_user_level_consider_only_most_recent_words_easy_words_most_recent():
|
||||
''' Consider only the most recent three words '''
|
||||
user = UserVocabularyLevel(
|
||||
{'simple':['202408050930'], 'apple': ['202408040000'], 'happy':['202408030000'], 'pasture':['202408020000'], 'putrid':['202408010000'], 'dearth':['202407310000']}
|
||||
)
|
||||
assert 4 <= user.level <= 5
|
|
@ -1,52 +0,0 @@
|
|||
import requests
|
||||
import hashlib
|
||||
import time
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# 假设这是从某个配置文件中读取的
|
||||
class BaiduContent:
|
||||
APPID = '20240702002090356'
|
||||
SECRET = '3CcqcMAJdIIpgG0uMS_f'
|
||||
|
||||
def generate_sign(q, salt):
|
||||
"""生成百度翻译API所需的签名"""
|
||||
appid = BaiduContent.APPID
|
||||
secret = BaiduContent.SECRET
|
||||
appid_with_data = appid + q + salt + secret
|
||||
md5_obj = hashlib.md5(appid_with_data.encode('utf-8'))
|
||||
return md5_obj.hexdigest()
|
||||
|
||||
def translate(q, from_lang, to_lang):
|
||||
"""调用百度翻译API进行翻译"""
|
||||
salt = str(int(time.time())) # 生成一个时间戳作为salt
|
||||
sign = generate_sign(q, salt)
|
||||
|
||||
# 封装请求参数
|
||||
params = {
|
||||
'q': q,
|
||||
'from': from_lang,
|
||||
'to': to_lang,
|
||||
'appid': BaiduContent.APPID,
|
||||
'salt': salt,
|
||||
'sign': sign
|
||||
}
|
||||
|
||||
# 构造请求URL(百度翻译API使用POST请求,并将参数放在请求体中)
|
||||
url = "http://api.fanyi.baidu.com/api/trans/vip/translate"
|
||||
|
||||
# 发送POST请求
|
||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
data = urlencode(params).encode('utf-8') # 注意:需要编码为bytes
|
||||
|
||||
response = requests.post(url, data=data, headers=headers)
|
||||
|
||||
# 检查响应状态码
|
||||
if response.status_code == 200:
|
||||
# 解析并返回JSON响应体中的翻译结果
|
||||
try:
|
||||
return response.json()['trans_result'][0]['dst']
|
||||
except (KeyError, IndexError):
|
||||
return "Invalid response from API"
|
||||
else:
|
||||
# 返回错误信息或状态码
|
||||
return {"error": f"Failed with status code {response.status_code}"}
|
|
@ -1,5 +1,5 @@
|
|||
from datetime import datetime
|
||||
from admin_service import ADMIN_NAME
|
||||
|
||||
from flask import *
|
||||
|
||||
# from app import Yaml
|
||||
|
@ -15,57 +15,26 @@ from wordfreqCMD import sort_in_descending_order
|
|||
import pickle_idea
|
||||
import pickle_idea2
|
||||
|
||||
import logging
|
||||
logging.basicConfig(filename='log.txt', format='%(asctime)s %(message)s', level=logging.DEBUG)
|
||||
|
||||
# 初始化蓝图
|
||||
userService = Blueprint("user_bp", __name__)
|
||||
|
||||
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')
|
||||
|
||||
@userService.route("/<username>/reset", methods=['GET', 'POST'])
|
||||
def user_reset(username):
|
||||
'''
|
||||
用户界面
|
||||
:param username: 用户名
|
||||
:return: 返回页面内容
|
||||
'''
|
||||
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
|
||||
logging.debug('/get_next_article: start calling get_today_arcile()')
|
||||
visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles'))
|
||||
logging.debug('/get_next_arcile: done.')
|
||||
data = {
|
||||
'visited_articles': visited_articles,
|
||||
'today_article': today_article,
|
||||
'result_of_generate_article': result_of_generate_article
|
||||
}
|
||||
session['articleID'] = None
|
||||
return redirect(url_for('user_bp.userpage', username=username))
|
||||
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):
|
||||
|
@ -79,7 +48,7 @@ def unfamiliar(username, word):
|
|||
pickle_idea.unfamiliar(user_freq_record, word)
|
||||
session['thisWord'] = word # 1. put a word into session
|
||||
session['time'] = 1
|
||||
return "success"
|
||||
return redirect(url_for('user_bp.userpage', username=username))
|
||||
|
||||
|
||||
@userService.route("/<username>/<word>/familiar", methods=['GET', 'POST'])
|
||||
|
@ -94,7 +63,7 @@ def familiar(username, word):
|
|||
pickle_idea.familiar(user_freq_record, word)
|
||||
session['thisWord'] = word # 1. put a word into session
|
||||
session['time'] = 1
|
||||
return "success"
|
||||
return redirect(url_for('user_bp.userpage', username=username))
|
||||
|
||||
|
||||
@userService.route("/<username>/<word>/del", methods=['GET', 'POST'])
|
||||
|
@ -107,12 +76,11 @@ def deleteword(username, word):
|
|||
'''
|
||||
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"
|
||||
flash(f'<strong>{word}</strong> is no longer in your word list.')
|
||||
return redirect(url_for('user_bp.userpage', username=username))
|
||||
|
||||
|
||||
@userService.route("/<username>/userpage", methods=['GET', 'POST'])
|
||||
@userService.route("/<username>", methods=['GET', 'POST'])
|
||||
def userpage(username):
|
||||
'''
|
||||
用户界面
|
||||
|
@ -126,7 +94,7 @@ def userpage(username):
|
|||
# 用户过期
|
||||
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)
|
||||
return render_template('expiry.html')
|
||||
|
||||
# 获取session里的用户名
|
||||
username = session.get('username')
|
||||
|
@ -149,21 +117,27 @@ def userpage(username):
|
|||
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,加载前端的显示页面
|
||||
user_level,text_level,article_date,article_title,article_body,question_part,answer_part = get_today_article(user_freq_record, session['articleID'])
|
||||
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,
|
||||
flashed_messages=get_flashed_messages_if_any(),
|
||||
d=d,
|
||||
user_level=user_level,
|
||||
text_level=text_level,
|
||||
article_date=article_date,
|
||||
article_title=article_title,
|
||||
article_body=article_body,
|
||||
question_part=question_part,
|
||||
answer_part=answer_part,
|
||||
d_len=len(d),
|
||||
lst3=lst3,
|
||||
yml=Yaml.yml,
|
||||
words=words)
|
||||
|
||||
|
||||
|
||||
|
||||
@userService.route("/<username>/mark", methods=['GET', 'POST'])
|
||||
def user_mark_word(username):
|
||||
'''
|
||||
|
@ -178,17 +152,10 @@ def user_mark_word(username):
|
|||
d = load_freq_history(user_freq_record)
|
||||
lst_history = pickle_idea2.dict2lst(d)
|
||||
lst = []
|
||||
lst2 = []
|
||||
for word in request.form.getlist('marked'):
|
||||
if not word in pickle_idea2.exclusion_lst and len(word) > 2:
|
||||
lst.append((word, [get_time()]))
|
||||
lst2.append(word)
|
||||
lst.append((word, [get_time()]))
|
||||
d = pickle_idea2.merge_frequency(lst, lst_history)
|
||||
if len(lst_history) > 999:
|
||||
flash('You have way too many words in your difficult-words book. Delete some first.')
|
||||
else:
|
||||
pickle_idea2.save_frequency_to_pickle(d, user_freq_record)
|
||||
flash('Added %s.' % ', '.join(lst2))
|
||||
pickle_idea2.save_frequency_to_pickle(d, user_freq_record)
|
||||
return redirect(url_for('user_bp.userpage', username=username))
|
||||
else:
|
||||
return 'Under construction'
|
||||
|
@ -200,3 +167,15 @@ def get_time():
|
|||
'''
|
||||
return datetime.now().strftime('%Y%m%d%H%M') # upper to minutes
|
||||
|
||||
def get_flashed_messages_if_any():
|
||||
'''
|
||||
在用户界面显示黄色提示信息
|
||||
:return: 包含HTML标签的提示信息
|
||||
'''
|
||||
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
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
'''
|
||||
Estimate a user's vocabulary level given his vocabulary data
|
||||
Estimate an English article's difficulty level given its content
|
||||
Preliminary design
|
||||
|
||||
Hui, 2024-09-23
|
||||
Last upated: 2024-09-25, 2024-09-30
|
||||
'''
|
||||
|
||||
import pickle
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def load_record(pickle_fname):
|
||||
with open(pickle_fname, 'rb') as f:
|
||||
d = pickle.load(f)
|
||||
return d
|
||||
|
||||
|
||||
class VocabularyLevelEstimator:
|
||||
_test = load_record('words_and_tests.p') # 单词到来源的映射
|
||||
_source_levels = { # 来源到难度分数的映射
|
||||
'BBC': 1,
|
||||
'CET4': 2,
|
||||
'CET6': 3,
|
||||
'GRADUATE': 4,
|
||||
'OXFORD3000': 1,
|
||||
'TOEFL': 5,
|
||||
'IELTS': 5,
|
||||
'GRE': 7
|
||||
}
|
||||
|
||||
def get_word_level(self, word):
|
||||
"""获取单词难度分数"""
|
||||
if word in self._test:
|
||||
sources = self._test[word]
|
||||
word_levels = [
|
||||
self._source_levels[src]
|
||||
for src in sources
|
||||
if src in self._source_levels
|
||||
]
|
||||
if word_levels:
|
||||
# 使用最高分
|
||||
return max(word_levels)
|
||||
return 0 # 未知单词难度为0
|
||||
|
||||
|
||||
class UserVocabularyLevel(VocabularyLevelEstimator):
|
||||
def __init__(self, d, recent_count=3):
|
||||
self.d = d
|
||||
# 按时间戳排序(最新的在前)
|
||||
sorted_words = sorted(d.items(), key=lambda x: max(x[1]), reverse=True)
|
||||
# 取最近的单词(默认3个)
|
||||
self.word_lst = [word for word, _ in sorted_words[:recent_count]]
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
if not self.word_lst:
|
||||
return 0.0
|
||||
|
||||
# 使用最高分
|
||||
max_score = 0
|
||||
for word in self.word_lst:
|
||||
score = self.get_word_level(word)
|
||||
if score > max_score:
|
||||
max_score = score
|
||||
return max_score
|
||||
|
||||
|
||||
class ArticleVocabularyLevel(VocabularyLevelEstimator):
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
# 更智能的分词,处理连字符和缩写
|
||||
words = re.findall(r'\b[\w-]+\b', content.lower())
|
||||
|
||||
# 计算每个单词的频率和分数
|
||||
word_freq = defaultdict(int)
|
||||
word_scores = {}
|
||||
|
||||
for word in words:
|
||||
if word.isalpha():
|
||||
word_freq[word] += 1
|
||||
if word not in word_scores:
|
||||
word_scores[word] = self.get_word_level(word)
|
||||
|
||||
# 计算加权分数(频率 * 分数)
|
||||
weighted_scores = []
|
||||
for word, score in word_scores.items():
|
||||
if score > 0:
|
||||
weighted_scores.append((score * word_freq[word], score, word))
|
||||
|
||||
# 如果没有有效单词,直接返回
|
||||
if not weighted_scores:
|
||||
self.difficult_words = []
|
||||
return
|
||||
|
||||
# 按加权分数排序
|
||||
weighted_scores.sort(reverse=True)
|
||||
|
||||
# 只保留前20%的单词(至少5个,最多15个)
|
||||
num_top_words = max(5, min(15, len(weighted_scores) // 5))
|
||||
self.difficult_words = [score for _, score, _ in weighted_scores[:num_top_words]]
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
if not self.difficult_words:
|
||||
return 0.0
|
||||
|
||||
# 使用最高分
|
||||
return max(self.difficult_words)
|
||||
|
||||
if __name__ == '__main__':
|
||||
d = load_record('frequency_mrlan85.pickle')
|
||||
print(d)
|
||||
user = UserVocabularyLevel(d)
|
||||
print(user.level) # level is a property
|
||||
article = ArticleVocabularyLevel('This is an interesting article')
|
||||
print(article.level)
|
||||
|
||||
|
||||
|
|
@ -4,38 +4,11 @@
|
|||
###########################################################################
|
||||
|
||||
import collections
|
||||
import html
|
||||
import string
|
||||
import operator
|
||||
import os, sys # 引入模块sys,因为我要用里面的sys.argv列表中的信息来读取命令行参数。
|
||||
import pickle_idea
|
||||
|
||||
|
||||
def map_percentages_to_levels(percentages):
|
||||
'''
|
||||
功能:按照加权平均难度,给生词本计算难度分,计算权重的规则是(10 - 该词汇难度) * 该难度词汇占总词汇的比例,再进行归一化处理
|
||||
输入:难度占比字典,键代表难度3~8,值代表每种难度的单词的占比
|
||||
输出:权重字典,键代表难度3~8,值代表每种难度的单词的权重
|
||||
'''
|
||||
# 已排序的键
|
||||
sorted_keys = sorted(percentages.keys())
|
||||
|
||||
# 计算权重和权重总和
|
||||
sum = 0 # 总和
|
||||
levels_proportions = {}
|
||||
for k in sorted_keys:
|
||||
levels_proportions[k] = 10 - k
|
||||
for k in sorted_keys:
|
||||
levels_proportions[k] *= percentages[k]
|
||||
sum += levels_proportions[k]
|
||||
|
||||
# 归一化权重到权重总和为1
|
||||
for k in sorted_keys:
|
||||
levels_proportions[k] /= sum
|
||||
|
||||
return levels_proportions
|
||||
|
||||
|
||||
def freq(fruit):
|
||||
'''
|
||||
功能: 把字符串转成列表。 目的是得到每个单词的频率。
|
||||
|
@ -66,8 +39,7 @@ def file2str(fname):#文件转字符
|
|||
|
||||
|
||||
def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。
|
||||
special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|,。?!¥……()、《》【】:;·' # 把里面的字符都去掉
|
||||
s = html.unescape(s) # 将HTML实体转换为对应的字符,比如<会被识别为小于号
|
||||
special_characters = '_©~=+[]*&$%^@.,?!:;#()"“”—‘’' # 把里面的字符都去掉
|
||||
for c in special_characters:
|
||||
s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况
|
||||
s = s.replace('--', ' ')
|
||||
|
@ -98,7 +70,7 @@ def sort_in_ascending_order(lst):# 单词按频率降序排列
|
|||
return lst2
|
||||
|
||||
|
||||
def make_html_page(lst, fname): # 只是在wordfreqCMD.py中的main函数中调用,所以不做修改
|
||||
def make_html_page(lst, fname):
|
||||
'''
|
||||
功能:把lst的信息存到fname中,以html格式。
|
||||
'''
|
||||
|
@ -132,7 +104,7 @@ if __name__ == '__main__':
|
|||
print('%s\t%d\t%s' % (x[0], x[1], youdao_link(x[0])))#函数导出
|
||||
|
||||
# 把频率的结果放result.html中
|
||||
make_html_page(sort_in_descending_order(L), 'result.html')
|
||||
make_html_page(sort_in_descending_order(L), 'result.html')
|
||||
|
||||
print('\nHistory:\n')
|
||||
if os.path.exists('frequency.p'):
|
||||
|
|
Binary file not shown.
5
build.sh
5
build.sh
|
@ -1,8 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
DEPLOYMENT_DIR=/home/lanhui/englishpal2/EnglishPal
|
||||
DEPLOYMENT_DIR=/home/lanhui/EnglishPal
|
||||
cd $DEPLOYMENT_DIR
|
||||
pwd
|
||||
|
||||
# Stop service
|
||||
sudo docker stop EnglishPal
|
||||
|
@ -12,7 +11,7 @@ sudo docker rm EnglishPal
|
|||
sudo docker build -t englishpal .
|
||||
|
||||
# Run the application
|
||||
sudo docker run --restart=always -d --name EnglishPal -p 90:80 -v ${DEPLOYMENT_DIR}/app/static/frequency:/app/static/frequency --mount type=volume,src=englishpal-db,target=/app/db -t englishpal # for permanently saving data
|
||||
sudo docker run -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
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
Flask==2.0.3
|
||||
Flask==1.1.2
|
||||
selenium==3.141.0
|
||||
PyYAML~=6.0
|
||||
pony==0.7.16
|
||||
snowballstemmer==2.2.0
|
||||
Werkzeug==2.2.2
|
||||
requests
|
||||
pytest~=8.1.1
|
||||
Flask-HTTPAuth==4.4.0
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
"C:\Users\Kq Aseri\Desktop\package\软件项目管理\EnglishPal\EnglishPal\.venv\Scripts\python.exe" "G:/PyCharm 2024.3.1.1/plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py" --path "C:\Users\Kq Aseri\Desktop\package\软件项目管理\EnglishPal\EnglishPal\app\test_vocabulary.py"
|
||||
Testing started at 上午12:51 ...
|
||||
Launching pytest with arguments C:\Users\Kq Aseri\Desktop\package\软件项目管理\EnglishPal\EnglishPal\app\test_vocabulary.py --no-header --no-summary -q in C:\Users\Kq Aseri\Desktop\package\软件项目管理\EnglishPal\EnglishPal\app
|
||||
|
||||
============================= test session starts =============================
|
||||
collecting ... collected 16 items
|
||||
|
||||
test_vocabulary.py::test_article_level_empty_content PASSED [ 6%]
|
||||
test_vocabulary.py::test_article_level_punctuation_only PASSED [ 12%]
|
||||
test_vocabulary.py::test_article_level_digit_only PASSED [ 18%]
|
||||
test_vocabulary.py::test_article_level_single_word PASSED [ 25%]
|
||||
test_vocabulary.py::test_article_level_subset_vs_superset PASSED [ 31%]
|
||||
test_vocabulary.py::test_article_level_multiple_words PASSED [ 37%]
|
||||
test_vocabulary.py::test_article_level_short_paragraph PASSED [ 43%]
|
||||
test_vocabulary.py::test_article_level_medium_paragraph PASSED [ 50%]
|
||||
test_vocabulary.py::test_article_level_long_paragraph FAILED [ 56%]
|
||||
test_vocabulary.py:49 (test_article_level_long_paragraph)
|
||||
6 != 5
|
||||
|
||||
Expected :5
|
||||
Actual :6
|
||||
<Click to see difference>
|
||||
|
||||
def test_article_level_long_paragraph():
|
||||
''' Boundary case test '''
|
||||
article = ArticleVocabularyLevel('These several facts accord well with my theory. I believe in no fixed law of development, causing all the inhabitants of a country to change abruptly, or simultaneously, or to an equal degree. The process of modification must be extremely slow. The variability of each species is quite independent of that of all others. Whether such variability be taken advantage of by natural selection, and whether the variations be accumulated to a greater or lesser amount, thus causing a greater or lesser amount of modification in the varying species, depends on many complex contingencies,—on the variability being of a beneficial nature, on the power of intercrossing, on the rate of breeding, on the slowly changing physical conditions of the country, and more especially on the nature of the other inhabitants with which the varying species comes into competition. Hence it is by no means surprising that one species should retain the same identical form much longer than others; or, if changing, that it should change less. We see the same fact in geographical distribution; for instance, in the land-shells and coleopterous insects of Madeira having come to differ considerably from their nearest allies on the continent of Europe, whereas the marine shells and birds have remained unaltered. We can perhaps understand the apparently quicker rate of change in terrestrial and in more highly organised productions compared with marine and lower productions, by the more complex relations of the higher beings to their organic and inorganic conditions of life, as explained in a former chapter. When many of the inhabitants of a country have become modified and improved, we can understand, on the principle of competition, and on that of the many all-important relations of organism to organism, that any form which does not become in some degree modified and improved, will be liable to be exterminated. Hence we can see why all the species in the same region do at last, if we look to wide enough intervals of time, become modified; for those which do not change will become extinct.')
|
||||
> assert 6 <= article.level <= 8
|
||||
E assert 6 <= 5
|
||||
E + where 5 = <vocabulary.ArticleVocabularyLevel object at 0x000001FF1BB0E5D0>.level
|
||||
|
||||
test_vocabulary.py:53: AssertionError
|
||||
|
||||
test_vocabulary.py::test_user_level_empty_dictionary PASSED [ 62%]
|
||||
test_vocabulary.py::test_user_level_one_simple_word PASSED [ 68%]
|
||||
test_vocabulary.py::test_user_level_invalid_word PASSED [ 75%]
|
||||
test_vocabulary.py::test_user_level_one_hard_word PASSED [ 81%]
|
||||
test_vocabulary.py::test_user_level_multiple_words FAILED [ 87%]
|
||||
test_vocabulary.py:74 (test_user_level_multiple_words)
|
||||
6 != 1
|
||||
|
||||
Expected :1
|
||||
Actual :6
|
||||
<Click to see difference>
|
||||
|
||||
def test_user_level_multiple_words():
|
||||
''' Boundary case test '''
|
||||
user = UserVocabularyLevel(
|
||||
{'sessile': ['202408050930'], 'putrid': ['202408050930'], 'prodigal': ['202408050930'], 'presumptuous': ['202408050930'], 'prehension': ['202408050930'], 'pied': ['202408050930'], 'pedunculated': ['202408050930'], 'pasture': ['202408050930'], 'parturition': ['202408050930'], 'ovigerous': ['202408050930'], 'ova': ['202408050930'], 'orifice': ['202408050930'], 'obliterate': ['202408050930'], 'niggard': ['202408050930'], 'neuter': ['202408050930'], 'locomotion': ['202408050930'], 'lineal': ['202408050930'], 'glottis': ['202408050930'], 'frivolous': ['202408050930'], 'frena': ['202408050930'], 'flotation': ['202408050930'], 'ductus': ['202408050930'], 'dorsal': ['202408050930'], 'dearth': ['202408050930'], 'crustacean': ['202408050930'], 'cornea': ['202408050930'], 'contrivance': ['202408050930'], 'collateral': ['202408050930'], 'cirriped': ['202408050930'], 'canon': ['202408050930'], 'branchiae': ['202408050930'], 'auditory': ['202408050930'], 'articulata': ['202408050930'], 'alimentary': ['202408050930'], 'adduce': ['202408050930'], 'aberration': ['202408050930']}
|
||||
)
|
||||
> assert 6 <= user.level <= 8
|
||||
E assert 6 <= 1
|
||||
E + where 1 = <vocabulary.UserVocabularyLevel object at 0x000001FF1BB0DCD0>.level
|
||||
|
||||
test_vocabulary.py:80: AssertionError
|
||||
|
||||
test_vocabulary.py::test_user_level_consider_only_most_recent_words_difficult_words_most_recent PASSED [ 93%]
|
||||
test_vocabulary.py::test_user_level_consider_only_most_recent_words_easy_words_most_recent PASSED [100%]
|
||||
|
||||
======================== 2 failed, 14 passed in 0.10s =========================
|
||||
|
||||
Process finished with exit code 1
|
Loading…
Reference in New Issue