Compare commits

...

6 Commits

Author SHA1 Message Date
mrlan 43c719b6b2 Merge pull request 'Fix bug 501 - 特殊字符&加入生词库后删除按钮失效' (#81) from Bug501-Hui into master
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/81
2023-01-31 16:45:50 +08:00
Hui Lan a1955341c6 Fix bug 501 - 特殊字符&加入生词库后删除按钮失效 2023-01-31 16:39:11 +08:00
mrlan 92a8b4a994 Lanhui-update-README2 (#80)
Co-authored-by: Lan Hui <1348141770@qq.com>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/80
Co-authored-by: mrlan <mrlan@noreply.121.4.94.30>
Co-committed-by: mrlan <mrlan@noreply.121.4.94.30>
2023-01-30 15:44:01 +08:00
王志豪 e10dbf9d67 Bug507-WuWenZhuo (#70)
### 修复了生词簿为空时,双击文章单词无法高亮的问题
通过增加一个判断,判断生词簿为空时,不把生词簿的内容进行处理,仅处理选中单词。

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

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

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

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

Co-authored-by: 2658626578 <2658626578@qq.com>
Co-authored-by: Hui Lan <lanhui@zjnu.edu.cn>
Reviewed-on: http://121.4.94.30:3000/mrlan/EnglishPal/pulls/63
Co-authored-by: 陈秋伟 <2658626578@qq.com>
Co-committed-by: 陈秋伟 <2658626578@qq.com>
2023-01-29 11:49:27 +08:00
6 changed files with 228 additions and 67 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

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

View File

@ -5,7 +5,6 @@ from Login import check_username_availability, verify_user, add_user, get_expiry
# 初始化蓝图 # 初始化蓝图
accountService = Blueprint("accountService", __name__) accountService = Blueprint("accountService", __name__)
### Sign-up, login, logout ### ### Sign-up, login, logout ###
@accountService.route("/signup", methods=['GET', 'POST']) @accountService.route("/signup", methods=['GET', 'POST'])
def signup(): def signup():
@ -20,6 +19,7 @@ def signup():
# POST方法需判断是否注册成功再根据结果返回不同的内容 # POST方法需判断是否注册成功再根据结果返回不同的内容
username = escape(request.form['username']) username = escape(request.form['username'])
password = escape(request.form['password']) password = escape(request.form['password'])
password2 = escape(request.form['password2'])
#! 添加如下代码为了过滤注册时的非法字符 #! 添加如下代码为了过滤注册时的非法字符
warn = WarningMessage(username) warn = WarningMessage(username)
@ -32,6 +32,8 @@ def signup():
return render_template('signup.html') return render_template('signup.html')
elif len(password.strip()) < 4: # 密码过短 elif len(password.strip()) < 4: # 密码过短
return '密码过于简单。' return '密码过于简单。'
elif password != password2:
return '确认密码与输入密码不一致!'
else: # 添加账户信息 else: # 添加账户信息
add_user(username, password) add_user(username, password)
verified = verify_user(username, password) verified = verify_user(username, password)
@ -48,6 +50,7 @@ def signup():
return '用户名密码验证失败。' return '用户名密码验证失败。'
@accountService.route("/login", methods=['GET', 'POST']) @accountService.route("/login", methods=['GET', 'POST'])
def login(): def login():
''' '''

View File

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

View File

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

View File

@ -19,19 +19,33 @@
{% endif %} {% endif %}
<title>EnglishPal Study Room for {{ username }}</title> <title>EnglishPal Study Room for {{ username }}</title>
<style>
.shaking {
animation: shakes 1600ms ease-in-out;
}
@keyframes shakes {
10%, 90% { transform: translate3d(-1px, 0, 0); }
20%, 50% { transform: translate3d(+2px, 0, 0); }
30%, 70% { transform: translate3d(-4px, 0, 0); }
40%, 60% { transform: translate3d(+4px, 0, 0); }
50% { transform: translate3d(-4px, 0, 0); }
}
</style>
</head> </head>
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<p><b>English Pal for <font id="username" color="red">{{ username }}</font></b> <p><b>English Pal for <font id="username" color="red">{{ username }}</font></b>
<a class="btn btn-secondary" href="/logout" role="button" onclick="stopRead()">退出</a> <a class="btn btn-secondary" href="/logout" role="button">退出</a>
<a class="btn btn-secondary" href="/reset" role="button" onclick="stopRead()">重设密码</a> <a class="btn btn-secondary" href="/reset" role="button">重设密码</a>
</p> </p>
{{ flashed_messages|safe }} {{ flashed_messages|safe }}
<a class="btn btn-success" href="/{{ username }}/reset" role="button" onclick="stopRead()"> 下一篇 Next Article </a> <a class="btn btn-success" href="/{{ username }}/reset" role="button"> 下一篇 Next Article </a>
{% if session.get('articleID') != session.get('old_articleID') %} {% if session.get('articleID') != session.get('old_articleID') %}
{% if session.get('old_articleID') != None %} {% if session.get('old_articleID') != None %}
<a class="btn btn-success" href="/{{ username }}/back" role="button" onclick="stopRead()"> 上一篇 Previous Article </a> <a class="btn btn-success" href="/{{ username }}/back" role="button"> 上一篇 Previous Article </a>
{% endif%} {% endif%}
{% endif %} {% endif %}
@ -52,7 +66,7 @@
<p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p> <p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p>
<form method="post" action="/{{ username }}"> <form method="post" action="/{{ username }}">
<textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/> <textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/>
<input type="submit" onclick="stopRead()" value="把生词加入我的生词库"/> <input type="submit" value="把生词加入我的生词库"/>
<input type="reset" value="清除"/> <input type="reset" value="清除"/>
</form> </form>
{% if session.get['thisWord'] %} {% if session.get['thisWord'] %}
@ -67,22 +81,30 @@
{% endif %} {% endif %}
{% if d_len > 0 %} {% if d_len > 0 %}
<p><b>我的生词簿</b></p> <p>
{% for x in lst3 %} <b>我的生词簿</b>
{% set word = x[0] %} <label for="move_dynamiclly">
{% set freq = x[1] %} <input type="checkbox" name="move_dynamiclly" id="move_dynamiclly" checked>
{% if session.get('thisWord') == x[0] and session.get('time') == 1 %} 允许动态调整顺序
<a name="aaa"></a> </label>
{% endif %} </p>
<p id='p_{{ word }}' class="new-word" > <a name="aaa"></a>
<a id="word_{{ word }}" class="btn btn-light" href='http://youdao.com/w/eng/{{ word }}/#keyfrom=dict2.index' <div class="word-container">
role="button">{{ word }}</a> {% for x in lst3 %}
( <a id="freq_{{ word }}" title="{{ word }}">{{ freq }}</a> ) {% set word = x[0] %}
<a class="btn btn-success" onclick="familiar('{{ word }}')" role="button">熟悉</a> {% set freq = x[1] %}
<a class="btn btn-warning" onclick="unfamiliar('{{ word }}')" role="button">不熟悉</a> {% if session.get('thisWord') == x[0] and session.get('time') == 1 %}
<a class="btn btn-danger" onclick="delete_word('{{ word }}')" role="button">删除</a> {% endif %}
</p> <p id='p_{{ word }}' class="new-word" >
{% endfor %} <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>
</p>
{% endfor %}
</div>
<input id="selected-words2" type="hidden" value="{{ words }}"> <input id="selected-words2" type="hidden" value="{{ words }}">
{% endif %} {% endif %}
</div> </div>