Compare commits
	
		
			279 Commits 
		
	
	
		
			master
			...
			Alpha-snap
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 68e4ba33c5 | |
|  | d9512c929b | |
|  | ce58f14406 | |
|  | 05dc0ecbb7 | |
|  | 8859827eac | |
|  | b6a36b1d1a | |
|  | 12752341db | |
|  | 3abebdfb21 | |
|  | 92331ca7a0 | |
|  | 1166d3499f | |
|  | fe1d4c29ff | |
|  | aaf2db8657 | |
|  | e478217343 | |
|  | 4f95935037 | |
|  | 370a215d08 | |
|  | de81f5966d | |
|  | 38206271c6 | |
|  | 172727f95f | |
|  | 46d1793397 | |
|  | 74186df292 | |
|  | db652d3bf2 | |
|  | b43174ffba | |
|  | 74f47b786e | |
|  | d00432cee1 | |
|  | 19ea14b38c | |
|  | 621ac24991 | |
|  | ceb5f14ee9 | |
|  | 8f31b030ea | |
|  | 909119b587 | |
|  | e6c945bac7 | |
|  | 0c6616d52c | |
|  | 44d9c39d4b | |
|  | b2e11aea6f | |
|  | 2bec0642dd | |
|  | 62dd580974 | |
|  | 47a359c798 | |
|  | fb190b0563 | |
|  | 262604e761 | |
|  | 61a0b39507 | |
|  | 391e859d30 | |
|  | c453317ad8 | |
|  | f9003ece69 | |
|  | 146781df50 | |
|  | 329732038f | |
|  | 0bfb35b978 | |
|  | 13a091be72 | |
|  | 30ff57909e | |
|  | fb1f927e83 | |
|  | 3b6d88bd68 | |
|  | eff0d01fa0 | |
|  | 8f38e5fd87 | |
|  | b39cf2e4fa | |
|  | 6f4b91fd73 | |
|  | 09beaff831 | |
|  | ebfe7416e6 | |
|  | 98027158b5 | |
|  | 92a1af4222 | |
|  | 79dff9a3b5 | |
|  | bd4fb6846b | |
|  | 2e203699c0 | |
|  | d502b3f474 | |
|  | 66899376c3 | |
|  | 6cc7f043da | |
|  | 1d80fa8886 | |
|  | dc3ef2bc9f | |
|  | 9eac40493b | |
|  | fe6a28a81d | |
|  | bf8bb1a6bc | |
|  | 7a97e25b8c | |
|  | eb3268a139 | |
|  | 220e108ee1 | |
|  | af59afc7e0 | |
|  | 3049a4314a | |
|  | d8263d17e0 | |
|  | 794dcf399c | |
|  | 9bbd3a978d | |
|  | 6b3efad1dc | |
|  | faf5ec14a4 | |
|  | 70fc469f5e | |
|  | 9dab83219a | |
|  | 7bf1a958b5 | |
|  | 55eac68160 | |
|  | dc3fd67b02 | |
|  | 02f3c4fdfa | |
|  | 109a9447a7 | |
|  | 84e0fc51f7 | |
|  | 18472acd3d | |
|  | 75c96a60df | |
|  | 5657d8d5ee | |
|  | 1f718e201f | |
|  | 7cf9c7cf56 | |
|  | 6633df7b70 | |
|  | ccf22af9df | |
|  | 3383e411ec | |
|  | db66c8ed86 | |
|  | 9215339b1e | |
|  | cb576b40ed | |
|  | f4488672ec | |
|  | eb2051ca3f | |
|  | 00ae770195 | |
|  | 3db629b57d | |
|  | 54d09469f5 | |
|  | 4b06915dc4 | |
|  | 8664da12de | |
|  | 2d765b5c63 | |
|  | be7ae9e296 | |
|  | 2095550f33 | |
|  | 1c08e80236 | |
|  | d2d383f21a | |
|  | d83809dcc7 | |
|  | 2785ded52b | |
|  | e3db7c30b0 | |
|  | 9f03cc49fa | |
|  | eed852cd66 | |
|  | c6f8a3448d | |
|  | d8af2a7e54 | |
|  | a0c9b82ee7 | |
|  | 4a42c5c22c | |
|  | 101c359596 | |
|  | 64a82bee22 | |
|  | 9537024339 | |
|  | 85a3a9ce6a | |
|  | f3b9fd8790 | |
|  | 624426b6cf | |
|  | d1589f6062 | |
|  | fcd2c83904 | |
|  | 29a673cd92 | |
|  | 1bec0bd107 | |
|  | 630cb6befa | |
|  | 5dbdf60a2c | |
|  | 101506a511 | |
|  | 35a6f1c828 | |
|  | a42e63dc27 | |
|  | 6500eeca84 | |
|  | 7d65782728 | |
|  | d8e4fbbb2d | |
|  | 9b3551bbc8 | |
|  | 90ac789bb4 | |
|  | 8575a0dc33 | |
|  | 3373ba1429 | |
|  | d296580b3b | |
|  | a4db1edffb | |
|  | 49ead93bcb | |
|  | 9afd38a09a | |
|  | d97743649a | |
|  | 9f6a007426 | |
|  | 7fe3feae9a | |
|  | b2a53a0e40 | |
|  | 0301c39cf0 | |
|  | 5857395251 | |
|  | 77ed63441b | |
|  | b19eabd225 | |
|  | b9d0ad4a15 | |
|  | aca827a912 | |
|  | 23f0dfd8ca | |
|  | 6ee94d1610 | |
|  | 416f40222e | |
|  | 66f5c28ead | |
|  | c9c0ef60d8 | |
|  | a4e64ee4a0 | |
|  | 1d23062c47 | |
|  | df34770af0 | |
|  | b626df7c4b | |
|  | 184656230f | |
|  | 2d9aa8fa61 | |
|  | bd5f8f63f5 | |
|  | 2500fa5fc8 | |
|  | ddcc5206d5 | |
|  | 6b3ad77c44 | |
|  | 739094e844 | |
|  | 8d76133cca | |
|  | 6dbb1e2c06 | |
|  | 8ef62395ed | |
|  | 374da86174 | |
|  | 393264a6fe | |
|  | d5c76674da | |
|  | b8222c951b | |
|  | 4120e7e54b | |
|  | 8e769587cf | |
|  | 9aa718b236 | |
|  | 230e8e92dc | |
|  | 4ea6d9aeed | |
|  | 768c81828d | |
|  | 9901d887e0 | |
|  | 498639a753 | |
|  | 1ded133056 | |
|  | db66b59513 | |
|  | 692d8cf453 | |
|  | 426d131f64 | |
|  | 4fe96cfc9c | |
|  | e8fbccdcf7 | |
|  | 93264da3d9 | |
|  | 64b9c51fab | |
|  | 0fd1592036 | |
|  | 46233ead1e | |
|  | b5bde9d33d | |
|  | 83491ce28c | |
|  | 77a3adb546 | |
|  | 4f91659713 | |
|  | 083cbfd040 | |
|  | 0dc253bc19 | |
|  | a4608db424 | |
|  | 0a63c5354a | |
|  | b7fe68c54d | |
|  | 85a3faaa9f | |
|  | 2966a8162f | |
|  | b8f2919959 | |
|  | f164465903 | |
|  | 292972c0ce | |
|  | 04c4064c68 | |
|  | 7d5b1c0ed4 | |
|  | d9e28e3a2b | |
|  | 41d1d9619d | |
|  | 30b54f8023 | |
|  | 1e3ac7a379 | |
|  | 8dd6a2a343 | |
|  | d2f30daab1 | |
|  | ed1d0fd714 | |
|  | f3aa407c56 | |
|  | e9ac50422b | |
|  | f4df263d6e | |
|  | dff560cc73 | |
|  | c110de0393 | |
|  | aaabd3e3bb | |
|  | 9da1a1cae6 | |
|  | 9b1d60748d | |
|  | 83bbd8f600 | |
|  | 1b211f107d | |
|  | 10c291bed2 | |
|  | 6d15b65e3c | |
|  | e4f870c995 | |
|  | 06f896a33a | |
|  | 25c2e0aca8 | |
|  | dca76969eb | |
|  | 00ae957b27 | |
|  | a397c756cf | |
|  | 14ab63c85c | |
|  | b8c3d9bda7 | |
|  | 43419ab4b6 | |
|  | 78d9a66e88 | |
|  | 79bdec2a7d | |
|  | fb80e952b9 | |
|  | 6ea0b970a2 | |
|  | 20051e1a93 | |
|  | 5711f0e826 | |
|  | cc92e5e29a | |
|  | cd562a745c | |
|  | c284893097 | |
|  | 87fd594636 | |
|  | 18c37d583a | |
|  | 472c0c115f | |
|  | 9a156ebf7e | |
|  | 807d74741b | |
|  | 287d496ae9 | |
|  | 582f399f73 | |
|  | c37ee98b77 | |
|  | f40a388277 | |
|  | 2277473afe | |
|  | f01c334827 | |
|  | 4d2535a6e8 | |
|  | bb2d0363e4 | |
|  | 0ed7657747 | |
|  | 2fb3003808 | |
|  | 9816596cf8 | |
|  | 682247bff1 | |
|  | b22c654f0f | |
|  | d402bb45cb | |
|  | cdf6180901 | |
|  | 38837c9c2f | |
|  | a0ddf4bdad | |
|  | dc37f5f229 | |
|  | e9ec65e7a5 | |
|  | df64065dcc | |
|  | ce28b91bd5 | |
|  | d6bd24ee1c | |
|  | e9e2bd3d23 | |
|  | 320a99d479 | |
|  | 3eca9234a9 | |
|  | 8924166975 | 
|  | @ -2,12 +2,20 @@ | |||
| venv/ | ||||
| app/__init__.py | ||||
| app/__pycache__/ | ||||
| .DS_Store | ||||
| app/.DS_Store | ||||
| app/sqlite_commands.py | ||||
| app/static/usr/*.jpg | ||||
| app/static/img/ | ||||
| app/static/frequency/frequency_*.pickle | ||||
| app/static/frequency/frequency.p | ||||
| app/static/wordfreqapp.db | ||||
| app/wordfreqapp.db | ||||
| app/db/wordfreqapp.db | ||||
| app/static/donate-the-author.jpg | ||||
| app/static/donate-the-author-hidden.jpg | ||||
| app/model/__pycache__/ | ||||
| app/model/__pycache__/ | ||||
| app/test/__pycache__/ | ||||
| app/test/.pytest_cache/ | ||||
| app/test/pytest_report.html | ||||
| app/test/assets | ||||
| app/log.txt | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| FROM tiangolo/uwsgi-nginx-flask:python3.6 | ||||
| COPY requirements.txt /app | ||||
| RUN pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ | ||||
| COPY ./app /app | ||||
| FROM tiangolo/uwsgi-nginx-flask:python3.8-alpine | ||||
| COPY requirements.txt /tmp | ||||
| COPY ./app/ /app/ | ||||
| RUN pip3 install -U pip -i https://mirrors.aliyun.com/pypi/simple/ | ||||
| RUN pip3 install -r /tmp/requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ | ||||
|  |  | |||
|  | @ -10,8 +10,8 @@ pipeline { | |||
|     stages { | ||||
|         stage('MakeDatabasefile') { | ||||
| 	    steps { | ||||
| 	        sh 'touch ./app/static/wordfreqapp.db && rm -f ./app/static/wordfreqapp.db'  | ||||
| 	        sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/static/wordfreqapp.db' | ||||
| 	        sh 'touch ./app/wordfreqapp.db && rm -f ./app/wordfreqapp.db'  | ||||
| 	        sh 'cat ./app/static/wordfreqapp.sql | sqlite3 ./app/wordfreqapp.db' | ||||
| 	    } | ||||
| 	} | ||||
|         stage('BuildIt') { | ||||
|  |  | |||
							
								
								
									
										30
									
								
								README.md
								
								
								
								
							
							
						
						
									
										30
									
								
								README.md
								
								
								
								
							|  | @ -61,15 +61,15 @@ My steps for deploying English on a Ubuntu server. | |||
| 
 | ||||
| 
 | ||||
| All articles are stored in the `article` table in a SQLite file called | ||||
| `app/static/wordfreqapp.db`. | ||||
| `app/db/wordfreqapp.db`. | ||||
| 
 | ||||
| ### Adding new articles | ||||
| 
 | ||||
| To add articles, open and edit `app/static/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org). | ||||
| To add articles, open and edit `app/db/wordfreqapp.db` using DB Browser for SQLite (https://sqlitebrowser.org). | ||||
| 
 | ||||
| ### Extending an account's expiry date | ||||
| 
 | ||||
| By default, an account's expiry is 30 days after first sign-up.  To extend account's expiry date, open and edit `user` table in `app/static/wordfreqapp.db`.  Simply update field `expiry_date`. | ||||
| By default, an account's expiry is 30 days after first sign-up.  To extend account's expiry date, open and edit `user` table in `app/db/wordfreqapp.db`.  Simply update field `expiry_date`. | ||||
| 
 | ||||
| ### Exporting the database | ||||
| 
 | ||||
|  | @ -95,7 +95,7 @@ sqlite3 wordfreqapp.db`.  Delete wordfreqapp.db first if it exists. | |||
| ### Uploading wordfreqapp.db to the server | ||||
| 
 | ||||
| 
 | ||||
| `pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/static` | ||||
| `pscp wordfreqapp.db lanhui@118.*.*.118:/home/lanhui/englishpal2/EnglishPal/app/db/` | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -129,6 +129,28 @@ 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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| from WordFreq import WordFreq | ||||
| from wordfreqCMD import youdao_link, sort_in_descending_order | ||||
| from UseSqlite import InsertQuery, RecordQuery | ||||
| import pickle_idea, pickle_idea2 | ||||
| import os | ||||
| import random, glob | ||||
|  | @ -8,18 +7,37 @@ 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' | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| path_prefix = '/var/www/wordfreq/wordfreq/' | ||||
| path_prefix = './'  # comment this line in deployment | ||||
| def calculate_ratio(oxford_word_count, total_words): | ||||
|     if total_words == 0: | ||||
|         return 0 | ||||
|     return oxford_word_count / total_words | ||||
| 
 | ||||
| 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(): | ||||
|     rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') | ||||
|     rq.instructions("SELECT * FROM article") | ||||
|     rq.do() | ||||
|     result = rq.get_results() | ||||
|     return len(result) | ||||
|     return get_number_of_articles() | ||||
| 
 | ||||
| 
 | ||||
| def get_article_title(s): | ||||
|  | @ -33,32 +51,36 @@ def get_article_body(s): | |||
| 
 | ||||
| 
 | ||||
| def get_today_article(user_word_list, visited_articles): | ||||
|     rq = RecordQuery(path_prefix + 'static/wordfreqapp.db') | ||||
|     if visited_articles is None: | ||||
|         visited_articles = { | ||||
|             "index" : 0,  # 为 article_ids 的索引 | ||||
|             "article_ids": []  # 之前显示文章的id列表,越后越新 | ||||
|         } | ||||
|     if visited_articles["index"] > len(visited_articles["article_ids"])-1:  # 生成新的文章,因此查找所有的文章 | ||||
|         rq.instructions("SELECT * FROM article") | ||||
|         result = get_all_articles() | ||||
|     else:  # 生成阅读过的文章,因此查询指定 article_id 的文章 | ||||
|         if visited_articles["article_ids"][visited_articles["index"]] == 'null':  # 可能因为直接刷新页面导致直接去查询了'null',因此当刷新的页面的时候,需要直接进行“上一篇”操作 | ||||
|             visited_articles["index"] -= 1 | ||||
|             visited_articles["article_ids"].pop() | ||||
|         rq.instructions('SELECT * FROM article WHERE article_id=%d' % (visited_articles["article_ids"][visited_articles["index"]])) | ||||
|     rq.do() | ||||
|     result = rq.get_results() | ||||
|         article_id = visited_articles["article_ids"][visited_articles["index"]] | ||||
|         result = get_article_by_id(article_id) | ||||
|     random.shuffle(result) | ||||
| 
 | ||||
|     # Choose article according to reader's level | ||||
|     d1 = load_freq_history(path_prefix + 'static/frequency/frequency.p') | ||||
|     logging.debug('* get_today_article(): start d1 = ... ') | ||||
|     d1 = load_freq_history(user_word_list) | ||||
|     d2 = load_freq_history(path_prefix + 'static/words_and_tests.p') | ||||
|     logging.debug(' ... get_today_article(): get_difficulty_level_for_user() start') | ||||
|     d3 = get_difficulty_level_for_user(d1, d2) | ||||
|     logging.debug(' ... get_today_article(): done') | ||||
| 
 | ||||
|     d = None | ||||
|     result_of_generate_article = "not found" | ||||
| 
 | ||||
|     d_user = load_freq_history(user_word_list) | ||||
|     logging.debug('* get_today_article(): user_difficulty_level() start') | ||||
|     user_level = user_difficulty_level(d_user, d3)  # more consideration as user's behaviour is dynamic. Time factor should be considered. | ||||
|     logging.debug('* get_today_article(): done') | ||||
|     text_level = 0 | ||||
|     if visited_articles["index"] > len(visited_articles["article_ids"])-1:  # 生成新的文章 | ||||
|         amount_of_visited_articles = len(visited_articles["article_ids"]) | ||||
|  | @ -86,15 +108,19 @@ def get_today_article(user_word_list, visited_articles): | |||
| 
 | ||||
|     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.2f' % user_level, | ||||
|             "text_level": '%4.2f' % text_level, | ||||
|             "user_level": '%4.1f' % user_level, | ||||
|             "text_level": '%4.1f' % text_level, | ||||
|             "date": d['date'], | ||||
|             "article_title": get_article_title(d['text']), | ||||
|             "article_body": get_article_body(d['text']), | ||||
|             "source": d["source"], | ||||
|             "question": get_question_part(d['question']), | ||||
|             "answer": get_answer_part(d['question']) | ||||
|             "answer": get_answer_part(d['question']), | ||||
|             "ratio" : ratio | ||||
|         } | ||||
| 
 | ||||
|     return visited_articles, today_article, result_of_generate_article | ||||
|  |  | |||
							
								
								
									
										66
									
								
								app/Login.py
								
								
								
								
							
							
						
						
									
										66
									
								
								app/Login.py
								
								
								
								
							|  | @ -1,7 +1,8 @@ | |||
| import hashlib | ||||
| import string | ||||
| from datetime import datetime, timedelta | ||||
| from UseSqlite import InsertQuery, RecordQuery | ||||
| import unicodedata | ||||
| 
 | ||||
| 
 | ||||
| def md5(s): | ||||
|     ''' | ||||
|  | @ -12,16 +13,13 @@ def md5(s): | |||
|     h = hashlib.md5(s.encode(encoding='utf-8')) | ||||
|     return h.hexdigest() | ||||
| 
 | ||||
| 
 | ||||
| # import model.user after the defination of md5(s) to avoid circular import | ||||
| from model.user import get_user_by_username, insert_user, update_password_by_username | ||||
| 
 | ||||
| path_prefix = '/var/www/wordfreq/wordfreq/' | ||||
| path_prefix = './'  # comment this line in deployment | ||||
| 
 | ||||
| def verify_pass(newpass,oldpass): | ||||
|     if(newpass==oldpass): | ||||
|         return True | ||||
| 
 | ||||
| 
 | ||||
| def verify_user(username, password): | ||||
|     user = get_user_by_username(username) | ||||
|  | @ -31,7 +29,7 @@ def verify_user(username, password): | |||
| 
 | ||||
| def add_user(username, password): | ||||
|     start_date = datetime.now().strftime('%Y%m%d') | ||||
|     expiry_date = (datetime.now() + timedelta(days=30)).strftime('%Y%m%d') # will expire after 30 days | ||||
|     expiry_date = (datetime.now() + timedelta(days=30)).strftime('%Y%m%d')  # will expire after 30 days | ||||
|     # 将用户名和密码一起加密,以免暴露不同用户的相同密码 | ||||
|     password = md5(username + password) | ||||
|     insert_user(username=username, password=password, start_date=start_date, expiry_date=expiry_date) | ||||
|  | @ -51,12 +49,12 @@ def change_password(username, old_password, new_password): | |||
|     :return: 修改成功:True 否则:False | ||||
|     ''' | ||||
|     if not verify_user(username, old_password):  # 旧密码错误 | ||||
|         return False | ||||
|         return {'error':'Old password is wrong.', 'username':username} | ||||
|     # 将用户名和密码一起加密,以免暴露不同用户的相同密码 | ||||
|     if verify_pass(new_password,old_password): #新旧密码一致 | ||||
|         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 True | ||||
|     return {'success':'Password changed', 'username':username} | ||||
| 
 | ||||
| 
 | ||||
| def get_expiry_date(username): | ||||
|  | @ -66,30 +64,64 @@ def get_expiry_date(username): | |||
|     else: | ||||
|         return user.expiry_date | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
|         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 | ||||
|         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 | ||||
|         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']: | ||||
|         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): | ||||
|     def __init__(self, s, type='username'): | ||||
|         self.s = s | ||||
|         self.type = type | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return UserName(self.s).validate() | ||||
| 
 | ||||
|         if self.type == 'username': | ||||
|             return UserName(self.s).validate() | ||||
|         if self.type == 'password': | ||||
|             return Password(self.s).validate() | ||||
|  |  | |||
|  | @ -1,87 +0,0 @@ | |||
| ########################################################################### | ||||
| # Copyright 2019 (C) Hui Lan <hui.lan@cantab.net> | ||||
| # Written permission must be obtained from the author for commercial uses. | ||||
| ########################################################################### | ||||
| 
 | ||||
| 
 | ||||
| # Reference: Dusty Phillips.  Python 3 Objected-oriented Programming Second Edition. Pages 326-328. | ||||
| # Copyright (C) 2019 Hui Lan | ||||
| 
 | ||||
| import sqlite3 | ||||
| 
 | ||||
| class Sqlite3Template: | ||||
|     def __init__(self, db_fname): | ||||
|         self.db_fname = db_fname | ||||
| 
 | ||||
|     def connect(self, db_fname): | ||||
|         self.conn = sqlite3.connect(self.db_fname) | ||||
| 
 | ||||
|     def instructions(self, query_statement): | ||||
|         raise NotImplementedError() | ||||
| 
 | ||||
|     def operate(self): | ||||
|         self.conn.row_factory = sqlite3.Row | ||||
|         self.results = self.conn.execute(self.query) # self.query is to be given in the child classes | ||||
|         self.conn.commit() | ||||
| 
 | ||||
|     def format_results(self): | ||||
|         raise NotImplementedError() | ||||
| 
 | ||||
|     def do(self): | ||||
|         self.connect(self.db_fname) | ||||
|         self.instructions(self.query) | ||||
|         self.operate() | ||||
| 
 | ||||
|     def instructions_with_parameters(self, query_statement, parameters): | ||||
|         self.query = query_statement | ||||
|         self.parameters = parameters | ||||
| 
 | ||||
|     def do_with_parameters(self): | ||||
|         self.connect(self.db_fname) | ||||
|         self.instructions_with_parameters(self.query, self.parameters) | ||||
|         self.operate_with_parameters() | ||||
| 
 | ||||
|     def operate_with_parameters(self): | ||||
|         self.conn.row_factory = sqlite3.Row | ||||
|         self.results = self.conn.execute(self.query, self.parameters) # self.query is to be given in the child classes | ||||
|         self.conn.commit() | ||||
| 
 | ||||
| 
 | ||||
| class InsertQuery(Sqlite3Template): | ||||
|     def instructions(self, query): | ||||
|         self.query = query | ||||
| 
 | ||||
| 
 | ||||
| class RecordQuery(Sqlite3Template): | ||||
|     def instructions(self, query): | ||||
|         self.query = query | ||||
| 
 | ||||
|     def format_results(self): | ||||
|         output = [] | ||||
|         for row_dict in self.results.fetchall(): | ||||
|             lst = [] | ||||
|             for k in dict(row_dict): | ||||
|                 lst.append( row_dict[k] ) | ||||
|             output.append(', '.join(lst)) | ||||
|         return '\n\n'.join(output) | ||||
| 
 | ||||
|     def get_results(self): | ||||
|         result = [] | ||||
|         for row_dict in self.results.fetchall(): | ||||
|             result.append( dict(row_dict) ) | ||||
|         return result | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
| 
 | ||||
|     #iq = InsertQuery('RiskDB.db') | ||||
|     #iq.instructions("INSERT INTO inspection Values ('FoodSupplies', 'RI2019051301', '2019-05-13', '{}')") | ||||
|     #iq.do() | ||||
|     #iq.instructions("INSERT INTO inspection Values ('CarSupplies', 'RI2019051302', '2019-05-13', '{[{\"risk_name\":\"elevator\"}]}')") | ||||
|     #iq.do() | ||||
| 
 | ||||
|     rq = RecordQuery('wordfreqapp.db') | ||||
|     rq.instructions("SELECT * FROM article WHERE level=3") | ||||
|     rq.do() | ||||
|     #print(rq.format_results()) | ||||
|  | @ -1,7 +1,7 @@ | |||
| from flask import * | ||||
| from markupsafe import escape | ||||
| from Login import check_username_availability, verify_user, add_user, get_expiry_date, change_password, WarningMessage | ||||
| 
 | ||||
| 
 | ||||
| # 初始化蓝图 | ||||
| accountService = Blueprint("accountService", __name__) | ||||
| 
 | ||||
|  | @ -43,7 +43,6 @@ def signup(): | |||
|                 return jsonify({'status': '1'}) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @accountService.route("/login", methods=['GET', 'POST']) | ||||
| def login(): | ||||
|     ''' | ||||
|  | @ -59,17 +58,48 @@ def login(): | |||
|         username = escape(request.form['username']) | ||||
|         password = escape(request.form['password']) | ||||
|         verified = verify_user(username, password) | ||||
|         if verified: | ||||
|             # 登录成功,写入session | ||||
|             session['logged_in'] = True | ||||
|             session[username] = username | ||||
|             session['username'] = username | ||||
|             user_expiry_date = get_expiry_date(username) | ||||
|             session['expiry_date'] = user_expiry_date | ||||
|             session['visited_articles'] = None | ||||
|             return jsonify({'status': '1'}) | ||||
|         else: | ||||
|             return jsonify({'status': '0'}) | ||||
|         #读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'}) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @accountService.route("/logout", methods=['GET', 'POST']) | ||||
|  | @ -83,6 +113,7 @@ def logout(): | |||
|     return redirect(url_for('mainpage')) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @accountService.route("/reset", methods=['GET', 'POST']) | ||||
| def reset(): | ||||
|     ''' | ||||
|  | @ -102,9 +133,7 @@ def reset(): | |||
|         # POST请求用于提交修改后信息 | ||||
|         old_password = escape(request.form['old-password']) | ||||
|         new_password = escape(request.form['new-password']) | ||||
|         flag = change_password(username, old_password, new_password) # flag表示是否修改成功 | ||||
|         if flag: | ||||
|             session['logged_in'] = False | ||||
|             return jsonify({'status':'1'})  # 修改成功 | ||||
|         else: | ||||
|             return jsonify({'status':'2'})  # 修改失败 | ||||
|         result = change_password(username, old_password, new_password) | ||||
|         return jsonify(result) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| # System Library | ||||
| from flask import * | ||||
| from markupsafe import escape | ||||
| 
 | ||||
| # Personal library | ||||
| from Yaml import yml | ||||
|  | @ -37,6 +38,22 @@ def admin(): | |||
| 
 | ||||
| @adminService.route("/admin/article", methods=["GET", "POST"]) | ||||
| def article(): | ||||
| 
 | ||||
|     def _make_title_and_content(article_lst): | ||||
|         for article in article_lst: | ||||
|             text = escape(article.text) # Fix XSS vulnerability, contributed by Xu Xuan | ||||
|             article.title = text.split("\n")[0] | ||||
|             article.content = '<br/>'.join(text.split("\n")[1:]) | ||||
| 
 | ||||
| 
 | ||||
|     def _update_context(): | ||||
|         article_len = get_number_of_articles() | ||||
|         context["article_number"] = article_len | ||||
|         context["text_list"] = get_page_articles(_cur_page, _page_size) | ||||
|         _articles = get_page_articles(_cur_page, _page_size) | ||||
|         _make_title_and_content(_articles) | ||||
|         context["text_list"] = _articles | ||||
| 
 | ||||
|     global _cur_page, _page_size | ||||
| 
 | ||||
|     is_admin = check_is_admin() | ||||
|  | @ -44,20 +61,15 @@ def article(): | |||
|         return is_admin | ||||
| 
 | ||||
|     _article_number = get_number_of_articles() | ||||
| 
 | ||||
|     try: | ||||
|         _page_size = min( | ||||
|             max(1, int(request.args.get("size", 5))), _article_number | ||||
|         )  # 最小的size是1 | ||||
|         _cur_page = min( | ||||
|             max(1, int(request.args.get("page", 1))), _article_number // _page_size + (_article_number % _page_size > 0) | ||||
|         )  # 最小的page是1 | ||||
|         _page_size = min(max(1, int(request.args.get("size", 5))), _article_number)  # 最小的size是1 | ||||
|         _cur_page = min(max(1, int(request.args.get("page", 1))), _article_number // _page_size + (_article_number % _page_size > 0))  # 最小的page是1 | ||||
|     except ValueError: | ||||
|         return "page parmas must be int!" | ||||
|      | ||||
|         return "page parameters must be integer!" | ||||
| 
 | ||||
|     _articles = get_page_articles(_cur_page, _page_size) | ||||
|     for article in _articles:   # 获取每篇文章的title | ||||
|         article.title = article.text.split("\n")[0] | ||||
|         article.content = '<br/>'.join(article.text.split("\n")[1:]) | ||||
|     _make_title_and_content(_articles) | ||||
|      | ||||
|     context = { | ||||
|         "article_number": _article_number, | ||||
|  | @ -67,25 +79,18 @@ def article(): | |||
|         "username": session.get("username"), | ||||
|     } | ||||
| 
 | ||||
|     def _update_context(): | ||||
|         article_len = get_number_of_articles() | ||||
|         context["article_number"] = article_len | ||||
|         context["text_list"] = get_page_articles(_cur_page, _page_size) | ||||
|         _articles = get_page_articles(_cur_page, _page_size) | ||||
|         for article in _articles:   # 获取每篇文章的title | ||||
|             article.title = article.text.split("\n")[0] | ||||
|         context["text_list"] = _articles | ||||
| 
 | ||||
|     if request.method == "GET": | ||||
|         try: | ||||
|             delete_id = int(request.args.get("delete_id", 0)) | ||||
|         except: | ||||
|             return "Delete article ID must be int!" | ||||
|         if delete_id:  # delete article | ||||
|             delete_article_by_id(delete_id) | ||||
|             _update_context() | ||||
|     elif request.method == "POST": | ||||
|     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", "") | ||||
|  | @ -94,9 +99,10 @@ def article(): | |||
|             if level not in ['1', '2', '3', '4']: | ||||
|                 return "Level must be between 1 and 4." | ||||
|             add_article(content, source, level, question) | ||||
|             _update_context() | ||||
|             title = content.split('\n')[0] | ||||
|             flash(f'Article added. Title: {title}') | ||||
|             _update_context()  # 这行应在flash之后 否则会发生新建的文章即点即删 | ||||
| 
 | ||||
|     return render_template("admin_manage_article.html", **context) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,31 @@ | |||
| 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) | ||||
| 
 | ||||
|  | @ -0,0 +1 @@ | |||
| hsy | ||||
|  | @ -0,0 +1 @@ | |||
| Put wordfreqapp.db here | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -7,7 +7,7 @@ | |||
| 
 | ||||
| import pickle | ||||
| import math | ||||
| from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order | ||||
| from wordfreqCMD import remove_punctuation, freq, sort_in_descending_order, sort_in_ascending_order, map_percentages_to_levels | ||||
| import snowballstemmer | ||||
| 
 | ||||
| 
 | ||||
|  | @ -18,6 +18,7 @@ def load_record(pickle_fname): | |||
|     return d | ||||
| 
 | ||||
| 
 | ||||
| ENGLISH_WORD_DIFFICULTY_DICT = {} | ||||
| def convert_test_type_to_difficulty_level(d): | ||||
|     """ | ||||
|     对原本的单词库中的单词进行难度评级 | ||||
|  | @ -39,8 +40,10 @@ def convert_test_type_to_difficulty_level(d): | |||
|         elif 'BBC' in d[k]: | ||||
|             result[k] = 8 | ||||
| 
 | ||||
|     return result  # {'apple': 4, ...} | ||||
|     global ENGLISH_WORD_DIFFICULTY_DICT | ||||
|     ENGLISH_WORD_DIFFICULTY_DICT = result | ||||
| 
 | ||||
|     return result  # {'apple': 4, ...} | ||||
| 
 | ||||
| def get_difficulty_level_for_user(d1, d2): | ||||
|     """ | ||||
|  | @ -49,7 +52,11 @@ def get_difficulty_level_for_user(d1, d2): | |||
|     在d2的后面添加单词,没有新建一个新的字典 | ||||
|     """ | ||||
|     # TODO: convert_test_type_to_difficulty_level() should not be called every time.  Each word's difficulty level should be pre-computed. | ||||
|     d2 = convert_test_type_to_difficulty_level(d2)  # 根据d2的标记评级{'apple': 4, 'abandon': 4, ...} | ||||
|     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:  # 用户的词 | ||||
|  | @ -87,30 +94,58 @@ def revert_dict(d): | |||
|     return d2 | ||||
| 
 | ||||
| 
 | ||||
| def user_difficulty_level(d_user, d): | ||||
| 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 = 0 | ||||
|     geometric = 1 | ||||
|     for date in sorted(d_user2.keys(), | ||||
|                        reverse=True):  # most recently added words are more important while determining user's level | ||||
|     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 | ||||
|         lst2 = []  # a list of tuples, (word, difficulty level) | ||||
|         for word in lst: | ||||
|             if word in d: | ||||
|                 lst2.append((word, d[word])) | ||||
|                 if d[word] not in count: | ||||
|                     count[d[word]] = 0 | ||||
|                 count[d[word]] += 1 | ||||
|                 total += 1 | ||||
| 
 | ||||
|         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) | ||||
|     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 | ||||
| 
 | ||||
|     return geometric ** (1 / max(count, 1)) | ||||
| 
 | ||||
| 
 | ||||
| def text_difficulty_level(s, d): | ||||
|  |  | |||
							
								
								
									
										47
									
								
								app/main.py
								
								
								
								
							
							
						
						
									
										47
									
								
								app/main.py
								
								
								
								
							|  | @ -1,24 +1,29 @@ | |||
| #! /usr/bin/python3 | ||||
| # -*- coding: utf-8 -*- | ||||
| 
 | ||||
| ########################################################################### | ||||
| # Copyright 2019 (C) Hui Lan <hui.lan@cantab.net> | ||||
| # Written permission must be obtained from the author for commercial uses. | ||||
| ########################################################################### | ||||
| from flask import escape | ||||
| from flask import abort, 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 = 'lunch.time!' | ||||
| app.secret_key = os.urandom(32) | ||||
| 
 | ||||
| # 将蓝图注册到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 | ||||
|  | @ -55,6 +60,11 @@ 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(): | ||||
|     ''' | ||||
|  | @ -80,10 +90,23 @@ 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']) | ||||
|         f = WordFreq(content) | ||||
|         lst = f.get_freq() | ||||
|         lst = [ t for t in f.get_freq() if good_word(t[0]) ] # only keep normal words | ||||
|         # save history | ||||
|         d = load_freq_history(path_prefix + 'static/frequency/frequency.p') | ||||
|         lst_history = pickle_idea.dict2lst(d) | ||||
|  | @ -103,7 +126,17 @@ def mainpage(): | |||
|                                d_len=d_len, | ||||
|                                lst=lst, | ||||
|                                yml=Yaml.yml, | ||||
|                                number_of_essays=number_of_essays) | ||||
|                                number_of_essays=number_of_essays, | ||||
|                                ratio = ratio) | ||||
| 
 | ||||
| @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,7 +1,7 @@ | |||
| from pony.orm import * | ||||
| 
 | ||||
| db = Database() | ||||
| db.bind("sqlite", "../static/wordfreqapp.db", create_db=True)  # bind sqlite file | ||||
| db.bind("sqlite", "../db/wordfreqapp.db", create_db=True)  # bind sqlite file | ||||
| 
 | ||||
| 
 | ||||
| class User(db.Entity): | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ def add_article(content, source="manual_input", level="5", question="No question | |||
|         Article( | ||||
|             text=content, | ||||
|             source=source, | ||||
|             date=datetime.now().strftime("%-d %b %Y"),  # format style of `5 Oct 2022` | ||||
|             date=datetime.now().strftime("%d %b %Y"),  # format style of `5 Oct 2022` | ||||
|             level=level, | ||||
|             question=question, | ||||
|         ) | ||||
|  | @ -32,3 +32,17 @@ def get_page_articles(num, size): | |||
|             x | ||||
|             for x in Article.select().order_by(desc(Article.article_id)).page(num, size) | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| def get_all_articles(): | ||||
|     articles = [] | ||||
|     with db_session: | ||||
|         for article in Article.select(): | ||||
|             articles.append(article.to_dict()) | ||||
|     return articles | ||||
| 
 | ||||
| 
 | ||||
| def get_article_by_id(article_id): | ||||
|     with db_session: | ||||
|         article = Article.get(article_id=article_id) | ||||
|     return [article.to_dict()] | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| # 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 | ||||
| 
 | ||||
|  | @ -55,11 +56,13 @@ def save_frequency_to_pickle(d, pickle_fname): | |||
|     f.close() | ||||
| 
 | ||||
| def unfamiliar(path,word): | ||||
|     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) | ||||
|     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) | ||||
| 
 | ||||
| def familiar(path,word): | ||||
|     f = open(path,"rb") | ||||
|  |  | |||
|  | @ -64,7 +64,6 @@ 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: | ||||
|  | @ -73,6 +72,7 @@ def save_frequency_to_pickle(d, pickle_fname): | |||
|     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__': | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,13 +2,14 @@ | |||
| css: | ||||
|   item: | ||||
|     - ../static/css/bootstrap.css | ||||
| 
 | ||||
|     - ../static/css/highlighted.css | ||||
| # 全局引入的js文件地址 | ||||
| js: | ||||
|   head: # 在页面加载之前加载 | ||||
|     - ../static/js/jquery.js | ||||
|     - ../static/js/read.js | ||||
|     - ../static/js/word_operation.js | ||||
|     - ../static/js/checkboxes.js | ||||
|   bottom: # 在页面加载完之后加载 | ||||
|     - ../static/js/fillword.js | ||||
|     - ../static/js/highlight.js | ||||
|  |  | |||
|  | @ -0,0 +1,37 @@ | |||
| /* 按钮(上一篇,下一篇)样式 */ | ||||
| .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; | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| 
 | ||||
| .highlighted { | ||||
|     color: red; | ||||
|     font-weight: normal; | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| 
 | ||||
| #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; | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| function toggleCheckboxSelection(checkStatus) { | ||||
|     // used in userpage_post.html
 | ||||
|     const checkBoxes = document.getElementsByName('marked'); | ||||
|     checkBoxes.forEach((checkbox) => { checkbox.checked = checkStatus;} ); | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| let isRead = true; | ||||
| let isChoose = true; | ||||
| let isRead = localStorage.getItem('readChecked') !== 'false';  // default to true
 | ||||
| let isChoose = localStorage.getItem('chooseChecked') !== 'false'; | ||||
| 
 | ||||
| function getWord() { | ||||
|     return window.getSelection ? window.getSelection() : document.selection.createRange().text; | ||||
|  | @ -8,24 +8,45 @@ function getWord() { | |||
| function fillInWord() { | ||||
|     let word = getWord(); | ||||
|     if (isRead) Reader.read(word, inputSlider.value); | ||||
|     if (!isChoose) return; | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| document.getElementById("text-content").addEventListener("click", fillInWord, false); | ||||
| if (document.getElementById("text-content")) { | ||||
|     document.getElementById("text-content").addEventListener("click", fillInWord, false); | ||||
| } | ||||
| 
 | ||||
| const sliderValue = document.getElementById("rangeValue"); | ||||
| const inputSlider = document.getElementById("rangeComponent"); | ||||
| inputSlider.oninput = () => { | ||||
|     let value = inputSlider.value; | ||||
|     sliderValue.textContent = value + '×'; | ||||
| }; | ||||
| 
 | ||||
| if (inputSlider) { | ||||
|     inputSlider.oninput = () => { | ||||
| 	let value = inputSlider.value; | ||||
| 	sliderValue.textContent = value + '×'; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| function onReadClick() { | ||||
|     isRead = !isRead; | ||||
|     localStorage.setItem('readChecked', isRead); | ||||
| } | ||||
| 
 | ||||
| function onChooseClick() { | ||||
|     isChoose = !isChoose; | ||||
|     localStorage.setItem('chooseChecked', isChoose); | ||||
| } | ||||
| 
 | ||||
| // 如果网页刷新,停止播放声音
 | ||||
| if (performance.getEntriesByType("navigation")[0].type == "reload") { | ||||
|     Reader.stopRead(); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| let isHighlight = true; | ||||
| let isHighlight = localStorage.getItem('highlightChecked') !== 'false'; // default to true
 | ||||
| 
 | ||||
| function cancelBtnHandler() { | ||||
|     cancelHighlighting(); | ||||
|  | @ -9,75 +9,113 @@ function cancelBtnHandler() { | |||
| } | ||||
| 
 | ||||
| function showBtnHandler() { | ||||
|     document.getElementById("text-content").removeEventListener("click", fillInWord2, false); | ||||
|     document.getElementById("text-content").removeEventListener("touchstart", fillInWord2, false); | ||||
|     document.getElementById("text-content").addEventListener("click", fillInWord, false); | ||||
|     document.getElementById("text-content").addEventListener("touchstart", fillInWord, false); | ||||
|     highLight(); | ||||
|     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() : document.selection.createRange().text; | ||||
|     return window.getSelection ? window.getSelection().toString() : document.selection.createRange().text; | ||||
| } | ||||
| 
 | ||||
| function highLight() { | ||||
|     if (!isHighlight) return; | ||||
|     let articleContent = document.getElementById("article").innerText; //将原来的.innerText改为.innerHtml,使用innerText会把原文章中所包含的<br>标签去除,导致处理后的文章内容失去了原来的格式
 | ||||
|     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 = "";  //初始化allWords的值,避免进入判断后编译器认为allWords未初始化的问题
 | ||||
|     if(dictionaryWords != null){//增加一个判断,检查生词本里面是否为空,如果为空,allWords只添加选中的单词
 | ||||
|         allWords = pickedWords.value + " " + dictionaryWords.value; | ||||
|     } | ||||
|     else{ | ||||
|         allWords = pickedWords.value + " "; | ||||
|     } | ||||
|     const list = allWords.split(" ");//将所有的生词放入一个list中,用于后续处理
 | ||||
|     for (let i = 0; i < list.length; ++i) { | ||||
|         list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); //消除单词两边的空字符
 | ||||
|         list[i] = list[i].replace('|', ""); | ||||
|         list[i] = list[i].replace('?', ""); | ||||
|         if (list[i] !== "" && "<mark>".indexOf(list[i]) === -1 && "</mark>".indexOf(list[i]) === -1) { | ||||
|            //将文章中所有出现该单词word的地方改为:"<mark>" + word + "<mark>"。 正则表达式RegExp()中,"\\b"代表单词边界匹配。
 | ||||
|     let 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 articleContent_fb = articleContent;  //文章副本
 | ||||
|             while(articleContent_fb.toLowerCase().indexOf(list[i].toLowerCase()) !== -1 && list[i]!=""){ | ||||
|                 //找到副本中和list[i]匹配的第一个单词(第一种匹配情况),并赋值给list[i]。
 | ||||
|                 const index = articleContent_fb.toLowerCase().indexOf(list[i].toLowerCase()); | ||||
|                 list[i] = articleContent_fb.substring(index, index + list[i].length); | ||||
| 
 | ||||
|                 articleContent_fb = articleContent_fb.substring(index + list[i].length);    // 使用副本中list[i]之后的子串替换掉副本
 | ||||
|                 articleContent = articleContent.replace(new RegExp("\\b"+list[i]+"\\b","g"),"<mark>" + list[i] + "</mark>"); | ||||
|             } | ||||
|     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").innerText;//将原来的.innerText改为.innerHtml,原因同上
 | ||||
|     let pickedWords = document.getElementById("selected-words"); | ||||
|     const dictionaryWords = document.getElementById("selected-words2");     | ||||
|     const list = pickedWords.value.split(" ");     | ||||
|     if (pickedWords != null) { | ||||
|         for (let i = 0; i < list.length; ++i) { | ||||
|             list[i] = list[i].replace(/(^\s*)|(\s*$)/g, ""); | ||||
|             if (list[i] !== "") { //原来判断的代码中,替换的内容为“list[i]”这个字符串,这明显是错误的,我们需要替换的是list[i]里的内容
 | ||||
|                 articleContent = articleContent.replace(new RegExp("<mark>"+list[i]+"</mark>", "g"), list[i]); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     if (dictionaryWords != null) { | ||||
|         let list2 = pickedWords.value.split(" "); | ||||
|         for (let i = 0; i < list2.length; ++i) { | ||||
|             list2 = dictionaryWords.value.split(" "); | ||||
|             list2[i] = list2[i].replace(/(^\s*)|(\s*$)/g, ""); | ||||
|             if (list2[i] !== "") { //原来代码中,替换的内容为“list[i]”这个字符串,这明显是错误的,我们需要替换的是list[i]里的内容
 | ||||
|                 articleContent = articleContent.replace(new RegExp("<mark>"+list2[i]+"</mark>", "g"), list2[i]); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     let articleContent = document.getElementById("article").innerHTML; | ||||
|     articleContent = articleContent.replace(new RegExp('<span class="highlighted">',"gi"), "") | ||||
|     articleContent = articleContent.replace(new RegExp("</span>","gi"), ""); | ||||
|     document.getElementById("article").innerHTML = articleContent; | ||||
| } | ||||
| 
 | ||||
|  | @ -97,6 +135,62 @@ function toggleHighlighting() { | |||
|         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
											
										
									
								
							|  | @ -0,0 +1,200 @@ | |||
| /** | ||||
|  * 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); | ||||
|  | @ -0,0 +1,20 @@ | |||
| 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; | ||||
| } | ||||
|  | @ -3,13 +3,14 @@ var Reader = (function() { | |||
|     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") { | ||||
|             if (ev.name === "word") { | ||||
|                 current_position = ev.charIndex; | ||||
|             } | ||||
|         } | ||||
|  | @ -24,12 +25,24 @@ var Reader = (function() { | |||
|         reader.speak(msg); | ||||
|     } | ||||
| 
 | ||||
|     function updateRate(rate) { | ||||
|         // 停止当前的朗读
 | ||||
|         stopRead(); | ||||
| 
 | ||||
|         // 更新当前速率
 | ||||
|         current_rate = rate; | ||||
| 
 | ||||
|         // 重新开始朗读
 | ||||
|         read(to_speak, current_rate); | ||||
|     } | ||||
| 
 | ||||
|     function stopRead() { | ||||
|         reader.cancel(); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         read: read, | ||||
|         stopRead: stopRead | ||||
|         stopRead: stopRead, | ||||
|         updateRate: updateRate // 添加这一行,将 updateRate 方法暴露出去
 | ||||
|     }; | ||||
| })(); | ||||
| }) (); | ||||
|  |  | |||
|  | @ -1,19 +1,20 @@ | |||
| function familiar(theWord) { | ||||
|     let username = $("#username").text(); | ||||
|     let word = $("#word_" + theWord).text(); | ||||
|     let freq = $("#freq_" + theWord).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){ | ||||
|         success:function(response) { | ||||
|             let new_freq = freq - 1; | ||||
|             const allow_move = document.getElementById("move_dynamiclly").checked; | ||||
|             if (allow_move) { | ||||
| 
 | ||||
|                 if (new_freq <= 0) { | ||||
|                     removeWord(theWord); | ||||
|                 } else { | ||||
|                     renderWord({ word: theWord, freq: new_freq }); | ||||
|                     renderWord({word: theWord, freq: new_freq}); | ||||
|                 } | ||||
|             } else { | ||||
|                 if(new_freq <1) { | ||||
|  | @ -28,16 +29,18 @@ function familiar(theWord) { | |||
| 
 | ||||
| function unfamiliar(theWord) { | ||||
|     let username = $("#username").text(); | ||||
|     let word = $("#word_" + theWord).text(); | ||||
|     let freq = $("#freq_" + theWord).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){ | ||||
|         success:function(response) { | ||||
|             let new_freq = parseInt(freq) + 1; | ||||
|             const allow_move = document.getElementById("move_dynamiclly").checked; | ||||
|             if (allow_move) { | ||||
|                 renderWord({ word: theWord, freq: new_freq }); | ||||
|                 renderWord({word: theWord, freq: new_freq}); | ||||
|             } else { | ||||
|                 $("#freq_" + theWord).text(new_freq); | ||||
|             } | ||||
|  | @ -51,13 +54,19 @@ function delete_word(theWord) { | |||
|     $.ajax({ | ||||
|         type:"GET", | ||||
|         url:"/" + username + "/" + word + "/del", | ||||
|         success:function(response){ | ||||
|         success:function(response) { | ||||
|             const allow_move = document.getElementById("move_dynamiclly").checked; | ||||
|             if (allow_move) { | ||||
|                 removeWord(theWord); | ||||
|             } 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); | ||||
| 	    } | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | @ -69,6 +78,7 @@ function read_word(theWord) { | |||
|     Reader.read(to_speak, inputSlider.value); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /*  | ||||
|  * interface Word { | ||||
|  *   word: string, | ||||
|  | @ -83,7 +93,9 @@ function parseWord(element) { | |||
|     const word = element | ||||
|         .querySelector("a.btn.btn-light[role=button]")  // 获取当前词频元素的词汇元素
 | ||||
|         .innerText  // 获取词汇值;
 | ||||
|     const freq = Number.parseInt(element.querySelector(`#freq_${word}`).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 | ||||
|  | @ -95,17 +107,20 @@ function parseWord(element) { | |||
| */ | ||||
| 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" >
 | ||||
|     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-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为词频元素对应的单词 | ||||
|  | @ -114,7 +129,7 @@ function removeWord(word) { | |||
|     // 根据词频信息删除元素
 | ||||
|     word = word.replace('&', '&'); | ||||
|     const element_to_remove = document.getElementById(`p_${word}`); | ||||
|     if (element_to_remove != null) { | ||||
|     if (element_to_remove !== null) { | ||||
|         element_to_remove.remove(); | ||||
|     } | ||||
| } | ||||
|  | @ -129,7 +144,7 @@ function renderWord(word) { | |||
|     for (const current of container.children) { | ||||
|         const cur_word = parseWord(current); | ||||
|         // 找到第一个词频比它小的元素,插入到这个元素前面
 | ||||
|         if (compareWord(cur_word, word) == -1) { | ||||
|         if (compareWord(cur_word, word) === -1) { | ||||
|             container.insertBefore(new_element, current); | ||||
|             inserted = true; | ||||
|             break; | ||||
|  | @ -165,17 +180,67 @@ function elementFromString(string) { | |||
|  *  当first大于second时返回1 | ||||
|  */ | ||||
| function compareWord(first, second) { | ||||
|     if (first.freq < second.freq) { | ||||
|         return -1; | ||||
|     if (first.freq !== second.freq) { | ||||
|         return first.freq < second.freq ? -1 : 1; | ||||
|     } | ||||
|     if (first.freq > second.freq) { | ||||
|         return 1; | ||||
|     } | ||||
|     if (first.word < second.word) { | ||||
|         return -1; | ||||
|     } | ||||
|     if (first.word > second.word) { | ||||
|         return 1; | ||||
|     if (first.word !== second.word) { | ||||
|         return first.word < second.word ? -1 : 1; | ||||
|     } | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| /* 生词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'; | ||||
|           } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,6 +7,11 @@ | |||
|         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;"> | ||||
|  | @ -66,9 +71,10 @@ | |||
|         <div class="list-group"> | ||||
|             {% for text in text_list %} | ||||
|             <div class="list-group-item list-group-item-action" aria-current="true"> | ||||
|                 <div> | ||||
|                     <a type="button" href="/admin/article?delete_id={{text.article_id}}" class="btn btn-outline-danger btn-sm">删除</a> | ||||
|                 </div> | ||||
|                 <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> | ||||
|  |  | |||
|  | @ -1,45 +1,107 @@ | |||
| {% block body %} | ||||
| {% if session['logged_in'] %} | ||||
| 
 | ||||
| 你已登录 <a href="/{{ session['username'] }}">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>。 | ||||
| 你已登录 <a href="/{{ session['username'] }}/userpage">{{ session['username'] }}</a>。 登出点击<a href="/logout">这里</a>。 | ||||
| 
 | ||||
| {% else %} | ||||
| <meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0, user-scalable=yes" /> | ||||
| <link rel="stylesheet" href="static/css/login_service.css"> | ||||
| <script src="static/js/jquery.js"></script> | ||||
| <script> | ||||
|     function login(){ | ||||
| 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; | ||||
|         } | ||||
|         $.post( | ||||
|             "/login", {'username': username, 'password': password}, | ||||
|             function (response) { | ||||
|                 if (response.status === '0') { | ||||
|                     alert('无法通过验证。'); | ||||
|                     window.location.href = "/login"; | ||||
|                 } else if (response.status === '1') { | ||||
|                     window.location.href = "/"+username+"/userpage"; | ||||
|         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> | ||||
|     <h1>Sign in</h1> | ||||
|   </section> | ||||
| 
 | ||||
|   <input type="text" placeholder="用户名" class="username" id="username"> | ||||
|   <input type="password" placeholder="密码" class="password"  id="password"> | ||||
|   <button type="button" class="btn" onclick="login()">登录</button> | ||||
|   <a class="signup" href="/signup">注册</a> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| {% endif %} | ||||
|  |  | |||
|  | @ -31,12 +31,12 @@ | |||
|             <p><a href="/login">登录</a>  <a href="/signup">注册</a> <a href="/static/usr/instructions.html">使用说明</a></p > | ||||
|             <p><b> {{ random_ads }}。 <a href="/signup">试试</a>吧!</b></p> | ||||
|         {% endif %} | ||||
|         <div class="alert alert-success" role="alert">共有文章 <span class="badge bg-success"> {{ number_of_essays }} </span> 篇</div> | ||||
|         <div class="alert alert-success" role="alert">共有文章 <span class="badge bg-success"> {{ number_of_essays }} </span> 篇,Oxford 5000 单词占比 <span class="badge bg-success"> {{ (ratio * 100) | int }}%  </span> </div> | ||||
|         <p>粘贴1篇文章 (English only)</p> | ||||
|         <form method="post" action="/"> | ||||
|             <textarea name="content" rows="10" cols="120"></textarea><br/> | ||||
|             <textarea name="content" id="article" rows="10" cols="120"></textarea><br/> | ||||
|             <input type="submit" value="get文章中的词频"/> | ||||
|             <input type="reset" value="清除"/> | ||||
|             <input type="reset" value="清除" onclick="clearArticle()"/> | ||||
|         </form> | ||||
|         {% if d_len > 0 %} | ||||
|             <p><b>最常见的词</b></p> | ||||
|  | @ -44,6 +44,7 @@ | |||
|                 <a href="http://youdao.com/w/eng/{{x[0]}}/#keyfrom=dict2.index">{{x[0]}}</a> {{x[1]}} | ||||
|             {% endfor %} | ||||
|         {% endif %} | ||||
|         <p class="text-muted">Version: 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 }} | ||||
|  | @ -52,5 +53,22 @@ | |||
|             <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'] %} | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
|           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(); | ||||
|  | @ -12,6 +13,10 @@ | |||
|                 alert('输入不能为空!'); | ||||
|                 return false; | ||||
|             } | ||||
|             if (old_password.includes(' ') || new_password.includes(' ')) { | ||||
|                 alert('输入不能包含空格!'); | ||||
|                 return false; | ||||
|             } | ||||
|             if (new_password !== re_new_password) { | ||||
|                 alert('新密码不匹配,请重新输入'); | ||||
|                 return false; | ||||
|  | @ -20,15 +25,19 @@ | |||
|                 alert('密码过于简单。(密码长度至少4位)'); | ||||
|                 return false; | ||||
|             } | ||||
|             if (!containsDigitsLettersSpecialCharacters(new_password)) { | ||||
| 		alert('密码过于简单。(密码要包括数字,字母,特殊符号)'); | ||||
| 		return false; | ||||
|             } | ||||
|             $.post("/reset", {'old-password': old_password, 'new-password': new_password}, | ||||
|                 function (response) { | ||||
|                     if (response.status === '1') { | ||||
|                         alert('密码修改成功,请重新登录。'); | ||||
|                         window.location.href = "/login"; | ||||
|                     } else if (response.status === '2') { | ||||
|                         alert('密码修改失败'); | ||||
|                         window.location.href = "/reset"; | ||||
|                    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; | ||||
|  | @ -48,4 +57,4 @@ | |||
|         <button class="btn" onclick="window.location.href='/{{ username }}/userpage'">放弃修改</button> | ||||
| 
 | ||||
|     </div> | ||||
| {% endblock %} | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -1,68 +1,112 @@ | |||
| {% 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"> | ||||
| <script src="static/js/jquery.js"></script> | ||||
|     <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> | ||||
|         function signup() { | ||||
|             let username = $("#username").val(); | ||||
|             let password = $("#password").val(); | ||||
|             let password2 = $("#password2").val(); | ||||
|             if (username === "" || password === "" || password2 === ""){ | ||||
|                 alert('输入不能为空!'); | ||||
|                 return false; | ||||
|             } | ||||
|             if (password !== password2) { | ||||
|                 alert('确认密码与输入密码不一致!'); | ||||
|                 return false; | ||||
|             } | ||||
|             if (password.length < 4) { | ||||
|                 alert('密码过于简单。(密码长度至少4位)'); | ||||
|                 return false; | ||||
|             } | ||||
|             $.post("/signup", {'username': username, 'password': password}, | ||||
|                 function (response) { | ||||
|                     if (response.status === '0') { | ||||
|                         alert('用户名'+username+'已经被注册。'); | ||||
|                         window.location.href = "/signup"; | ||||
|                     } else if (response.status === '1') { | ||||
|                         alert('用户名密码验证失败。'); | ||||
|                         window.location.href = "/signup"; | ||||
|                     } else if (response.status === '2') { | ||||
|                         let f = confirm("恭喜,你已成功注册,你的用户名是"+username+'.\n点击“确认”开始使用,或点击“取消”返回首页'); | ||||
|                         if (f) { | ||||
|                             window.location.href = '/'+username+'/userpage'; | ||||
|                         } else { | ||||
|                             window.location.href = '/'; | ||||
|                         } | ||||
|                     } else if (response.status === '3') { | ||||
|                         alert(response.warn); | ||||
|                     } | ||||
|                 } | ||||
|             ) | ||||
|     var slider | ||||
|     let username,password,password2 | ||||
|     $(document).ready(function() { | ||||
|         slider = new SliderUnlock("#slider", { | ||||
|                 successLabelTip: "验证成功" | ||||
|             }, function() { | ||||
| 
 | ||||
|         }); | ||||
|         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>{{ get_flashed_messages()[0] | safe }}</p> | ||||
| 
 | ||||
| 
 | ||||
| <div class="container"> | ||||
| 
 | ||||
|   <section class="signin-heading"> | ||||
|     <h1>Sign Up</h1> | ||||
|   </section> | ||||
| 
 | ||||
|   <p><input type="username" id="username" placeholder="输入用户名" class="username"></p> | ||||
|   <p><input type="password" id="password" placeholder="输入密码" class="password"></p> | ||||
|   <p><input type="password" id="password2" placeholder="确认密码" class="password" ></p> | ||||
|   <button type="button" class="btn" onclick="signup()">注册</button> | ||||
| 
 | ||||
| </div> | ||||
|    | ||||
| {% endif %} | ||||
| {% endblock %} | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| <!DOCTYPE html> | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|  | @ -6,6 +6,8 @@ | |||
|           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 }} | ||||
|  | @ -23,44 +25,34 @@ | |||
|     <title>EnglishPal Study Room for {{ username }}</title> | ||||
| 
 | ||||
|     <style> | ||||
|         .shaking { | ||||
|             animation: shakes 1600ms ease-in-out; | ||||
|         } | ||||
|       .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); | ||||
|             } | ||||
|         } | ||||
|       @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; | ||||
|         } | ||||
|       .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 { | ||||
| 	  padding: 0; | ||||
| 	  font-size: 20px; | ||||
| 	  line-height: 21px; | ||||
| 	  display: inline-block; | ||||
|       } | ||||
| 
 | ||||
|         .arrow:hover { | ||||
|             cursor: pointer; | ||||
|         } | ||||
|       .arrow:hover { | ||||
| 	  cursor: pointer; | ||||
|       } | ||||
| 
 | ||||
|     </style> | ||||
| </head> | ||||
|  | @ -69,83 +61,87 @@ | |||
|     <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> | ||||
|         <a class="btn btn-secondary" href="/admin" role="button" onclick="stopRead()">管理</a> | ||||
|         {% endif %} | ||||
|         <a id="quit" class="btn btn-secondary" href="/logout" role="button" onclick="stopRead()">退出</a> | ||||
|         <a class="btn btn-secondary" href="/reset" role="button" onclick="stopRead()">重设密码</a> | ||||
| 
 | ||||
|     </p> | ||||
|     {% for message in get_flashed_messages() %} | ||||
|         <div class="alert alert-warning alert-dismissible fade show" role="alert"> | ||||
|             {{ message }} | ||||
|             <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> | ||||
|         </div> | ||||
|     <div class="alert alert-warning alert-dismissible fade show" role="alert"> | ||||
| 	{{ message }} | ||||
| 	<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> | ||||
|     </div> | ||||
|     {% endfor %} | ||||
| 
 | ||||
|     <button class="arrow" id="load_next_article" onclick="load_next_article();Reader.stopRead()" | ||||
|             title="下一篇 Next Article">⇨ | ||||
|     </button> | ||||
|     <button class="arrow" id="load_pre_article" onclick="load_pre_article();Reader.stopRead()" style="display: none" | ||||
|             title="上一篇 Previous Article">⇦ | ||||
|     </button> | ||||
|         <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> | ||||
| 
 | ||||
|     <p><b>阅读文章并回答问题</b></p> | ||||
| 
 | ||||
|     <div id="text-content"> | ||||
|         <div id="found"> | ||||
|             <div class="alert alert-success" role="alert">According to your word list, your level is <span | ||||
|                     class="text-decoration-underline" id="user_level">{{ today_article["user_level"] }}</span> and we | ||||
|                 have chosen an article with a difficulty level of <span class="text-decoration-underline" | ||||
|                                                                         id="text_level">{{ today_article["text_level"] }}</span> | ||||
|                 for you. | ||||
|             </div> | ||||
|           <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/> | ||||
|             <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/> | ||||
| 	    <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') | ||||
|                         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 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> | ||||
|             <p class="text-muted"><span class="badge bg-success">Notes:</span><br>No article is currently available for you. You can try again a few times or mark new words in the passage to improve your level.</p> | ||||
|         </div> | ||||
|         <div class="alert alert-success" role="alert" id="read_all" style="display:none;"> | ||||
|             <p class="text-muted"><span class="badge bg-success">Notes:</span><br>You've read all the articles.</p> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <input type="checkbox" id="highlightCheckbox" onclick="toggleHighlighting()"/>生词高亮 | ||||
|     <input type="checkbox" id="readCheckbox" onclick="onReadClick()"/>大声朗读 | ||||
|     <input type="checkbox" id="chooseCheckbox" onclick="onChooseClick()"/>划词入库 | ||||
|     <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"/> | ||||
|             <input type="range" id="rangeComponent" min="0.5" max="2" value="1" step="0.25" /> | ||||
|         </div> | ||||
|     </div> | ||||
|     <p><b>收集生词吧</b> (可以在正文中划词,也可以复制黏贴)</p> | ||||
|     <form method="post" action="/{{ username }}/userpage"> | ||||
|         <textarea name="content" id="selected-words" rows="10" cols="120"></textarea><br/> | ||||
|         <button class="btn btn-primary btn-lg" type="submit" onclick="Reader.stopRead()">把生词加入我的生词库</button> | ||||
|         <button class="btn btn-primary btn-lg" type="reset" onclick="clearSelectedWords()">清除</button> | ||||
|         <button class="btn btn-primary btn-lg" type="reset"  onclick="clearSelectedWords()">清除</button> | ||||
|     </form> | ||||
|     {% if session.get['thisWord'] %} | ||||
|         <script type="text/javascript"> | ||||
|  | @ -160,12 +156,18 @@ | |||
| 
 | ||||
|     {% if d_len > 0 %} | ||||
|         <p> | ||||
| 
 | ||||
|             <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 %} | ||||
|  | @ -173,19 +175,21 @@ | |||
|                 {% 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> | ||||
|                 <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-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 --> | ||||
|                 </p> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         <input id="selected-words2" type="hidden" value="{{ words }}"> | ||||
| 
 | ||||
|     {% endif %} | ||||
| </div> | ||||
| {{ yml['footer'] | safe }} | ||||
|  | @ -194,6 +198,28 @@ | |||
|         <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 = { | ||||
|  | @ -219,11 +245,12 @@ | |||
|         elements.chooseCheckbox.checked = settings.chooseChecked; | ||||
|         elements.rangeComponent.value = settings.rangeValue; | ||||
|         elements.rangeValueDisplay.textContent = `${settings.rangeValue}x`; | ||||
|         elements.selectedWordsInput.value = settings.selectedWords; | ||||
| <!--        elements.selectedWordsInput.value = settings.selectedWords;--> | ||||
| 
 | ||||
|         // 刷新页面或进入页面时判断,若不是首篇文章,则上一篇按钮可见 | ||||
|         if (sessionStorage.getItem('pre_page_button') !== 'display' && sessionStorage.getItem('pre_page_button')) { | ||||
|             $('#load_pre_article').show(); | ||||
| 
 | ||||
|         // 刷新页面或进入页面时判断,若是首篇文章,则颜色为灰色 | ||||
|         if (sessionStorage.getItem('pre_page_button') === 'display' || !sessionStorage.getItem('pre_page_button')) { | ||||
|             $('#load_pre_article').addClass('gray-background'); | ||||
|         } | ||||
| 
 | ||||
|         // 事件监听器 | ||||
|  | @ -244,88 +271,168 @@ | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     function load_next_article() { | ||||
|         $("#load_next_article").prop("disabled", true) | ||||
|     function load_next_article(){ | ||||
|         $.ajax({ | ||||
|             url: '/get_next_article/{{username}}', | ||||
|             dataType: 'json', | ||||
|             success: function (data) { | ||||
|             success: function(data) { | ||||
|                 // 更新页面内容 | ||||
|                 if (data['today_article']) { | ||||
|                 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(); | ||||
|                 } | ||||
|             }, complete: function (xhr, status) { | ||||
|                 $("#load_next_article").prop("disabled", false) | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     function load_pre_article() { | ||||
|     function load_pre_article(){ | ||||
|         $.ajax({ | ||||
|             url: '/get_pre_article/{{username}}', | ||||
|             dataType: 'json', | ||||
|             success: function (data) { | ||||
|             success: function(data) { | ||||
|                 // 更新页面内容 | ||||
|                 if (data['today_article']) { | ||||
|                 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) { | ||||
|     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"]); | ||||
|         $('#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); | ||||
|         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); | ||||
|         setTimeout(() => {document.querySelector('#user_level').classList.remove('mark');}, 2000); | ||||
|     } | ||||
| 
 | ||||
|     <!-- 检查是否存在上一篇或下一篇,不存在则对应按钮隐藏--> | ||||
|     function check_pre(visited_articles) { | ||||
|         if ((visited_articles == '') || (visited_articles['index'] <= 0)) { | ||||
|             $('#load_pre_article').hide(); | ||||
|     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').show(); | ||||
|         }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(); | ||||
|     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") { | ||||
|         }else if(result_of_generate_article == "not found"){ | ||||
|             $('#found').hide(); | ||||
|             $('#not_found').show(); | ||||
|             $('#read_all').hide(); | ||||
|         } else { | ||||
|         }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: #{{ yml['highlight']['color'] }}; | ||||
|         color: red; | ||||
|         background-color: rgba(0, 0, 0, 0); | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,45 +1,52 @@ | |||
| <!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> | ||||
|      <p>取消勾选认识的单词</p> | ||||
|       <form method="post" action="/{{username}}/mark"> | ||||
|        <input type="submit" name="add-btn" value="加入我的生词簿"/> | ||||
|        {% for x in lst %} | ||||
|           {% set word = x[0]%} | ||||
|         <p> | ||||
|             <font color="grey">{{loop.index}}</font> | ||||
|             : | ||||
|             <a href='http://youdao.com/w/eng/{{word}}/#keyfrom=dict2.index' title={{word}}>{{word}}</a> | ||||
|             ({{x[1]}}) | ||||
|             <input type="checkbox" name="marked" value="{{word}}" checked> | ||||
|         </p> | ||||
|        <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> | ||||
| 
 | ||||
|        {% endfor %} | ||||
|        </form> | ||||
|     {{ yml['footer'] | safe }} | ||||
|     {% if yml['js']['bottom'] %} | ||||
|         {% for js in yml['js']['bottom'] %} | ||||
|             <script src="{{ js }}" ></script> | ||||
|         {% endfor %} | ||||
|     {% endif %} | ||||
| </body> | ||||
|                {% 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> | ||||
|  |  | |||
|  | @ -1,6 +1,9 @@ | |||
| import pytest | ||||
| import sqlite3 | ||||
| import time | ||||
| from selenium import webdriver | ||||
| from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | ||||
| 
 | ||||
| from pathlib import Path | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def URL(): | ||||
|  | @ -9,5 +12,24 @@ def URL(): | |||
| 
 | ||||
| @pytest.fixture | ||||
| def driver(): | ||||
|     my_driver = webdriver.Edge()  # uncomment this line if you wish to run the test on your laptop     | ||||
|     return my_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) | ||||
|  |  | |||
|  | @ -0,0 +1,33 @@ | |||
| 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,76 +1,31 @@ | |||
| # -*- 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/' | ||||
| import time | ||||
| from helper import signup | ||||
| 
 | ||||
| 
 | ||||
| def has_punctuation(s): | ||||
|     return [c for c in s if c in string.punctuation] != [] | ||||
|      | ||||
| def test_add_word(): | ||||
| def test_add_word(URL, driver): | ||||
|     try: | ||||
|         driver.get(HOME_PAGE) | ||||
|         assert 'English Pal -' in driver.page_source | ||||
|      | ||||
|         # login | ||||
|         elem = driver.find_element_by_link_text('登录') | ||||
|         elem.click() | ||||
|      | ||||
|         uname = 'lanhui' | ||||
|         password = 'l0ve1t' | ||||
|         elem = driver.find_element_by_name('username') | ||||
|         elem.send_keys(uname) | ||||
|      | ||||
|         elem = driver.find_element_by_name('password') | ||||
|         elem.send_keys(password) | ||||
|          | ||||
|         elem = driver.find_element_by_xpath('//form[1]/p[3]/input[1]') # 找到登录按钮 | ||||
|         elem.click() | ||||
|      | ||||
|         assert 'EnglishPal Study Room for ' + uname in  driver.title | ||||
|      | ||||
|         # get essay content | ||||
|         elem = driver.find_element_by_id('text-content') | ||||
|         essay_content = elem.text | ||||
|      | ||||
|         elem = driver.find_element_by_id('selected-words') | ||||
|         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()) | ||||
|         username, password = signup(URL, driver) # sign up a new account and automatically log in | ||||
|         time.sleep(1) | ||||
| 
 | ||||
|         # enter the word in the text area | ||||
|         elem = driver.find_element_by_id('selected-words') | ||||
|         word = 'devour' | ||||
|         elem.send_keys(word) | ||||
| 
 | ||||
|         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 = driver.find_element_by_xpath('//form[1]//button[1]') # 找到"把生词加入我的生词库"按钮 | ||||
|         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() | ||||
|  |  | |||
|  | @ -0,0 +1,95 @@ | |||
| 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)) | ||||
|  | @ -0,0 +1,88 @@ | |||
| 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 | ||||
|  | @ -0,0 +1,45 @@ | |||
| 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() | ||||
|  | @ -0,0 +1,55 @@ | |||
| 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() | ||||
|  | @ -0,0 +1,44 @@ | |||
| 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() | ||||
|  | @ -0,0 +1,39 @@ | |||
| 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() | ||||
|  | @ -0,0 +1,37 @@ | |||
| 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() | ||||
|  | @ -0,0 +1,58 @@ | |||
| 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() | ||||
|  | @ -0,0 +1,43 @@ | |||
| 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.") | ||||
|  | @ -0,0 +1,27 @@ | |||
| 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,85 +0,0 @@ | |||
| ''' Contributed by Lin Junhong et al. 2023-06.''' | ||||
| 
 | ||||
| from selenium import webdriver | ||||
| from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | ||||
| 
 | ||||
| from selenium.webdriver.support.ui import WebDriverWait | ||||
| from selenium.webdriver.support import expected_conditions as EC | ||||
| from selenium.common.exceptions import UnexpectedAlertPresentException, NoAlertPresentException | ||||
| import random, time | ||||
| import string | ||||
| 
 | ||||
| # 初始化webdriver | ||||
| # driver = webdriver.Remote('http://localhost:4444/wd/hub', DesiredCapabilities.CHROME) | ||||
| # driver.implicitly_wait(10) | ||||
| driver = webdriver.Chrome("C:\\Users\\12993\AppData\Local\Programs\Python\Python38\\chromedriver.exe") | ||||
| 
 | ||||
| 
 | ||||
| def test_next_article(): | ||||
|     try: | ||||
|         driver.get("http://118.25.96.118:90") | ||||
|         assert 'English Pal -' in driver.page_source | ||||
|         # login | ||||
|         elem = driver.find_element_by_link_text('登录') | ||||
|         elem.click() | ||||
| 
 | ||||
|         uname = 'abcdefg' | ||||
|         password = 'abcdefg' | ||||
|         elem = driver.find_element_by_id('username') | ||||
|         elem.send_keys(uname) | ||||
| 
 | ||||
|         elem = driver.find_element_by_id('password') | ||||
|         elem.send_keys(password) | ||||
|         elem = driver.find_element_by_xpath('/html/body/div/button')  # 找到登录按钮 | ||||
|         elem.click() | ||||
| 
 | ||||
|         time.sleep(0.5) | ||||
|         assert 'EnglishPal Study Room for ' + uname in driver.title | ||||
|         for i in range(50): | ||||
|             time.sleep(0.1) | ||||
|             # 找到固定按钮 | ||||
|             elem = driver.find_element_by_xpath('//*[@id="load_next_article"]') | ||||
|             elem.click() | ||||
|     except Exception as e: | ||||
|         print(e) | ||||
| 
 | ||||
| 
 | ||||
| def test_local_next_article(): | ||||
|     try: | ||||
|         driver.get("http://127.0.0.1:5000") | ||||
|         assert 'English Pal -' in driver.page_source | ||||
|         # login | ||||
|         elem = driver.find_element_by_link_text('注册') | ||||
|         elem.click() | ||||
| 
 | ||||
|         uname = 'abcdefg' | ||||
|         password = 'abcdefg' | ||||
|         elem = driver.find_element_by_id('username') | ||||
|         elem.send_keys(uname) | ||||
| 
 | ||||
|         elem = driver.find_element_by_id('password') | ||||
|         elem.send_keys(password) | ||||
| 
 | ||||
|         elem = driver.find_element_by_id('password2') | ||||
|         elem.send_keys(password) | ||||
| 
 | ||||
|         time.sleep(0.5) | ||||
| 
 | ||||
|         elem = driver.find_element_by_class_name('btn')  # 找到提交按钮 | ||||
|         elem.click() | ||||
|         time.sleep(0.5) | ||||
|         try: | ||||
|             WebDriverWait(driver, 1).until(EC.alert_is_present()) | ||||
|             driver.switch_to.alert.accept() | ||||
|         except (UnexpectedAlertPresentException, NoAlertPresentException): | ||||
|             pass | ||||
| 
 | ||||
|         time.sleep(0.5) | ||||
|         assert 'EnglishPal Study Room for ' + uname in driver.title | ||||
|         for i in range(50): | ||||
|             time.sleep(0.1) | ||||
|             # 找到固定按钮 | ||||
|             elem = driver.find_element_by_xpath('//*[@id="load_next_article"]') | ||||
|             elem.click() | ||||
|     except Exception as e: | ||||
|         print(e) | ||||
|  | @ -0,0 +1,43 @@ | |||
| ''' Contributed by Lin Junhong et al. 2023-06.''' | ||||
| 
 | ||||
| import requests | ||||
| import multiprocessing | ||||
| import time | ||||
| 
 | ||||
| def stress(username): | ||||
|     try: | ||||
|         data = { | ||||
|             'username': username, | ||||
|             'password': '123123' | ||||
|         } | ||||
|         headers = { | ||||
|             'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36 Edg/114.0.1823.51' | ||||
|         } | ||||
|         session = requests.session() | ||||
|         response = session.post(url='http://127.0.0.1:5000/signup', data=data, headers=headers) | ||||
|         print('Sign up ', response.status_code) | ||||
|         time.sleep(0.5) | ||||
|         response = session.post(url='http://127.0.0.1:5000/login', data=data, headers=headers) | ||||
|         print('Sign in ', response.status_code) | ||||
|         time.sleep(0.5) | ||||
|         response = session.get(url=f'http://127.0.0.1:5000/{username}/userpage', headers=headers) | ||||
|         print('User page', response.status_code) | ||||
|         time.sleep(0.5) | ||||
|         print(session.cookies) | ||||
|         for i in range(5): | ||||
|             response = session.get(url=f'http://127.0.0.1:5000/get_next_article/{username}', headers=headers, cookies=session.cookies) | ||||
|             time.sleep(0.5) | ||||
|             print(f'Next page ({i}) [{username}]') | ||||
|             print(response.status_code) | ||||
|             print(response.json()['today_article']['article_title']) | ||||
|     except Exception as e: | ||||
|         print(e) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     username = 'Learner' | ||||
|     pool = multiprocessing.Pool(processes=10) | ||||
|     for i in range(10): | ||||
|         pool.apply_async(stress, (f'{username}{i}',)) | ||||
|     pool.close() | ||||
|     pool.join() | ||||
|  | @ -0,0 +1,52 @@ | |||
| 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}"} | ||||
|  | @ -15,6 +15,9 @@ from wordfreqCMD import sort_in_descending_order | |||
| import pickle_idea | ||||
| import pickle_idea2 | ||||
| 
 | ||||
| import logging | ||||
| logging.basicConfig(filename='log.txt', format='%(asctime)s %(message)s', level=logging.DEBUG) | ||||
| 
 | ||||
| # 初始化蓝图 | ||||
| userService = Blueprint("user_bp", __name__) | ||||
| 
 | ||||
|  | @ -32,7 +35,9 @@ def get_next_article(username): | |||
|         else:  # 当前不为“null”,直接 index+=1 | ||||
|             visited_articles["index"] += 1 | ||||
|         session["visited_articles"] = visited_articles | ||||
|         logging.debug('/get_next_article: start calling get_today_arcile()') | ||||
|         visited_articles, today_article, result_of_generate_article = get_today_article(user_freq_record, session.get('visited_articles')) | ||||
|         logging.debug('/get_next_arcile: done.') | ||||
|         data = { | ||||
|             'visited_articles': visited_articles, | ||||
|             'today_article': today_article, | ||||
|  | @ -129,7 +134,7 @@ def userpage(username): | |||
|     user_freq_record = path_prefix + 'static/frequency/' + 'frequency_%s.pickle' % (username) | ||||
| 
 | ||||
|     if request.method == 'POST':  # when we submit a form | ||||
|         content = escape(request.form['content']) | ||||
|         content = request.form['content'] | ||||
|         f = WordFreq(content) | ||||
|         lst = f.get_freq() | ||||
|         return render_template('userpage_post.html',username=username,lst = lst, yml=Yaml.yml) | ||||
|  | @ -173,10 +178,17 @@ 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'): | ||||
|             lst.append((word, [get_time()])) | ||||
|             if not word in pickle_idea2.exclusion_lst and len(word) > 2: | ||||
|                 lst.append((word, [get_time()])) | ||||
|                 lst2.append(word) | ||||
|         d = pickle_idea2.merge_frequency(lst, lst_history) | ||||
|         pickle_idea2.save_frequency_to_pickle(d, user_freq_record) | ||||
|         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)) | ||||
|         return redirect(url_for('user_bp.userpage', username=username)) | ||||
|     else: | ||||
|         return 'Under construction' | ||||
|  |  | |||
|  | @ -4,11 +4,38 @@ | |||
| ########################################################################### | ||||
| 
 | ||||
| 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): | ||||
|     ''' | ||||
|     功能: 把字符串转成列表。 目的是得到每个单词的频率。 | ||||
|  | @ -39,7 +66,8 @@ def file2str(fname):#文件转字符 | |||
| 
 | ||||
| 
 | ||||
| def remove_punctuation(s): # 这里是s是形参 (parameter)。函数被调用时才给s赋值。 | ||||
|     special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|' # 把里面的字符都去掉 | ||||
|     special_characters = '\_©~<=>+/[]*&$%^@.,?!:;#()"“”—‘’{}|,。?!¥……()、《》【】:;·' # 把里面的字符都去掉 | ||||
|     s = html.unescape(s) # 将HTML实体转换为对应的字符,比如<会被识别为小于号 | ||||
|     for c in special_characters: | ||||
|         s = s.replace(c, ' ') # 防止出现把 apple,apple 移掉逗号后变成 appleapple 情况 | ||||
|     s = s.replace('--', ' ') | ||||
|  | @ -104,7 +132,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'): | ||||
|  |  | |||
							
								
								
									
										7
									
								
								build.sh
								
								
								
								
							
							
						
						
									
										7
									
								
								build.sh
								
								
								
								
							|  | @ -2,10 +2,7 @@ | |||
| 
 | ||||
| DEPLOYMENT_DIR=/home/lanhui/englishpal2/EnglishPal | ||||
| cd $DEPLOYMENT_DIR | ||||
| 
 | ||||
| # Install dependencies | ||||
| 
 | ||||
| pip3 install -r requirements.txt | ||||
| pwd | ||||
| 
 | ||||
| # Stop service | ||||
| sudo docker stop EnglishPal | ||||
|  | @ -15,7 +12,7 @@ sudo docker rm EnglishPal | |||
| sudo docker build -t englishpal . | ||||
| 
 | ||||
| # Run the application | ||||
| sudo docker run --restart=always -d --name EnglishPal -p 90:80 -v ${DEPLOYMENT_DIR}/app/static/frequency:/app/static/frequency -v ${DEPLOYMENT_DIR}/app/static/:/app/static/ -t englishpal  # for permanently saving data | ||||
| sudo docker run --restart=always -d --name EnglishPal -p 90:80  -v ${DEPLOYMENT_DIR}/app/static/frequency:/app/static/frequency --mount type=volume,src=englishpal-db,target=/app/db -t englishpal # for permanently saving data | ||||
| 
 | ||||
| # Save space.  Run it after sudo docker run | ||||
| sudo docker system prune -a -f | ||||
|  |  | |||
|  | @ -4,5 +4,6 @@ PyYAML~=6.0 | |||
| pony==0.7.16 | ||||
| snowballstemmer==2.2.0 | ||||
| Werkzeug==2.2.2 | ||||
| 
 | ||||
| pytest~=8.1.1 | ||||
| requests | ||||
| pytest~=8.1.1 | ||||
| Flask-HTTPAuth==4.4.0 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue